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:
parent
3887f4fdda
commit
091af31bb4
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue