diff --git a/jam-ui/src/components/client/JKSessionJamTrackModal.js b/jam-ui/src/components/client/JKSessionJamTrackModal.js new file mode 100644 index 000000000..b629cb90e --- /dev/null +++ b/jam-ui/src/components/client/JKSessionJamTrackModal.js @@ -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 ( + + Open JamTrack + + + + + + + + + + + + + + + + + + {loading ? ( + + + + ) : jamTracks.length > 0 ? ( + jamTracks.map((jamTrack, index) => ( + + + + + )) + ) : ( + + + + )} + +
SongOriginal Artist
+
+ Loading... +
+ Searching... +
+ { + e.preventDefault(); + if (onJamTrackSelect) { + onJamTrackSelect(jamTrack); + } + toggle(); // Close modal after selection + }} + style={{ color: '#007bff', textDecoration: 'none' }} + > + {jamTrack.name} + + {jamTrack.original_artist}
+ {searchTerm || selected ? 'No results found. Try a different search.' : 'Search for JamTracks above.'} +
+
+ + + + +
+ ); +}; + +export default JKSessionJamTrackModal; diff --git a/jam-ui/src/components/client/JKSessionJamTrackStems.js b/jam-ui/src/components/client/JKSessionJamTrackStems.js new file mode 100644 index 000000000..837ac6776 --- /dev/null +++ b/jam-ui/src/components/client/JKSessionJamTrackStems.js @@ -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 ( +
+ {stemTracks.map((track, index) => ( + + ))} +
+ ); +}; + +export default JKSessionJamTrackStems; diff --git a/jam-ui/src/components/client/JKSessionMyTrack.js b/jam-ui/src/components/client/JKSessionMyTrack.js index 598d4e7f6..6feb7fecf 100644 --- a/jam-ui/src/components/client/JKSessionMyTrack.js +++ b/jam-ui/src/components/client/JKSessionMyTrack.js @@ -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
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')} > avatar
diff --git a/jam-ui/src/components/client/JKSessionOpenMenu.js b/jam-ui/src/components/client/JKSessionOpenMenu.js index f81c80ef8..e1d588443 100644 --- a/jam-ui/src/components/client/JKSessionOpenMenu.js +++ b/jam-ui/src/components/client/JKSessionOpenMenu.js @@ -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 diff --git a/jam-ui/src/components/client/JKSessionScreen.js b/jam-ui/src/components/client/JKSessionScreen.js index 23b6163d6..be9760c26 100644 --- a/jam-ui/src/components/client/JKSessionScreen.js +++ b/jam-ui/src/components/client/JKSessionScreen.js @@ -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 ( {!isConnected &&
Connecting to backend...
} @@ -721,7 +872,7 @@ const JKSessionScreen = () => { - + setShowJamTrackModal(true)} onMetronomeSelected={handleMetronomeSelected} /> @@ -729,7 +880,7 @@ const JKSessionScreen = () => { - +
Audio Inputs
@@ -759,6 +910,33 @@ const JKSessionScreen = () => {
+ {/* JamTrack Section */} + {selectedJamTrack && jamTrackStems.length > 0 && ( + <> +
+ + + )} @@ -989,6 +1167,12 @@ const JKSessionScreen = () => { /> )} + + setShowJamTrackModal(!showJamTrackModal)} + onJamTrackSelect={handleJamTrackSelect} + />
) } diff --git a/jam-ui/src/components/dashboard/JKDashboardMain.js b/jam-ui/src/components/dashboard/JKDashboardMain.js index 356825a0b..1ec166527 100644 --- a/jam-ui/src/components/dashboard/JKDashboardMain.js +++ b/jam-ui/src/components/dashboard/JKDashboardMain.js @@ -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() { + {/*Redirect*/} diff --git a/jam-ui/src/components/popups/JKPopupMediaControls.js b/jam-ui/src/components/popups/JKPopupMediaControls.js new file mode 100644 index 000000000..9275b3cb5 --- /dev/null +++ b/jam-ui/src/components/popups/JKPopupMediaControls.js @@ -0,0 +1,14 @@ +import React, { useEffect } from 'react' + +const JKPopupMediaControls = () => { + useEffect(() => { + console.log('JKPopupMediaControls mounted'); + alert('JKPopupMediaControls mounted'); + }, []) + + return ( +
JKPopupMediaControls
+ ) +} + +export default JKPopupMediaControls \ No newline at end of file diff --git a/jam-ui/src/context/GlobalContext.js b/jam-ui/src/context/GlobalContext.js index 77f7674a3..2d01f2e76 100644 --- a/jam-ui/src/context/GlobalContext.js +++ b/jam-ui/src/context/GlobalContext.js @@ -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 ( {children} diff --git a/jam-ui/src/context/JamClientContext.js b/jam-ui/src/context/JamClientContext.js index f0e61b538..5f72ba702 100644 --- a/jam-ui/src/context/JamClientContext.js +++ b/jam-ui/src/context/JamClientContext.js @@ -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 ( {children} diff --git a/jam-ui/src/helpers/rest.js b/jam-ui/src/helpers/rest.js index 257490065..8d1fb20f4 100644 --- a/jam-ui/src/helpers/rest.js +++ b/jam-ui/src/helpers/rest.js @@ -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)); + }); +}; diff --git a/jam-ui/src/hooks/useMetronomeState.js b/jam-ui/src/hooks/useMetronomeState.js new file mode 100644 index 000000000..f70bee7f2 --- /dev/null +++ b/jam-ui/src/hooks/useMetronomeState.js @@ -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; diff --git a/jam-ui/src/jamClientProxy.js b/jam-ui/src/jamClientProxy.js index 1c2c08c39..4a4f530d9 100644 --- a/jam-ui/src/jamClientProxy.js +++ b/jam-ui/src/jamClientProxy.js @@ -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']}`);