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:
Nuwan 2026-01-07 11:55:20 +05:30
parent 278de95a61
commit 4844a2b530
7 changed files with 726 additions and 18 deletions

View File

@ -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 }) => {

View File

@ -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>
)
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -16,7 +16,7 @@
//= require utils
//= require subscription_utils
//= require jamkazam
//= require JamServer_copy
//= require modern/JamServer_copy
//= require fakeJamClient
//= require fakeJamClientMessages