media popup system implementation
- Create MediaContext - Unified state management for all media types - Enhance WindowPortal - Better window lifecycle and communication - Implement JKPopupMediaControls - Main media controls component - Integrate MediaContext into JKSessionScreen - Update JKSessionOpenMenu to use new media context
This commit is contained in:
parent
278de95a61
commit
4844a2b530
|
|
@ -1,6 +1,10 @@
|
|||
import React, { useState, useRef, useEffect, useContext } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useJamClient } from '../../context/JamClientContext';
|
||||
import { useMediaContext } from '../../context/MediaContext';
|
||||
import { useCurrentSessionContext } from '../../context/CurrentSessionContext';
|
||||
import { useAuth } from '../../context/UserAuth';
|
||||
import { toast } from 'react-toastify';
|
||||
import openIcon from '../../assets/img/client/open.svg';
|
||||
|
||||
const JKSessionOpenMenu = ({ onBackingTrackSelected, onJamTrackSelected, onMetronomeSelected }) => {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { useJamServerContext } from '../../context/JamServerContext.js';
|
|||
import { useGlobalContext } from '../../context/GlobalContext.js';
|
||||
import { useJamKazamApp } from '../../context/JamKazamAppContext.js';
|
||||
import { useMixersContext } from '../../context/MixersContext.js';
|
||||
import { useMediaContext } from '../../context/MediaContext';
|
||||
import { useAuth } from '../../context/UserAuth';
|
||||
|
||||
import { dkeys } from '../../helpers/utils.js';
|
||||
|
|
@ -40,6 +41,7 @@ import JKSessionJamTrackStems from './JKSessionJamTrackStems.js';
|
|||
import JKSessionOpenMenu from './JKSessionOpenMenu.js';
|
||||
import WindowPortal from '../common/WindowPortal.js';
|
||||
import JKSessionBackingTrackPlayer from './JKSessionBackingTrackPlayer.js';
|
||||
import JKPopupMediaControls from '../popups/JKPopupMediaControls.js';
|
||||
import { SESSION_PRIVACY_MAP } from '../../helpers/globals.js';
|
||||
import { toast } from 'react-toastify';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
|
@ -82,6 +84,7 @@ const JKSessionScreen = () => {
|
|||
const { globalObject, metronomeState, closeMetronome, resetMetronome } = useGlobalContext();
|
||||
const { getCurrentRecordingState, reset: resetRecordingState, currentlyRecording } = useRecordingHelpers();
|
||||
const { SessionPageEnter } = useSessionUtils();
|
||||
const { mediaSummary, openBackingTrack, openMetronome, loadJamTrack } = useMediaContext();
|
||||
|
||||
// Use the session model hook
|
||||
const sessionModel = useSessionModel(app, server, null); // sessionScreen is null for now
|
||||
|
|
@ -130,6 +133,10 @@ const JKSessionScreen = () => {
|
|||
const [showBackingTrackPopup, setShowBackingTrackPopup] = useState(false);
|
||||
const [backingTrackData, setBackingTrackData] = useState(null);
|
||||
|
||||
//state for media controls popup
|
||||
const [showMediaControlsPopup, setShowMediaControlsPopup] = useState(false);
|
||||
const [mediaControlsOpened, setMediaControlsOpened] = useState(false);
|
||||
|
||||
//state for jam track modal
|
||||
const [showJamTrackModal, setShowJamTrackModal] = useState(false);
|
||||
|
||||
|
|
@ -1218,6 +1225,24 @@ const JKSessionScreen = () => {
|
|||
toggle={() => setShowJamTrackModal(!showJamTrackModal)}
|
||||
onJamTrackSelect={handleJamTrackSelect}
|
||||
/>
|
||||
|
||||
{/* Media Controls Popup - Only show when explicitly opened */}
|
||||
{showMediaControlsPopup && (
|
||||
<WindowPortal
|
||||
onClose={() => {
|
||||
setShowMediaControlsPopup(false);
|
||||
setMediaControlsOpened(false);
|
||||
}}
|
||||
windowFeatures="width=600,height=500,left=250,top=150,menubar=no,toolbar=no,status=no,scrollbars=yes,resizable=yes,location=no,addressbar=no"
|
||||
title="Media Controls"
|
||||
windowId="media-controls"
|
||||
>
|
||||
<JKPopupMediaControls onClose={() => {
|
||||
setShowMediaControlsPopup(false);
|
||||
setMediaControlsOpened(false);
|
||||
}} />
|
||||
</WindowPortal>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,60 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
const WindowPortal = ({ children, onClose, windowFeatures = 'width=400,height=300,left=200,top=200,menubar=no,toolbar=no,status=no,scrollbars=yes,resizable=yes,location=no, addressbar=no', title = 'Backing Track' }) => {
|
||||
const WindowPortal = ({
|
||||
children,
|
||||
onClose,
|
||||
windowFeatures = 'width=400,height=300,left=200,top=200,menubar=no,toolbar=no,status=no,scrollbars=yes,resizable=yes,location=no, addressbar=no',
|
||||
title = 'Media Controls',
|
||||
onWindowReady,
|
||||
onWindowMessage,
|
||||
windowId
|
||||
}) => {
|
||||
const [externalWindow, setExternalWindow] = useState(null);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const containerRef = useRef(null);
|
||||
const messageHandlerRef = useRef(null);
|
||||
|
||||
// Message handling
|
||||
const handleMessage = useCallback((event) => {
|
||||
// Only accept messages from our popup window
|
||||
if (event.source === externalWindow) {
|
||||
if (onWindowMessage) {
|
||||
onWindowMessage(event.data);
|
||||
}
|
||||
}
|
||||
}, [externalWindow, onWindowMessage]);
|
||||
|
||||
// Send message to popup window
|
||||
const sendMessage = useCallback((message) => {
|
||||
if (externalWindow && !externalWindow.closed) {
|
||||
externalWindow.postMessage(message, window.location.origin);
|
||||
}
|
||||
}, [externalWindow]);
|
||||
|
||||
// Auto-resize window based on content
|
||||
const resizeWindow = useCallback(() => {
|
||||
if (!externalWindow || externalWindow.closed) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const width = container.offsetWidth;
|
||||
const height = container.offsetHeight;
|
||||
|
||||
// Add some padding for window chrome
|
||||
const chromePadding = 20;
|
||||
const newWidth = Math.max(width + chromePadding, 350);
|
||||
const newHeight = Math.max(height + chromePadding, 200);
|
||||
|
||||
externalWindow.resizeTo(newWidth, newHeight);
|
||||
}, [externalWindow]);
|
||||
|
||||
useEffect(() => {
|
||||
// Open new window
|
||||
const newWindow = window.open('', '_blank', windowFeatures);
|
||||
// DEBUG: Comment out window.open and use console.log to test for infinite loops
|
||||
console.log('WindowPortal: Attempting to open window with features:', windowFeatures);
|
||||
// const newWindow = window.open('', '_blank', windowFeatures);
|
||||
const newWindow = null; // Temporarily disabled for debugging
|
||||
|
||||
if (!newWindow) {
|
||||
console.error('Failed to open popup window - popup blocker may be active');
|
||||
|
|
@ -22,21 +68,38 @@ const WindowPortal = ({ children, onClose, windowFeatures = 'width=400,height=30
|
|||
newWindow.document.body.style.padding = '0';
|
||||
newWindow.document.body.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", sans-serif';
|
||||
newWindow.document.body.style.backgroundColor = '#f8f9fa';
|
||||
newWindow.document.body.style.overflow = 'hidden';
|
||||
|
||||
// Add window ID for identification
|
||||
if (windowId) {
|
||||
newWindow.windowId = windowId;
|
||||
}
|
||||
|
||||
// Create container div
|
||||
const container = newWindow.document.createElement('div');
|
||||
container.style.width = '100vw';
|
||||
container.style.height = '100vh';
|
||||
container.style.overflow = 'auto';
|
||||
newWindow.document.body.appendChild(container);
|
||||
containerRef.current = container;
|
||||
|
||||
setExternalWindow(newWindow);
|
||||
setIsReady(true);
|
||||
|
||||
// Handle window close
|
||||
// Set up message handling
|
||||
messageHandlerRef.current = handleMessage;
|
||||
window.addEventListener('message', messageHandlerRef.current);
|
||||
|
||||
// Notify parent that window is ready
|
||||
if (onWindowReady) {
|
||||
onWindowReady(newWindow, sendMessage);
|
||||
}
|
||||
|
||||
// Handle window close detection
|
||||
const checkClosed = setInterval(() => {
|
||||
if (newWindow.closed) {
|
||||
clearInterval(checkClosed);
|
||||
window.removeEventListener('message', messageHandlerRef.current);
|
||||
onClose();
|
||||
}
|
||||
}, 1000);
|
||||
|
|
@ -48,16 +111,36 @@ const WindowPortal = ({ children, onClose, windowFeatures = 'width=400,height=30
|
|||
}
|
||||
};
|
||||
|
||||
// Handle popup window unload
|
||||
const handlePopupUnload = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
newWindow.addEventListener('beforeunload', handlePopupUnload);
|
||||
|
||||
// Initial resize after a short delay
|
||||
setTimeout(resizeWindow, 100);
|
||||
|
||||
return () => {
|
||||
clearInterval(checkClosed);
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
window.removeEventListener('message', messageHandlerRef.current);
|
||||
if (newWindow && !newWindow.closed) {
|
||||
newWindow.removeEventListener('beforeunload', handlePopupUnload);
|
||||
newWindow.close();
|
||||
}
|
||||
};
|
||||
}, [windowFeatures]);
|
||||
}, [windowFeatures, title, windowId, onClose, onWindowReady, handleMessage]);
|
||||
|
||||
// Resize when children change
|
||||
useEffect(() => {
|
||||
if (isReady) {
|
||||
// Delay resize to allow DOM updates
|
||||
const timeoutId = setTimeout(resizeWindow, 50);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
}, [children, isReady, resizeWindow]);
|
||||
|
||||
if (!isReady || !externalWindow || !containerRef.current) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,352 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, FormGroup, Label, Input, Table, Row, Col } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPlay, faPause, faStop, faVolumeUp, faDownload, faEdit, faTrash, faPlus, faMinus, faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useMediaContext } from '../../context/MediaContext';
|
||||
import { useAuth } from '../../context/UserAuth';
|
||||
import { useJamClient } from '../../context/JamClientContext';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const JKPopupMediaControls = () => {
|
||||
useEffect(() => {
|
||||
console.log('JKPopupMediaControls mounted');
|
||||
alert('JKPopupMediaControls mounted');
|
||||
}, [])
|
||||
const JKPopupMediaControls = ({ onClose }) => {
|
||||
const { currentUser } = useAuth();
|
||||
const { jamClient } = useJamClient();
|
||||
const {
|
||||
mediaSummary,
|
||||
backingTracks,
|
||||
jamTracks,
|
||||
recordedTracks,
|
||||
metronome,
|
||||
jamTrackState,
|
||||
downloadingJamTrack,
|
||||
showMyMixes,
|
||||
showCustomMixes,
|
||||
editingMixdownId,
|
||||
creatingMixdown,
|
||||
createMixdownErrors,
|
||||
closeMedia,
|
||||
setShowMyMixes,
|
||||
setShowCustomMixes,
|
||||
setEditingMixdownId,
|
||||
setCreatingMixdown,
|
||||
setCreateMixdownErrors
|
||||
} = useMediaContext();
|
||||
|
||||
const [time, setTime] = useState('0:00');
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [loopEnabled, setLoopEnabled] = useState(false);
|
||||
|
||||
// Get file name helper
|
||||
const getFileName = (file) => {
|
||||
if (!file) return 'Unknown File';
|
||||
if (file.path) {
|
||||
return file.path.split('/').pop().split('\\').pop();
|
||||
}
|
||||
if (file.name) {
|
||||
return file.name;
|
||||
}
|
||||
return 'Audio File';
|
||||
};
|
||||
|
||||
// Handle close
|
||||
const handleClose = async () => {
|
||||
try {
|
||||
if (!mediaSummary.isOpener && !mediaSummary.metronomeOpen) {
|
||||
console.log("Only opener can close media");
|
||||
return;
|
||||
}
|
||||
await closeMedia();
|
||||
if (onClose) onClose();
|
||||
} catch (error) {
|
||||
console.error('Error closing media:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle metronome display
|
||||
const handleShowMetronome = async () => {
|
||||
try {
|
||||
await jamClient.SessionShowNativeMetronomeGui();
|
||||
} catch (error) {
|
||||
console.error('Error showing metronome:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle mix sections
|
||||
const toggleMyMixes = () => setShowMyMixes(!showMyMixes);
|
||||
const toggleCustomMixes = () => setShowCustomMixes(!showCustomMixes);
|
||||
|
||||
// JamTrack actions
|
||||
const handleJamTrackPlay = async (jamTrack) => {
|
||||
try {
|
||||
await jamClient.JamTrackActivateNoMixdown(jamTrack);
|
||||
} catch (error) {
|
||||
console.error('Error playing jam track:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMixdownPlay = async (mixdown) => {
|
||||
try {
|
||||
setEditingMixdownId(null);
|
||||
await jamClient.JamTrackActivateMixdown(mixdown);
|
||||
} catch (error) {
|
||||
console.error('Error playing mixdown:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMixdownEdit = (mixdown) => {
|
||||
setEditingMixdownId(mixdown.id);
|
||||
};
|
||||
|
||||
const handleMixdownSave = async (mixdown) => {
|
||||
// Implementation for saving mixdown name
|
||||
setEditingMixdownId(null);
|
||||
};
|
||||
|
||||
const handleMixdownDelete = async (mixdown) => {
|
||||
if (window.confirm("Delete this custom mix?")) {
|
||||
try {
|
||||
await jamClient.JamTrackDeleteMixdown(mixdown);
|
||||
} catch (error) {
|
||||
console.error('Error deleting mixdown:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadMixdown = async (mixdown) => {
|
||||
// Implementation for downloading mixdown
|
||||
console.log('Download mixdown:', mixdown);
|
||||
};
|
||||
|
||||
const handleCreateMix = async () => {
|
||||
// Implementation for creating custom mix
|
||||
console.log('Create custom mix');
|
||||
};
|
||||
|
||||
// Determine content based on media type
|
||||
const renderContent = () => {
|
||||
// Backing Track
|
||||
if (mediaSummary.backingTrackOpen && backingTracks.length > 0) {
|
||||
const backingTrack = backingTracks[0];
|
||||
return (
|
||||
<div className="media-controls-popup">
|
||||
<div className="header">
|
||||
<h3>Audio File: {getFileName(backingTrack)}</h3>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="loop"
|
||||
checked={loopEnabled}
|
||||
onChange={(e) => setLoopEnabled(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="loop">Loop audio file playback</label>
|
||||
</div>
|
||||
|
||||
<div className="actions">
|
||||
<Button color="secondary" onClick={handleClose}>
|
||||
Close Audio File
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// JamTrack
|
||||
if (mediaSummary.jamTrackOpen && jamTrackState.jamTrack) {
|
||||
const jamTrack = jamTrackState.jamTrack;
|
||||
const selectedMixdown = jamTrack.activeMixdown;
|
||||
|
||||
return (
|
||||
<div className="media-controls-popup">
|
||||
<div className="header">
|
||||
<h3>JamTrack: {jamTrack.name}</h3>
|
||||
<h4>
|
||||
{selectedMixdown ? 'Custom Mix' : 'Full JamTrack'}
|
||||
{downloadingJamTrack && ' (Loading...)'}
|
||||
</h4>
|
||||
{selectedMixdown && <h5>{selectedMixdown.name}</h5>}
|
||||
</div>
|
||||
|
||||
{/* My Mixes Section */}
|
||||
<div className="my-mixes-section">
|
||||
<h4 onClick={toggleMyMixes} style={{ cursor: 'pointer' }}>
|
||||
My Mixes {showMyMixes ? '▼' : '▶'}
|
||||
</h4>
|
||||
{showMyMixes && (
|
||||
<div className="my-mixes">
|
||||
{/* Full Track Option */}
|
||||
<div className={`mixdown-display ${!selectedMixdown ? 'active' : ''}`}>
|
||||
<div className="mixdown-name">Full JamTrack</div>
|
||||
<div className="mixdown-actions">
|
||||
<Button size="sm" onClick={() => handleJamTrackPlay(jamTrack)}>
|
||||
<FontAwesomeIcon icon={faPlay} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Mixes */}
|
||||
{jamTrack.mixdowns && jamTrack.mixdowns.map(mixdown => (
|
||||
<div key={mixdown.id} className={`mixdown-display ${selectedMixdown?.id === mixdown.id ? 'active' : ''}`}>
|
||||
<div className="mixdown-name">
|
||||
{editingMixdownId === mixdown.id ? (
|
||||
<Input
|
||||
type="text"
|
||||
defaultValue={mixdown.name}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleMixdownSave(mixdown);
|
||||
if (e.key === 'Escape') setEditingMixdownId(null);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
mixdown.name
|
||||
)}
|
||||
</div>
|
||||
<div className="mixdown-actions">
|
||||
<Button size="sm" onClick={() => handleMixdownPlay(mixdown)}>
|
||||
<FontAwesomeIcon icon={faPlay} />
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => handleDownloadMixdown(mixdown)}>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => handleMixdownEdit(mixdown)}>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</Button>
|
||||
<Button size="sm" color="danger" onClick={() => handleMixdownDelete(mixdown)}>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Create Custom Mix Section */}
|
||||
<div className="custom-mix-section">
|
||||
<h4 onClick={toggleCustomMixes} style={{ cursor: 'pointer' }}>
|
||||
Create Custom Mix {showCustomMixes ? '▼' : '▶'}
|
||||
</h4>
|
||||
{showCustomMixes && (
|
||||
<div className="create-mix">
|
||||
<p>Use the JamTrack controls on the session screen to set levels, mute/unmute, or pan any of the parts of the JamTrack as you like.</p>
|
||||
|
||||
<FormGroup>
|
||||
<Label>Change Tempo:</Label>
|
||||
<Input type="select" name="mix-speed">
|
||||
<option value="">No change</option>
|
||||
<option value="-5">Slower by 5%</option>
|
||||
<option value="-10">Slower by 10%</option>
|
||||
<option value="5">Faster by 5%</option>
|
||||
<option value="10">Faster by 10%</option>
|
||||
</Input>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<Label>Change Pitch:</Label>
|
||||
<Input type="select" name="mix-pitch">
|
||||
<option value="">No change</option>
|
||||
<option value="-1">Down 1 Semitone</option>
|
||||
<option value="1">Up 1 Semitone</option>
|
||||
</Input>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<Label>Mix Name:</Label>
|
||||
<Input type="text" name="mix-name" />
|
||||
</FormGroup>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={handleCreateMix}
|
||||
disabled={creatingMixdown}
|
||||
>
|
||||
{creatingMixdown ? 'Creating...' : 'CREATE MIX'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="actions">
|
||||
<Button color="link" href="https://jamkazam.desk.com/customer/portal/articles/2138903-using-custom-mixes-to-slow-tempo-change-pitch" target="_blank">
|
||||
<FontAwesomeIcon icon={faQuestionCircle} /> HELP
|
||||
</Button>
|
||||
<Button color="secondary" onClick={handleClose}>
|
||||
Close JamTrack
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Metronome
|
||||
if (mediaSummary.metronomeOpen) {
|
||||
return (
|
||||
<div className="media-controls-popup">
|
||||
<div className="header">
|
||||
<h3>Metronome</h3>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<Button color="secondary" onClick={handleShowMetronome}>
|
||||
Display visual metronome
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="actions">
|
||||
<Button color="secondary" onClick={handleClose}>
|
||||
Close Metronome
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Recording
|
||||
if (mediaSummary.recordingOpen) {
|
||||
return (
|
||||
<div className="media-controls-popup">
|
||||
<div className="header">
|
||||
<h3>Recording</h3>
|
||||
</div>
|
||||
|
||||
<div className="actions">
|
||||
<Button color="secondary" onClick={handleClose}>
|
||||
Close Recording
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="media-controls-popup">
|
||||
<div className="header">
|
||||
<h3>Media Controls</h3>
|
||||
</div>
|
||||
<p>No media currently open</p>
|
||||
<div className="actions">
|
||||
<Button color="secondary" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>JKPopupMediaControls</div>
|
||||
)
|
||||
}
|
||||
<div style={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", sans-serif',
|
||||
backgroundColor: '#f8f9fa',
|
||||
padding: '20px',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JKPopupMediaControls
|
||||
export default JKPopupMediaControls;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,255 @@
|
|||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import { useJamServerContext } from './JamServerContext';
|
||||
import { useJamClient } from './JamClientContext';
|
||||
|
||||
// Media types constants
|
||||
export const MEDIA_TYPES = {
|
||||
BACKING_TRACK: 'backing_track',
|
||||
JAM_TRACK: 'jam_track',
|
||||
RECORDING: 'recording',
|
||||
METRONOME: 'metronome'
|
||||
};
|
||||
|
||||
// Media states
|
||||
export const MEDIA_STATES = {
|
||||
CLOSED: 'closed',
|
||||
LOADING: 'loading',
|
||||
OPEN: 'open',
|
||||
ERROR: 'error'
|
||||
};
|
||||
|
||||
const MediaContext = createContext();
|
||||
|
||||
export const MediaProvider = ({ children }) => {
|
||||
// Core media state
|
||||
const [mediaSummary, setMediaSummary] = useState({
|
||||
mediaOpen: false,
|
||||
backingTrackOpen: false,
|
||||
jamTrackOpen: false,
|
||||
recordingOpen: false,
|
||||
metronomeOpen: false,
|
||||
isOpener: false,
|
||||
userNeedsMediaControls: false
|
||||
});
|
||||
|
||||
// Media data
|
||||
const [backingTracks, setBackingTracks] = useState([]);
|
||||
const [jamTracks, setJamTracks] = useState([]);
|
||||
const [recordedTracks, setRecordedTracks] = useState([]);
|
||||
const [metronome, setMetronome] = useState(null);
|
||||
const [jamTrackState, setJamTrackState] = useState({});
|
||||
const [downloadingJamTrack, setDownloadingJamTrack] = useState(false);
|
||||
|
||||
// UI state
|
||||
const [showMyMixes, setShowMyMixes] = useState(false);
|
||||
const [showCustomMixes, setShowCustomMixes] = useState(false);
|
||||
const [editingMixdownId, setEditingMixdownId] = useState(null);
|
||||
const [creatingMixdown, setCreatingMixdown] = useState(false);
|
||||
const [createMixdownErrors, setCreateMixdownErrors] = useState(null);
|
||||
|
||||
// Contexts
|
||||
const { jamClient } = useJamClient();
|
||||
const { registerMessageCallback, unregisterMessageCallback } = useJamServerContext();
|
||||
|
||||
// Message handlers for real-time updates
|
||||
const handleMixerChanges = useCallback((sessionMixers) => {
|
||||
const session = sessionMixers.session;
|
||||
const mixers = sessionMixers.mixers;
|
||||
|
||||
setMediaSummary(prev => ({
|
||||
...prev,
|
||||
...mixers.mediaSummary
|
||||
}));
|
||||
|
||||
setBackingTracks(mixers.backingTracks || []);
|
||||
setJamTracks(mixers.jamTracks || []);
|
||||
setRecordedTracks(mixers.recordedTracks || []);
|
||||
setMetronome(mixers.metronome || null);
|
||||
}, []);
|
||||
|
||||
const handleJamTrackChanges = useCallback((changes) => {
|
||||
setJamTrackState(changes);
|
||||
}, []);
|
||||
|
||||
// NOTE: Disabled automatic WebSocket message handling to prevent infinite popup loops
|
||||
// Message callbacks will be handled manually by components that need them
|
||||
// useEffect(() => {
|
||||
// if (!jamClient) return;
|
||||
|
||||
// const callbacks = [
|
||||
// { type: 'MIXER_CHANGES', callback: handleMixerChanges },
|
||||
// { type: 'JAM_TRACK_CHANGES', callback: handleJamTrackChanges }
|
||||
// ];
|
||||
|
||||
// callbacks.forEach(({ type, callback }) => {
|
||||
// registerMessageCallback(type, callback);
|
||||
// });
|
||||
|
||||
// return () => {
|
||||
// callbacks.forEach(({ type, callback }) => {
|
||||
// unregisterMessageCallback(type, callback);
|
||||
// });
|
||||
// };
|
||||
// }, [jamClient, registerMessageCallback, unregisterMessageCallback, handleMixerChanges, handleJamTrackChanges]);
|
||||
|
||||
// Actions
|
||||
const openBackingTrack = useCallback(async (file) => {
|
||||
try {
|
||||
await jamClient.SessionOpenBackingTrackFile(file, false);
|
||||
setMediaSummary(prev => ({
|
||||
...prev,
|
||||
backingTrackOpen: true,
|
||||
userNeedsMediaControls: true
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error opening backing track:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [jamClient]);
|
||||
|
||||
const closeMedia = useCallback(async (force = false) => {
|
||||
try {
|
||||
await jamClient.SessionCloseMedia(force);
|
||||
setMediaSummary(prev => ({
|
||||
...prev,
|
||||
mediaOpen: false,
|
||||
backingTrackOpen: false,
|
||||
jamTrackOpen: false,
|
||||
recordingOpen: false,
|
||||
metronomeOpen: false,
|
||||
userNeedsMediaControls: false
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error closing media:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [jamClient]);
|
||||
|
||||
const openMetronome = useCallback(async (bpm = 120, sound = "Beep", meter = 1, mode = 0) => {
|
||||
try {
|
||||
const result = await jamClient.SessionOpenMetronome(bpm, sound, meter, mode);
|
||||
setMediaSummary(prev => ({
|
||||
...prev,
|
||||
metronomeOpen: true,
|
||||
userNeedsMediaControls: true
|
||||
}));
|
||||
setMetronome({ bpm, sound, meter, mode });
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error opening metronome:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [jamClient]);
|
||||
|
||||
const closeMetronome = useCallback(async () => {
|
||||
try {
|
||||
await jamClient.SessionCloseMetronome();
|
||||
setMediaSummary(prev => ({
|
||||
...prev,
|
||||
metronomeOpen: false
|
||||
}));
|
||||
setMetronome(null);
|
||||
} catch (error) {
|
||||
console.error('Error closing metronome:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [jamClient]);
|
||||
|
||||
// JamTrack actions
|
||||
const loadJamTrack = useCallback(async (jamTrack) => {
|
||||
try {
|
||||
setDownloadingJamTrack(true);
|
||||
|
||||
// Load JMep data if available
|
||||
if (jamTrack.jmep) {
|
||||
const sampleRate = await jamClient.GetSampleRate();
|
||||
const sampleRateForFilename = sampleRate === 48 ? '48' : '44';
|
||||
const fqId = `${jamTrack.id}-${sampleRateForFilename}`;
|
||||
|
||||
await jamClient.JamTrackLoadJmep(fqId, jamTrack.jmep);
|
||||
}
|
||||
|
||||
// Play/load the jamtrack
|
||||
const result = await jamClient.JamTrackPlay(jamTrack.id);
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Unable to open JamTrack');
|
||||
}
|
||||
|
||||
setMediaSummary(prev => ({
|
||||
...prev,
|
||||
jamTrackOpen: true,
|
||||
userNeedsMediaControls: true
|
||||
}));
|
||||
|
||||
setDownloadingJamTrack(false);
|
||||
return result;
|
||||
} catch (error) {
|
||||
setDownloadingJamTrack(false);
|
||||
console.error('Error loading jam track:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [jamClient]);
|
||||
|
||||
const closeJamTrack = useCallback(async () => {
|
||||
try {
|
||||
await jamClient.JamTrackStopPlay();
|
||||
setMediaSummary(prev => ({
|
||||
...prev,
|
||||
jamTrackOpen: false
|
||||
}));
|
||||
setJamTrackState({});
|
||||
} catch (error) {
|
||||
console.error('Error closing jam track:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [jamClient]);
|
||||
|
||||
// Context value
|
||||
const value = {
|
||||
// State
|
||||
mediaSummary,
|
||||
backingTracks,
|
||||
jamTracks,
|
||||
recordedTracks,
|
||||
metronome,
|
||||
jamTrackState,
|
||||
downloadingJamTrack,
|
||||
showMyMixes,
|
||||
showCustomMixes,
|
||||
editingMixdownId,
|
||||
creatingMixdown,
|
||||
createMixdownErrors,
|
||||
|
||||
// Actions
|
||||
openBackingTrack,
|
||||
closeMedia,
|
||||
openMetronome,
|
||||
closeMetronome,
|
||||
loadJamTrack,
|
||||
closeJamTrack,
|
||||
|
||||
// UI actions
|
||||
setShowMyMixes,
|
||||
setShowCustomMixes,
|
||||
setEditingMixdownId,
|
||||
setCreatingMixdown,
|
||||
setCreateMixdownErrors
|
||||
};
|
||||
|
||||
return (
|
||||
<MediaContext.Provider value={value}>
|
||||
{children}
|
||||
</MediaContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useMediaContext = () => {
|
||||
const context = useContext(MediaContext);
|
||||
if (!context) {
|
||||
throw new Error('useMediaContext must be used within a MediaProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export default MediaContext;
|
||||
|
|
@ -12,6 +12,7 @@ import { CurrentSessionProvider } from '../context/CurrentSessionContext';
|
|||
import { MixersProvider } from '../context/MixersContext';
|
||||
import { VuProvider } from '../context/VuContext';
|
||||
import { GlobalProvider } from '../context/GlobalContext';
|
||||
import { MediaProvider } from '../context/MediaContext';
|
||||
|
||||
const JKClientLayout = ({ location }) => {
|
||||
|
||||
|
|
@ -28,7 +29,9 @@ const JKClientLayout = ({ location }) => {
|
|||
<CurrentSessionProvider>
|
||||
<VuProvider>
|
||||
<MixersProvider>
|
||||
<ClientRoutes />
|
||||
<MediaProvider>
|
||||
<ClientRoutes />
|
||||
</MediaProvider>
|
||||
</MixersProvider>
|
||||
</VuProvider>
|
||||
</CurrentSessionProvider>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
//= require utils
|
||||
//= require subscription_utils
|
||||
//= require jamkazam
|
||||
//= require JamServer_copy
|
||||
//= require modern/JamServer_copy
|
||||
|
||||
//= require fakeJamClient
|
||||
//= require fakeJamClientMessages
|
||||
|
|
|
|||
Loading…
Reference in New Issue