feat: redesign metronome popup controls and fix WindowPortal styling

Metronome Popup Redesign:
- Replace Apply button with Play/Stop circular icon buttons
- Change BPM from slider to dropdown with common BPM values (40-300)
- Update layout: Play/Stop buttons on left, form controls on right
- Add real-time updates: changes apply immediately when metronome is playing
- Use SessionStopPlay instead of SessionCloseMetronome to preserve state
- Center Close button at bottom with minimal styling

WindowPortal CSS Fix:
- Copy all stylesheets from parent window to popup window
- Support both <link> and <style> elements
- Fix issue where popup windows didn't inherit parent styles

Session Screen Improvements:
- Fix metronome auto-start issue: stop playback immediately after initialization
- Add SetVURefreshRate call to enable VU meter updates
- Update metronome opening to not play audio until user clicks Play

Design matches user-provided screenshot with minimal, clean styling.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-01-25 14:02:06 +05:30
parent 3887f4fdda
commit 091af31bb4
4 changed files with 186 additions and 124 deletions

View File

@ -1,25 +1,25 @@
.metronome-player-popup {
padding: 24px;
padding: 20px 24px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", sans-serif;
background-color: #ffffff;
min-width: 450px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
background-color: #f5f5f5;
min-width: 380px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.metronome-player-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid #dee2e6;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid #d0d0d0;
}
.metronome-player-header h3 {
margin: 0;
font-size: 1.4rem;
font-weight: 700;
color: #212529;
font-size: 1rem;
font-weight: 400;
color: #666;
}
.metronome-close-btn {
@ -50,56 +50,87 @@
.metronome-player-controls {
display: flex;
flex-direction: column;
gap: 24px;
gap: 0;
}
/* Playback controls (Play/Stop buttons) */
.metronome-playback-controls {
/* Main content area with buttons and form side by side */
.metronome-main-content {
display: flex;
gap: 12px;
padding-bottom: 16px;
border-bottom: 1px solid #dee2e6;
gap: 16px;
align-items: flex-start;
margin-bottom: 20px;
}
.metronome-play-btn,
.metronome-stop-btn {
flex: 1;
padding: 12px 20px;
font-size: 1rem;
font-weight: 600;
border-radius: 6px;
/* Playback buttons - circular icons */
.metronome-playback-buttons {
display: flex;
flex-direction: column;
gap: 8px;
}
.metronome-icon-btn {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid #007bff;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
border: none;
padding: 0;
}
.metronome-play-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
.metronome-icon-btn:hover:not(.disabled) {
background-color: #007bff;
color: #fff;
}
.metronome-stop-btn:disabled {
opacity: 0.5;
.metronome-icon-btn.disabled {
opacity: 0.3;
cursor: not-allowed;
border-color: #ccc;
}
.metronome-play-icon .play-icon {
font-size: 14px;
color: #007bff;
margin-left: 2px;
}
.metronome-icon-btn:hover:not(.disabled) .play-icon {
color: #fff;
}
.metronome-stop-icon .stop-icon {
font-size: 12px;
color: #007bff;
}
.metronome-icon-btn:hover:not(.disabled) .stop-icon {
color: #fff;
}
/* Form controls with labels on left, inputs on right */
.metronome-form-controls {
display: flex;
flex-direction: column;
gap: 16px;
gap: 12px;
flex: 1;
}
.metronome-form-row {
display: grid;
grid-template-columns: 120px 1fr;
grid-template-columns: 80px 1fr;
align-items: center;
gap: 16px;
gap: 12px;
}
.metronome-form-label {
font-weight: 600;
font-size: 0.9rem;
color: #495057;
font-weight: 400;
font-size: 0.875rem;
color: #666;
text-align: right;
margin: 0;
}
@ -108,25 +139,36 @@
.metronome-close-container {
display: flex;
justify-content: center;
padding-top: 16px;
border-top: 1px solid #dee2e6;
padding-top: 20px;
border-top: none;
}
.metronome-close-button {
min-width: 120px;
padding: 10px 24px;
font-size: 0.9rem;
font-weight: 600;
border-radius: 6px;
min-width: 100px;
padding: 8px 32px;
font-size: 0.875rem;
font-weight: 400;
border-radius: 4px;
border: 1px solid #d0d0d0;
background-color: #fff;
color: #333;
cursor: pointer;
transition: all 0.2s;
}
.metronome-close-button:hover {
background-color: #f8f8f8;
border-color: #999;
}
.metronome-error {
padding: 10px;
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
padding: 8px 12px;
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
border-radius: 4px;
font-size: 0.875rem;
font-size: 0.8rem;
margin-bottom: 12px;
}
/* Legacy styles - no longer used but kept for backwards compatibility */
@ -144,30 +186,32 @@
}
.metronome-select {
padding: 10px 14px;
border: 2px solid #ced4da;
border-radius: 6px;
font-size: 0.95rem;
padding: 6px 10px;
border: 1px solid #d0d0d0;
border-radius: 4px;
font-size: 0.875rem;
background-color: #fff;
cursor: pointer;
transition: all 0.2s;
width: 100%;
color: #333;
}
.metronome-select:hover {
border-color: #007bff;
.metronome-select:hover:not(:disabled) {
border-color: #999;
}
.metronome-select:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.15);
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.1);
}
.metronome-select:disabled {
background-color: #e9ecef;
opacity: 0.6;
cursor: not-allowed;
background-color: #f5f5f5;
color: #666;
cursor: default;
border-color: #e0e0e0;
}
/* Legacy checkbox and actions styles - removed in redesign */

View File

@ -148,6 +148,10 @@ const JKSessionMetronomePlayer = ({
cricket: cricket ? 1 : 0
});
// Stop any current playback first
await jamClient.SessionStopPlay();
// Start the metronome
await jamClient.SessionOpenMetronome(
bpm,
METRO_SOUND_LOOKUP[sound],
@ -175,7 +179,9 @@ const JKSessionMetronomePlayer = ({
try {
console.log('[Metronome] Stopping metronome');
await jamClient.SessionCloseMetronome();
// Use SessionStopPlay to pause metronome without destroying it
// This allows Play to work again without reinitializing
await jamClient.SessionStopPlay();
setIsPlaying(false);
console.log('[Metronome] Metronome stopped successfully');
@ -211,83 +217,79 @@ const JKSessionMetronomePlayer = ({
</div>
)}
{/* Play/Stop buttons */}
<div className="metronome-playback-controls">
<Button
color={isPlaying ? "secondary" : "success"}
onClick={handlePlay}
disabled={isPlaying}
className="metronome-play-btn"
>
Play
</Button>
<Button
color="danger"
onClick={handleStop}
disabled={!isPlaying}
className="metronome-stop-btn"
>
Stop
</Button>
</div>
{/* Form layout: labels on left, controls on right */}
<div className="metronome-form-controls">
{/* Sound selector */}
<div className="metronome-form-row">
<label className="metronome-form-label">Sound</label>
<select
className="metronome-select"
value={sound}
onChange={(e) => handleSoundChange(e.target.value)}
{/* Main content area with playback buttons and form */}
<div className="metronome-main-content">
{/* Play/Stop buttons - circular icons on the left */}
<div className="metronome-playback-buttons">
<button
onClick={handlePlay}
disabled={isPlaying}
className={`metronome-icon-btn metronome-play-icon ${isPlaying ? 'disabled' : ''}`}
title="Play"
>
<option value={2}>Beep</option>
<option value={3}>Click</option>
<option value={4}>Kick</option>
<option value={5}>Snare</option>
<option value={0}>Built-in</option>
<option value={1}>Sine Wave</option>
</select>
<span className="play-icon"></span>
</button>
<button
onClick={handleStop}
disabled={!isPlaying}
className={`metronome-icon-btn metronome-stop-icon ${!isPlaying ? 'disabled' : ''}`}
title="Stop"
>
<span className="stop-icon"></span>
</button>
</div>
{/* Tempo (BPM) selector */}
<div className="metronome-form-row">
<label className="metronome-form-label">Tempo (BPM)</label>
<select
className="metronome-select"
value={bpm}
onChange={(e) => handleBpmChange(e.target.value)}
>
{bpmOptions.map(value => (
<option key={value} value={value}>{value}</option>
))}
</select>
</div>
{/* Form layout: labels on left, controls on right */}
<div className="metronome-form-controls">
{/* Feature row - always shows "Metronome" */}
<div className="metronome-form-row">
<label className="metronome-form-label">Feature:</label>
<select className="metronome-select" value="metronome" disabled>
<option value="metronome">Metronome</option>
</select>
</div>
{/* Meter selector */}
<div className="metronome-form-row">
<label className="metronome-form-label">Meter</label>
<select
className="metronome-select"
value={meter}
onChange={(e) => handleMeterChange(e.target.value)}
>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map(m => (
<option key={m} value={m}>{m}</option>
))}
</select>
{/* Sound selector */}
<div className="metronome-form-row">
<label className="metronome-form-label">Sound:</label>
<select
className="metronome-select"
value={sound}
onChange={(e) => handleSoundChange(e.target.value)}
>
<option value={0}>Built-in</option>
<option value={1}>Sine Wave</option>
<option value={2}>Beep</option>
<option value={3}>Click</option>
<option value={4}>Kick</option>
<option value={5}>Snare</option>
</select>
</div>
{/* Tempo (BPM) selector */}
<div className="metronome-form-row">
<label className="metronome-form-label">Tempo:</label>
<select
className="metronome-select"
value={bpm}
onChange={(e) => handleBpmChange(e.target.value)}
>
{bpmOptions.map(value => (
<option key={value} value={value}>{value}</option>
))}
</select>
</div>
</div>
</div>
{/* Close button centered at bottom */}
<div className="metronome-close-container">
<Button
color="secondary"
<button
onClick={handleClose}
className="metronome-close-button"
>
Close
</Button>
</button>
</div>
</div>
);

View File

@ -1037,10 +1037,11 @@ const JKSessionScreen = () => {
// Inform server about metronome opening (like legacy SessionStore)
await openMetronome({ id: currentSession.id });
// Initialize metronome track (creates mixers) but stop immediately so no audio plays
// Initialize metronome track (creates mixers) then stop playback
// This allows the track to appear in the UI while audio remains stopped
// Use SessionStopPlay instead of SessionCloseMetronome to preserve metronome state
await jamClient.SessionOpenMetronome(bpm, soundName, meter, 0);
await jamClient.SessionCloseMetronome();
await jamClient.SessionStopPlay();
// Update local metronome state to show popup immediately
// Metronome track will be visible but audio is stopped (user must click Play)

View File

@ -68,6 +68,21 @@ const WindowPortal = ({
newWindow.document.body.style.backgroundColor = '#f8f9fa';
newWindow.document.body.style.overflow = 'hidden';
// Copy all stylesheets from parent window to popup
const stylesheets = Array.from(document.querySelectorAll('link[rel="stylesheet"], style'));
stylesheets.forEach(sheet => {
if (sheet.tagName === 'LINK') {
const newLink = newWindow.document.createElement('link');
newLink.rel = 'stylesheet';
newLink.href = sheet.href;
newWindow.document.head.appendChild(newLink);
} else if (sheet.tagName === 'STYLE') {
const newStyle = newWindow.document.createElement('style');
newStyle.textContent = sheet.textContent;
newWindow.document.head.appendChild(newStyle);
}
});
// Add window ID for identification
if (windowId) {
newWindow.windowId = windowId;