parent
72f94fe023
commit
3a856cd01d
|
|
@ -0,0 +1,199 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button, Table, Row, Col } from 'reactstrap';
|
||||
import JKJamTracksAutoComplete from '../jamtracks/JKJamTracksAutoComplete';
|
||||
import { autocompleteJamTracks, getPurchasedJamTracks } from '../../helpers/rest';
|
||||
|
||||
const JKSessionJamTrackModal = ({ isOpen, toggle, onJamTrackSelect }) => {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [jamTracks, setJamTracks] = useState([]);
|
||||
const [selected, setSelected] = useState(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const page = useRef(1);
|
||||
const PER_PAGE = 10;
|
||||
|
||||
// Update input value when selected changes (similar to JKJamTracksFilter)
|
||||
useEffect(() => {
|
||||
if (selected) {
|
||||
const displayValue = selected.type === 'artist' ? selected.original_artist : selected.name;
|
||||
setInputValue(displayValue);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
const queryOptions = (selected) => {
|
||||
const options = {
|
||||
per_page: PER_PAGE,
|
||||
page: page.current,
|
||||
};
|
||||
|
||||
if (typeof selected === 'string') {
|
||||
options.search = selected;
|
||||
return options;
|
||||
}
|
||||
|
||||
if (selected.type === 'artist') {
|
||||
options.artist = selected.original_artist;
|
||||
} else {
|
||||
options.song = selected.name;
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
const handleOnSelect = async (selected) => {
|
||||
page.current = 1;
|
||||
setJamTracks([]);
|
||||
setSearchTerm('');
|
||||
setSelected(selected);
|
||||
setShowDropdown(false); // Hide dropdown after selection
|
||||
const params = queryOptions(selected);
|
||||
await fetchJamTracks(params);
|
||||
};
|
||||
|
||||
const handleOnEnter = async (queryStr) => {
|
||||
page.current = 1;
|
||||
setJamTracks([]);
|
||||
setSelected(null);
|
||||
setSearchTerm(queryStr);
|
||||
const params = queryOptions(queryStr);
|
||||
await fetchJamTracks(params);
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
if (inputValue.trim()) {
|
||||
handleOnEnter(inputValue.trim());
|
||||
}
|
||||
};
|
||||
|
||||
const fetchJamTracks = async (options) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const resp = await getPurchasedJamTracks(options);
|
||||
const data = await resp.json();
|
||||
setJamTracks(prev => [...prev, ...data.jamtracks]);
|
||||
page.current = page.current + 1;
|
||||
} catch (error) {
|
||||
console.error('Error fetching jam tracks:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load purchased JamTracks when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadPurchasedJamTracks();
|
||||
} else {
|
||||
// Reset state when modal closes
|
||||
setJamTracks([]);
|
||||
setInputValue('');
|
||||
setSelected(null);
|
||||
setSearchTerm('');
|
||||
page.current = 1;
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const loadPurchasedJamTracks = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const resp = await getPurchasedJamTracks({ start: 0, search: '', append: false });
|
||||
const data = await resp.json();
|
||||
setJamTracks(data.jamtracks || []);
|
||||
} catch (error) {
|
||||
console.error('Error loading purchased jam tracks:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShopJamTracks = () => {
|
||||
// TODO: Implement shop functionality - could open external link or navigate to shop page
|
||||
console.log('Shop JamTracks clicked');
|
||||
window.open('/jamtracks', '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} toggle={toggle} size="lg">
|
||||
<ModalHeader toggle={toggle}>Open JamTrack</ModalHeader>
|
||||
<ModalBody>
|
||||
<Row className="mb-3">
|
||||
<Col md={9}>
|
||||
<JKJamTracksAutoComplete
|
||||
fetchFunc={autocompleteJamTracks}
|
||||
onSelect={handleOnSelect}
|
||||
onEnter={handleOnEnter}
|
||||
showDropdown={showDropdown}
|
||||
setShowDropdown={setShowDropdown}
|
||||
inputValue={inputValue}
|
||||
setInputValue={setInputValue}
|
||||
inputPlaceholder="Search for JamTracks..."
|
||||
/>
|
||||
</Col>
|
||||
<Col md={3} className="align-items-end">
|
||||
<Button color="secondary" outline onClick={handleSearch} className="w-100">
|
||||
Search
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<Table bordered striped className="fs-0" >
|
||||
<thead className="bg-200 text-900">
|
||||
<tr>
|
||||
<th>Song</th>
|
||||
<th>Original Artist</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan="2" className="text-center">
|
||||
<div className="spinner-border spinner-border-sm" role="status">
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
<span className="ml-2">Searching...</span>
|
||||
</td>
|
||||
</tr>
|
||||
) : jamTracks.length > 0 ? (
|
||||
jamTracks.map((jamTrack, index) => (
|
||||
<tr key={jamTrack.id || index}>
|
||||
<td>
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (onJamTrackSelect) {
|
||||
onJamTrackSelect(jamTrack);
|
||||
}
|
||||
toggle(); // Close modal after selection
|
||||
}}
|
||||
style={{ color: '#007bff', textDecoration: 'none' }}
|
||||
>
|
||||
{jamTrack.name}
|
||||
</a>
|
||||
</td>
|
||||
<td>{jamTrack.original_artist}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="2" className="text-center text-muted">
|
||||
{searchTerm || selected ? 'No results found. Try a different search.' : 'Search for JamTracks above.'}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" onClick={handleShopJamTracks}>
|
||||
Shop JamTracks
|
||||
</Button>
|
||||
<Button color="secondary" outline onClick={toggle}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default JKSessionJamTrackModal;
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import JKSessionAudioInputs from './JKSessionAudioInputs';
|
||||
import { getInstrumentIcon45 } from '../../helpers/utils';
|
||||
|
||||
const JKSessionJamTrackStems = ({ jamTrackStems, mixerHelper }) => {
|
||||
const stemTracks = useMemo(() => {
|
||||
if (!jamTrackStems || jamTrackStems.length === 0) return [];
|
||||
|
||||
return jamTrackStems.map((stem, index) => {
|
||||
// Create a track object similar to what JKSessionRemoteTracks creates
|
||||
const track = {
|
||||
// Use stem properties
|
||||
id: stem.id,
|
||||
part: stem.part,
|
||||
instrument: stem.instrument,
|
||||
track_type: stem.track_type,
|
||||
position: stem.position,
|
||||
// Add client_track_id for compatibility
|
||||
client_track_id: `jamtrack-stem-${stem.id}`,
|
||||
// Mock some properties that JKSessionMyTrack expects
|
||||
instrument_name: stem.part || stem.instrument
|
||||
};
|
||||
|
||||
// Create mixer data (placeholder for now)
|
||||
const mixerData = {
|
||||
mixer: null, // No actual mixer for jam track stems yet
|
||||
hasMixer: false
|
||||
};
|
||||
|
||||
const instrumentIcon = getInstrumentIcon45(stem.instrument) || '/assets/content/icon_instrument_guitar45.png';
|
||||
const trackName = stem.part || stem.instrument || `Stem ${index + 1}`;
|
||||
|
||||
return {
|
||||
track,
|
||||
mixerFinder: [`jamtrack-stem-${stem.id}`, track, false],
|
||||
mixers: mixerData,
|
||||
hasMixer: false,
|
||||
name: 'JamTrack',
|
||||
trackName,
|
||||
instrumentIcon,
|
||||
photoUrl: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDUiIGhlaWdodD0iNDUiIHZpZXdCb3g9IjAgMCA0NSA0NSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ1IiBoZWlnaHQ9IjQ1IiBmaWxsPSJ0cmFuc3BhcmVudCIvPgo8L3N2Zz4=', // Transparent placeholder to maintain height
|
||||
clientId: `jamtrack-stem-${stem.id}`,
|
||||
connStatsClientId: `jamtrack-stem-${stem.id}`,
|
||||
// Additional properties for JKSessionMyTrack
|
||||
isJamTrackStem: true,
|
||||
hideAvatar: true // Custom prop to hide avatar
|
||||
};
|
||||
});
|
||||
}, [jamTrackStems]);
|
||||
|
||||
if (!stemTracks || stemTracks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='d-flex' style={{ gap: '1rem' }}>
|
||||
{stemTracks.map((track, index) => (
|
||||
<JKSessionAudioInputs
|
||||
key={`stem-${track.track.id || index}`}
|
||||
myTracks={[track]} // Each stem gets its own JKSessionAudioInputs
|
||||
chat={null}
|
||||
mixerHelper={mixerHelper}
|
||||
isRemote={true} // Treat as remote since they're not user tracks
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JKSessionJamTrackStems;
|
||||
|
|
@ -12,7 +12,7 @@ import { getInstrumentName } from '../../helpers/utils';
|
|||
import { ASSIGNMENT } from '../../helpers/globals';
|
||||
import './JKSessionMyTrack.css';
|
||||
|
||||
const JKSessionMyTrack = ({ track, mixers, hasMixer, name, trackName, instrumentIcon, photoUrl, clientId, connStatsClientId, mode, isChat = false, isRemote = false }) => {
|
||||
const JKSessionMyTrack = ({ track, mixers, hasMixer, name, trackName, instrumentIcon, photoUrl, clientId, connStatsClientId, mode, isChat = false, isRemote = false, hideAvatar = false }) => {
|
||||
const mixerHelper = useMixersContext();
|
||||
const jamClient = useJamClient();
|
||||
const { convertPanToPercent } = usePanHelpers();
|
||||
|
|
@ -72,8 +72,11 @@ const JKSessionMyTrack = ({ track, mixers, hasMixer, name, trackName, instrument
|
|||
</div>
|
||||
<div
|
||||
className="track-avatar"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => window.open(`https://profile.jamkazam.com/user/${clientId}`, '_blank')}
|
||||
style={{
|
||||
cursor: hideAvatar ? 'default' : 'pointer',
|
||||
visibility: hideAvatar ? 'hidden' : 'visible'
|
||||
}}
|
||||
onClick={hideAvatar ? undefined : () => window.open(`https://profile.jamkazam.com/user/${clientId}`, '_blank')}
|
||||
>
|
||||
<img src={photoUrl} alt="avatar" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect, useContext } from 'react';
|
|||
import { createPortal } from 'react-dom';
|
||||
import { useJamClient } from '../../context/JamClientContext';
|
||||
|
||||
const JKSessionOpenMenu = ({ onBackingTrackSelected }) => {
|
||||
const JKSessionOpenMenu = ({ onBackingTrackSelected, onJamTrackSelected, onMetronomeSelected }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const buttonRef = useRef(null);
|
||||
const menuRef = useRef(null);
|
||||
|
|
@ -14,7 +14,11 @@ const JKSessionOpenMenu = ({ onBackingTrackSelected }) => {
|
|||
console.log(`Selected: ${item}`);
|
||||
setIsOpen(false);
|
||||
|
||||
if (item === 'Backing Tracks') {
|
||||
if (item === 'JamTracks') {
|
||||
if (onJamTrackSelected) {
|
||||
onJamTrackSelected();
|
||||
}
|
||||
} else if (item === 'Backing Tracks') {
|
||||
try {
|
||||
// Set up callback for when user selects a backing track file
|
||||
window.JK = window.JK || {};
|
||||
|
|
@ -30,8 +34,15 @@ const JKSessionOpenMenu = ({ onBackingTrackSelected }) => {
|
|||
} catch (error) {
|
||||
console.error('Error opening backing track dialog:', error);
|
||||
}
|
||||
} else if (item === 'Metronome') {
|
||||
try {
|
||||
if (onMetronomeSelected) {
|
||||
onMetronomeSelected();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error opening metronome:', error);
|
||||
}
|
||||
}
|
||||
// TODO: Implement other menu item actions
|
||||
};
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { useAuth } from '../../context/UserAuth';
|
|||
|
||||
import { dkeys } from '../../helpers/utils.js';
|
||||
|
||||
import { getSessionHistory, getSession, joinSession as joinSessionRest, updateSessionSettings, getFriends, startRecording, stopRecording, submitSessionFeedback, getVideoConferencingRoomUrl } from '../../helpers/rest';
|
||||
import { getSessionHistory, getSession, joinSession as joinSessionRest, updateSessionSettings, getFriends, startRecording, stopRecording, submitSessionFeedback, getVideoConferencingRoomUrl, getJamTrack, closeJamTrack, openMetronome } from '../../helpers/rest';
|
||||
|
||||
import { CLIENT_ROLE, RECORD_TYPE_AUDIO, RECORD_TYPE_BOTH } from '../../helpers/globals';
|
||||
import { MessageType } from '../../helpers/MessageFactory.js';
|
||||
|
|
@ -35,6 +35,8 @@ import JKSessionInviteModal from './JKSessionInviteModal.js';
|
|||
import JKSessionVolumeModal from './JKSessionVolumeModal.js';
|
||||
import JKSessionRecordingModal from './JKSessionRecordingModal.js';
|
||||
import JKSessionLeaveModal from './JKSessionLeaveModal.js';
|
||||
import JKSessionJamTrackModal from './JKSessionJamTrackModal.js';
|
||||
import JKSessionJamTrackStems from './JKSessionJamTrackStems.js';
|
||||
import JKSessionOpenMenu from './JKSessionOpenMenu.js';
|
||||
import WindowPortal from '../common/WindowPortal.js';
|
||||
import JKSessionBackingTrackPlayer from './JKSessionBackingTrackPlayer.js';
|
||||
|
|
@ -64,7 +66,7 @@ const JKSessionScreen = () => {
|
|||
server,
|
||||
registerMessageCallback } = useJamServerContext();
|
||||
const { currentSession, setCurrentSession, currentSessionIdRef, setCurrentSessionId, inSession } = useCurrentSessionContext();
|
||||
const { globalObject } = useGlobalContext();
|
||||
const { globalObject, metronomeState, closeMetronome, resetMetronome } = useGlobalContext();
|
||||
const { getCurrentRecordingState, reset: resetRecordingState, currentlyRecording } = useRecordingHelpers();
|
||||
const { SessionPageEnter } = useSessionUtils();
|
||||
|
||||
|
|
@ -118,6 +120,13 @@ const JKSessionScreen = () => {
|
|||
const [showBackingTrackPopup, setShowBackingTrackPopup] = useState(false);
|
||||
const [backingTrackData, setBackingTrackData] = useState(null);
|
||||
|
||||
//state for jam track modal
|
||||
const [showJamTrackModal, setShowJamTrackModal] = useState(false);
|
||||
|
||||
//state for selected jam track and stems
|
||||
const [selectedJamTrack, setSelectedJamTrack] = useState(null);
|
||||
const [jamTrackStems, setJamTrackStems] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected || !jamClient) return;
|
||||
console.debug("JKSessionScreen: -DEBUG- isConnected changed to true");
|
||||
|
|
@ -583,6 +592,12 @@ const JKSessionScreen = () => {
|
|||
try {
|
||||
setLeaveLoading(true);
|
||||
|
||||
// Close metronome if open before leaving
|
||||
if (metronomeState.isOpen) {
|
||||
console.log('Closing metronome before leaving session');
|
||||
closeMetronome();
|
||||
}
|
||||
|
||||
// Submit feedback to backend first
|
||||
const clientId = server.clientId;
|
||||
const backendDetails = jamClient.getAllClientsStateMap ? jamClient.getAllClientsStateMap() : {};
|
||||
|
|
@ -609,6 +624,17 @@ const JKSessionScreen = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// Cleanup metronome state when leaving session
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Reset metronome state when component unmounts (session ends)
|
||||
if (metronomeState.isOpen) {
|
||||
console.log('Resetting metronome state on session cleanup');
|
||||
resetMetronome();
|
||||
}
|
||||
};
|
||||
}, [metronomeState.isOpen, resetMetronome]);
|
||||
|
||||
// Check if user can use video (subscription/permission check)
|
||||
const canVideo = () => {
|
||||
// This would need to be implemented based on user subscription logic
|
||||
|
|
@ -703,6 +729,131 @@ const JKSessionScreen = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleJamTrackSelect = async (jamTrack) => {
|
||||
console.log('Jam track selected:', jamTrack);
|
||||
try {
|
||||
// Fetch jam track details with stems
|
||||
const response = await getJamTrack({ id: jamTrack.id });
|
||||
const jamTrackData = await response.json();
|
||||
|
||||
console.log('Jam track data:', jamTrackData);
|
||||
|
||||
// Set the selected jam track and stems
|
||||
setSelectedJamTrack(jamTrackData);
|
||||
setJamTrackStems(jamTrackData.tracks || []);
|
||||
|
||||
toast.success(`Loaded JamTrack: ${jamTrackData.name}`);
|
||||
} catch (error) {
|
||||
console.error('Error loading jam track:', error);
|
||||
toast.error('Failed to load JamTrack');
|
||||
}
|
||||
};
|
||||
|
||||
const handleJamTrackClose = async () => {
|
||||
console.log('Closing jam track');
|
||||
try {
|
||||
// Call the close jam track API
|
||||
await closeJamTrack({ id: currentSession.id });
|
||||
|
||||
// Clear the selected jam track and stems
|
||||
setSelectedJamTrack(null);
|
||||
setJamTrackStems([]);
|
||||
|
||||
toast.success('JamTrack closed successfully');
|
||||
} catch (error) {
|
||||
console.error('Error closing jam track:', error);
|
||||
toast.error('Failed to close JamTrack');
|
||||
}
|
||||
};
|
||||
|
||||
const handleMetronomeSelected = async () => {
|
||||
console.log('Opening metronome');
|
||||
try {
|
||||
// Check if currently recording - can't open metronome while recording
|
||||
if (currentlyRecording) {
|
||||
toast.warning("You can't open a metronome while recording.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for unstable NTP clocks (like legacy implementation)
|
||||
const unstableClocks = await checkUnstableClocks();
|
||||
if (currentSession.participants && currentSession.participants.length > 1 && unstableClocks.length > 0) {
|
||||
const names = unstableClocks.join(", ");
|
||||
toast.warning(`Couldn't open metronome due to unstable clocks: ${names}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Track analytics (like legacy SessionStore)
|
||||
if (window.stats && window.stats.write) {
|
||||
const data = {
|
||||
value: 1,
|
||||
session_size: currentSession.participants?.length || 1,
|
||||
user_id: currentUser?.id,
|
||||
user_name: currentUser?.name
|
||||
};
|
||||
window.stats.write('web.metronome.open', data);
|
||||
}
|
||||
|
||||
// Stop any current playback first (like legacy MixerStore)
|
||||
await jamClient.SessionStopPlay();
|
||||
|
||||
// Open the metronome with default settings
|
||||
const bpm = 120;
|
||||
const sound = "Beep";
|
||||
const meter = 1;
|
||||
const mode = 0;
|
||||
|
||||
console.log(`Opening metronome with bpm: ${bpm}, sound: ${sound}, meter: ${meter}, mode: ${mode}`);
|
||||
|
||||
// Inform server about metronome opening (like legacy SessionStore)
|
||||
await openMetronome({ id: currentSession.id });
|
||||
|
||||
// Start the metronome audio (backend will handle GUI via callback)
|
||||
|
||||
//alert('About to start metronome');
|
||||
const result = await jamClient.SessionOpenMetronome(bpm, sound, meter, mode);
|
||||
//alert('Metronome is started ' + JSON.stringify(result));
|
||||
|
||||
toast.success('Metronome opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Error opening metronome:', error);
|
||||
toast.error('Failed to open metronome');
|
||||
}
|
||||
};
|
||||
|
||||
const checkUnstableClocks = async () => {
|
||||
try {
|
||||
const unstable = [];
|
||||
|
||||
// Check current user's NTP stability
|
||||
const myState = await jamClient.getMyNetworkState();
|
||||
if (!myState.ntp_stable) {
|
||||
unstable.push('this computer');
|
||||
}
|
||||
|
||||
// Check other participants' NTP stability
|
||||
if (currentSession.participants) {
|
||||
for (const participant of currentSession.participants) {
|
||||
if (participant.client_id !== server.clientId) {
|
||||
try {
|
||||
const peerState = await jamClient.getPeerState(participant.client_id);
|
||||
if (!peerState.ntp_stable) {
|
||||
unstable.push(participant.user.first_name + ' ' + participant.user.last_name);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors for individual peer checks
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return unstable;
|
||||
} catch (error) {
|
||||
console.error('Error checking NTP stability:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
{!isConnected && <div className='d-flex align-items-center'>Connecting to backend...</div>}
|
||||
|
|
@ -721,7 +872,7 @@ const JKSessionScreen = () => {
|
|||
</Button>
|
||||
<Button className='btn-custom-outline' outline size="md" onClick={() => setShowRecordingModal(true)}>Record</Button>
|
||||
<Button className='btn-custom-outline' outline size="md" onClick={ handleBroadcast}>Broadcast</Button>
|
||||
<JKSessionOpenMenu onBackingTrackSelected={handleBackingTrackSelected} />
|
||||
<JKSessionOpenMenu onBackingTrackSelected={handleBackingTrackSelected} onJamTrackSelected={() => setShowJamTrackModal(true)} onMetronomeSelected={handleMetronomeSelected} />
|
||||
<Button className='btn-custom-outline' outline size="md">Chat</Button>
|
||||
<Button className='btn-custom-outline' outline size="md">Attach</Button>
|
||||
<Button className='btn-custom-outline' outline size="md">Resync</Button>
|
||||
|
|
@ -729,7 +880,7 @@ const JKSessionScreen = () => {
|
|||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardBody className="pl-4" style={{ backgroundColor: '#edf2f9f5' }}>
|
||||
<CardBody className="pl-4" style={{ backgroundColor: '#edf2f9f5', overflowX: 'auto', width: '100%' }}>
|
||||
<div className='d-flex' style={{ gap: '1rem' }}>
|
||||
<div className='audioInputs'>
|
||||
<h5>Audio Inputs <FontAwesomeIcon icon="question-circle" id="audioInputsTooltip" className="ml-2" style={{ cursor: 'pointer' }} /></h5>
|
||||
|
|
@ -759,6 +910,33 @@ const JKSessionScreen = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* JamTrack Section */}
|
||||
{selectedJamTrack && jamTrackStems.length > 0 && (
|
||||
<>
|
||||
<div style={{ borderLeft: '1px solid #ddd', paddingLeft: '1rem' }}></div>
|
||||
<div className='jamTrack'>
|
||||
<h5>
|
||||
JamTrack: {selectedJamTrack.name}
|
||||
<a
|
||||
href="#"
|
||||
className="text-muted ml-2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleJamTrackClose();
|
||||
}}
|
||||
style={{ fontSize: '1.2em', textDecoration: 'none' }}
|
||||
title="Close JamTrack"
|
||||
>
|
||||
<FontAwesomeIcon icon="times" /> Close
|
||||
</a>
|
||||
</h5>
|
||||
<JKSessionJamTrackStems
|
||||
jamTrackStems={jamTrackStems}
|
||||
mixerHelper={mixerHelper}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -989,6 +1167,12 @@ const JKSessionScreen = () => {
|
|||
/>
|
||||
</WindowPortal>
|
||||
)}
|
||||
|
||||
<JKSessionJamTrackModal
|
||||
isOpen={showJamTrackModal}
|
||||
toggle={() => setShowJamTrackModal(!showJamTrackModal)}
|
||||
onJamTrackSelect={handleJamTrackSelect}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import JKPayPalConfirmation from '../shopping-cart/JKPayPalConfirmation';
|
|||
|
||||
import JKUnsubscribe from '../public/JKUnsubscribe';
|
||||
import JKConfirmEmailChange from '../public/JKConfirmEmailChange';
|
||||
import JKPopupMediaControls from '../popups/JKPopupMediaControls';
|
||||
|
||||
|
||||
//import loadable from '@loadable/component';
|
||||
|
|
@ -324,6 +325,7 @@ function JKDashboardMain() {
|
|||
<PrivateRoute path="/applaunch" component={JKAppLaunch} />
|
||||
<PrivateRoute path="/unsubscribe/:tok" exact component={JKUnsubscribe} />
|
||||
<PrivateRoute path="/confirm-email-change" exact component={JKConfirmEmailChange} />
|
||||
<PrivateRoute path="/popups/media-controls" exact component={JKPopupMediaControls} />
|
||||
{/*Redirect*/}
|
||||
<Redirect to="/errors/404" />
|
||||
</Switch>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import React, { useEffect } from 'react'
|
||||
|
||||
const JKPopupMediaControls = () => {
|
||||
useEffect(() => {
|
||||
console.log('JKPopupMediaControls mounted');
|
||||
alert('JKPopupMediaControls mounted');
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>JKPopupMediaControls</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default JKPopupMediaControls
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { createContext, useState } from 'react';
|
||||
|
||||
import React, { createContext, useState, useCallback, useEffect } from 'react';
|
||||
import useMetronomeState from '../hooks/useMetronomeState';
|
||||
|
||||
// Create a global context
|
||||
export const GlobalContext = createContext({});
|
||||
|
|
@ -31,12 +31,64 @@ export const GlobalProvider = ({ children }) => {
|
|||
|
||||
const [videoEnabled, setVideoEnabled] = useState(false);
|
||||
|
||||
// Metronome state management
|
||||
const {
|
||||
metronomeState,
|
||||
updateMetronomeState,
|
||||
openMetronome,
|
||||
closeMetronome,
|
||||
resetMetronome
|
||||
} = useMetronomeState();
|
||||
|
||||
// Metronome callback handler - called by backend via execute_script
|
||||
const handleMetronomeCallback2 = useCallback((args) => {
|
||||
console.log('Metronome callback received:', args);
|
||||
// Backend sends: { bpm, cricket, meter, playback, sound }
|
||||
updateMetronomeState({
|
||||
...args,
|
||||
isOpen: true // Backend callback means metronome is open
|
||||
});
|
||||
}, [updateMetronomeState]);
|
||||
|
||||
// Register the callback globally when component mounts
|
||||
useEffect(() => {
|
||||
if (!globalObject.JK) {
|
||||
setGlobalObject(prev => ({
|
||||
...prev,
|
||||
JK: {}
|
||||
}));
|
||||
}
|
||||
|
||||
// Ensure JK object exists on window
|
||||
if (!window.JK) {
|
||||
window.JK = {};
|
||||
}
|
||||
|
||||
// Register the callback
|
||||
window.JK.HandleMetronomeCallback2 = handleMetronomeCallback2;
|
||||
|
||||
// Also register on globalObject for consistency
|
||||
setGlobalObject(prev => ({
|
||||
...prev,
|
||||
JK: {
|
||||
...prev.JK,
|
||||
HandleMetronomeCallback2: handleMetronomeCallback2
|
||||
}
|
||||
}));
|
||||
}, [handleMetronomeCallback2]);
|
||||
|
||||
return (
|
||||
<GlobalContext.Provider value={{
|
||||
trackVolumeObject,
|
||||
setTrackVolumeObject,
|
||||
globalObject,
|
||||
setGlobalObject,
|
||||
// Metronome state and functions
|
||||
metronomeState,
|
||||
updateMetronomeState,
|
||||
openMetronome,
|
||||
closeMetronome,
|
||||
resetMetronome,
|
||||
}}>
|
||||
{children}
|
||||
</GlobalContext.Provider>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { createContext, useContext, useRef } from 'react';
|
||||
import React, { createContext, useContext, useRef, useEffect } from 'react';
|
||||
import JamClientProxy from '../jamClientProxy';
|
||||
|
||||
import { FakeJamClientProxy } from '../fakeJamClientProxy';
|
||||
|
|
@ -40,6 +40,15 @@ export const JamClientProvider = ({ children }) => {
|
|||
const proxy = new JamClientProxy(app, console, globalObject);
|
||||
proxyRef.current = proxy.init();
|
||||
}
|
||||
|
||||
// Register metronome callback with jamClient when it's available
|
||||
useEffect(() => {
|
||||
if (proxyRef.current && proxyRef.current.setMetronomeOpenCallback) {
|
||||
console.log('Registering metronome callback with jamClient');
|
||||
proxyRef.current.setMetronomeOpenCallback("JK.HandleMetronomeCallback2");
|
||||
}
|
||||
}, [proxyRef.current]);
|
||||
|
||||
return (
|
||||
<JamClientContext.Provider value={proxyRef.current}>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -918,3 +918,27 @@ export const getVideoConferencingRoomUrl = (musicSessionId) => {
|
|||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
export const closeJamTrack = options => {
|
||||
const { id, ...rest } = options;
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/sessions/${id}/jam_tracks/close`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(rest)
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const openMetronome = options => {
|
||||
const { id, ...rest } = options;
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/sessions/${id}/metronome/open`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(rest)
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
|
||||
const METRO_SOUND_LOOKUP = {
|
||||
0: "BuiltIn",
|
||||
1: "SineWave",
|
||||
2: "Beep",
|
||||
3: "Click",
|
||||
4: "Kick",
|
||||
5: "Snare",
|
||||
6: "MetroFile"
|
||||
};
|
||||
|
||||
const useMetronomeState = () => {
|
||||
const [metronomeState, setMetronomeState] = useState({
|
||||
isOpen: false,
|
||||
bpm: 120,
|
||||
cricket: false,
|
||||
meter: 1,
|
||||
playback: 1,
|
||||
sound: 2,
|
||||
soundName: "Beep"
|
||||
});
|
||||
|
||||
const updateMetronomeState = useCallback((updates) => {
|
||||
setMetronomeState(prev => {
|
||||
const newState = { ...prev, ...updates };
|
||||
|
||||
// Update sound name based on sound number
|
||||
if (updates.sound !== undefined) {
|
||||
newState.soundName = METRO_SOUND_LOOKUP[updates.sound] || "Beep";
|
||||
}
|
||||
|
||||
return newState;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const openMetronome = useCallback((settings = {}) => {
|
||||
updateMetronomeState({
|
||||
isOpen: true,
|
||||
...settings
|
||||
});
|
||||
}, [updateMetronomeState]);
|
||||
|
||||
const closeMetronome = useCallback(() => {
|
||||
setMetronomeState(prev => ({
|
||||
...prev,
|
||||
isOpen: false
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const resetMetronome = useCallback(() => {
|
||||
setMetronomeState({
|
||||
isOpen: false,
|
||||
bpm: 120,
|
||||
cricket: false,
|
||||
meter: 1,
|
||||
playback: 1,
|
||||
sound: 2,
|
||||
soundName: "Beep"
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
metronomeState,
|
||||
updateMetronomeState,
|
||||
openMetronome,
|
||||
closeMetronome,
|
||||
resetMetronome
|
||||
};
|
||||
};
|
||||
|
||||
export default useMetronomeState;
|
||||
|
|
@ -504,6 +504,7 @@ class JamClientProxy {
|
|||
case '3006': // execute_script
|
||||
try {
|
||||
// eslint-disable-next-line no-eval
|
||||
console.log(`[jamClientProxy] execute_script: ${response['execute_script']}`);
|
||||
eval(response['execute_script']);
|
||||
} catch (error) {
|
||||
this.logger.log(`[jamClientProxy] error: execute_script: ${response['execute_script']}`);
|
||||
|
|
|
|||
Loading…
Reference in New Issue