diff --git a/jam-ui/src/components/client/JKSessionMetronomePlayer.css b/jam-ui/src/components/client/JKSessionMetronomePlayer.css index eb40f67e5..541d77b9d 100644 --- a/jam-ui/src/components/client/JKSessionMetronomePlayer.css +++ b/jam-ui/src/components/client/JKSessionMetronomePlayer.css @@ -1,23 +1,24 @@ .metronome-player-popup { - padding: 20px; + padding: 24px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", sans-serif; - background-color: #f8f9fa; - min-width: 400px; + background-color: #ffffff; + min-width: 450px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .metronome-player-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 20px; - padding-bottom: 10px; - border-bottom: 1px solid #dee2e6; + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 2px solid #dee2e6; } .metronome-player-header h3 { margin: 0; - font-size: 1.25rem; - font-weight: 600; + font-size: 1.4rem; + font-weight: 700; color: #212529; } @@ -49,7 +50,74 @@ .metronome-player-controls { display: flex; flex-direction: column; - gap: 20px; + gap: 24px; +} + +/* Playback controls (Play/Stop buttons) */ +.metronome-playback-controls { + display: flex; + gap: 12px; + padding-bottom: 16px; + border-bottom: 1px solid #dee2e6; +} + +.metronome-play-btn, +.metronome-stop-btn { + flex: 1; + padding: 12px 20px; + font-size: 1rem; + font-weight: 600; + border-radius: 6px; + transition: all 0.2s; + border: none; +} + +.metronome-play-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.metronome-stop-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Form controls with labels on left, inputs on right */ +.metronome-form-controls { + display: flex; + flex-direction: column; + gap: 16px; +} + +.metronome-form-row { + display: grid; + grid-template-columns: 120px 1fr; + align-items: center; + gap: 16px; +} + +.metronome-form-label { + font-weight: 600; + font-size: 0.9rem; + color: #495057; + text-align: right; + margin: 0; +} + +/* Close button container */ +.metronome-close-container { + display: flex; + justify-content: center; + padding-top: 16px; + border-top: 1px solid #dee2e6; +} + +.metronome-close-button { + min-width: 120px; + padding: 10px 24px; + font-size: 0.9rem; + font-weight: 600; + border-radius: 6px; } .metronome-error { @@ -61,6 +129,7 @@ font-size: 0.875rem; } +/* Legacy styles - no longer used but kept for backwards compatibility */ .metronome-control-group { display: flex; flex-direction: column; @@ -74,90 +143,25 @@ margin: 0; } -.metronome-tempo-controls { - display: flex; - gap: 12px; - align-items: center; -} - -.metronome-slider { - flex: 1; - height: 6px; - border-radius: 3px; - background: #dee2e6; - outline: none; - -webkit-appearance: none; -} - -.metronome-slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 18px; - height: 18px; - border-radius: 50%; - background: #007bff; - cursor: pointer; - transition: background 0.2s; -} - -.metronome-slider::-webkit-slider-thumb:hover { - background: #0056b3; -} - -.metronome-slider::-moz-range-thumb { - width: 18px; - height: 18px; - border-radius: 50%; - background: #007bff; - cursor: pointer; - border: none; - transition: background 0.2s; -} - -.metronome-slider::-moz-range-thumb:hover { - background: #0056b3; -} - -.metronome-slider:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.metronome-input { - width: 80px; - padding: 6px 10px; - border: 1px solid #ced4da; - border-radius: 4px; - font-size: 0.875rem; - text-align: center; -} - -.metronome-input:focus { - outline: none; - border-color: #007bff; - box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); -} - -.metronome-input:disabled { - background-color: #e9ecef; - opacity: 0.6; - cursor: not-allowed; -} - .metronome-select { - padding: 8px 12px; - border: 1px solid #ced4da; - border-radius: 4px; - font-size: 0.875rem; + padding: 10px 14px; + border: 2px solid #ced4da; + border-radius: 6px; + font-size: 0.95rem; background-color: #fff; cursor: pointer; - transition: border-color 0.2s; + transition: all 0.2s; + width: 100%; +} + +.metronome-select:hover { + border-color: #007bff; } .metronome-select:focus { outline: none; border-color: #007bff; - box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.15); } .metronome-select:disabled { @@ -166,43 +170,7 @@ cursor: not-allowed; } -.metronome-checkbox-label { - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; - font-size: 0.875rem; - color: #495057; - margin: 0; -} - -.metronome-checkbox-label input[type="checkbox"] { - width: 18px; - height: 18px; - cursor: pointer; -} - -.metronome-checkbox-label input[type="checkbox"]:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.metronome-actions { - display: flex; - gap: 10px; - margin-top: 10px; - padding-top: 20px; - border-top: 1px solid #dee2e6; -} - -.metronome-actions button { - flex: 1; - padding: 10px 16px; - font-size: 0.875rem; - font-weight: 600; - border-radius: 4px; - transition: all 0.2s; -} +/* Legacy checkbox and actions styles - removed in redesign */ /* Modal-specific styles */ .metronome-player-modal .modal-content { diff --git a/jam-ui/src/components/client/JKSessionMetronomePlayer.js b/jam-ui/src/components/client/JKSessionMetronomePlayer.js index e7b075b3a..68f396f99 100644 --- a/jam-ui/src/components/client/JKSessionMetronomePlayer.js +++ b/jam-ui/src/components/client/JKSessionMetronomePlayer.js @@ -38,7 +38,7 @@ const JKSessionMetronomePlayer = ({ const [sound, setSound] = useState(2); // Default: Beep const [meter, setMeter] = useState(1); const [cricket, setCricket] = useState(false); - const [isApplying, setIsApplying] = useState(false); + const [isPlaying, setIsPlaying] = useState(false); const [error, setError] = useState(null); // Visibility tracking for performance optimization @@ -65,70 +65,126 @@ const JKSessionMetronomePlayer = ({ } }, [metronomeState]); - // Handle BPM change - const handleBpmChange = useCallback((value) => { + // Handle BPM change - apply immediately if playing + const handleBpmChange = useCallback(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue >= 40 && numValue <= 300) { setBpm(numValue); - } - }, []); - // Handle sound change - const handleSoundChange = useCallback((value) => { + // Apply change immediately if metronome is playing + if (isPlaying && jamClient) { + try { + await jamClient.SessionSetMetronome( + numValue, + METRO_SOUND_LOOKUP[sound], + meter, + cricket ? 1 : 0 + ); + } catch (err) { + console.error('[Metronome] Error updating BPM:', err); + } + } + } + }, [isPlaying, jamClient, sound, meter, cricket]); + + // Handle sound change - apply immediately if playing + const handleSoundChange = useCallback(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue)) { setSound(numValue); - } - }, []); - // Handle meter change - const handleMeterChange = useCallback((value) => { + // Apply change immediately if metronome is playing + if (isPlaying && jamClient) { + try { + await jamClient.SessionSetMetronome( + bpm, + METRO_SOUND_LOOKUP[numValue], + meter, + cricket ? 1 : 0 + ); + } catch (err) { + console.error('[Metronome] Error updating sound:', err); + } + } + } + }, [isPlaying, jamClient, bpm, meter, cricket]); + + // Handle meter change - apply immediately if playing + const handleMeterChange = useCallback(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue >= 1 && numValue <= 12) { setMeter(numValue); + + // Apply change immediately if metronome is playing + if (isPlaying && jamClient) { + try { + await jamClient.SessionSetMetronome( + bpm, + METRO_SOUND_LOOKUP[sound], + numValue, + cricket ? 1 : 0 + ); + } catch (err) { + console.error('[Metronome] Error updating meter:', err); + } + } } - }, []); + }, [isPlaying, jamClient, bpm, sound, cricket]); - // Handle cricket toggle - const handleCricketChange = useCallback((checked) => { - setCricket(checked); - }, []); - - // Apply settings to backend - const handleApplySettings = useCallback(async () => { + // Play metronome + const handlePlay = useCallback(async () => { if (!jamClient) { setError('Audio engine not available'); return; } - setIsApplying(true); setError(null); try { - console.log('[Metronome] Applying settings:', { + console.log('[Metronome] Starting metronome:', { bpm, sound: METRO_SOUND_LOOKUP[sound], meter, cricket: cricket ? 1 : 0 }); - // Call jamClient.SessionSetMetronome with current settings - await jamClient.SessionSetMetronome( + await jamClient.SessionOpenMetronome( bpm, METRO_SOUND_LOOKUP[sound], meter, cricket ? 1 : 0 ); - console.log('[Metronome] Settings applied successfully'); + setIsPlaying(true); + console.log('[Metronome] Metronome started successfully'); } catch (err) { - console.error('[Metronome] Error applying settings:', err); - setError('Failed to apply metronome settings'); - } finally { - setIsApplying(false); + console.error('[Metronome] Error starting metronome:', err); + setError('Failed to start metronome'); } }, [jamClient, bpm, sound, meter, cricket]); + // Stop metronome + const handleStop = useCallback(async () => { + if (!jamClient) { + setError('Audio engine not available'); + return; + } + + setError(null); + + try { + console.log('[Metronome] Stopping metronome'); + + await jamClient.SessionCloseMetronome(); + + setIsPlaying(false); + console.log('[Metronome] Metronome stopped successfully'); + } catch (err) { + console.error('[Metronome] Error stopping metronome:', err); + setError('Failed to stop metronome'); + } + }, [jamClient]); + // Handle close const handleClose = useCallback(() => { console.log('[Metronome] Closing player'); @@ -137,6 +193,14 @@ const JKSessionMetronomePlayer = ({ } }, [onClose]); + // Generate BPM options (common BPM values) + const bpmOptions = []; + for (let i = 40; i <= 208; i += 4) { + bpmOptions.push(i); + } + // Add additional common BPMs + [220, 240, 260, 280, 300].forEach(val => bpmOptions.push(val)); + // Render controls const renderControls = () => (