session jamtrack

user select jamtrack from the list
This commit is contained in:
Nuwan 2025-12-22 09:46:47 +05:30
parent 72f94fe023
commit 3a856cd01d
12 changed files with 654 additions and 13 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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

View File

@ -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>
)
}

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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}

View File

@ -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));
});
};

View File

@ -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;

View File

@ -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']}`);