diff --git a/jam-ui/src/components/client/JKSessionOpenMenu.js b/jam-ui/src/components/client/JKSessionOpenMenu.js
index 5e324994b..44f344952 100644
--- a/jam-ui/src/components/client/JKSessionOpenMenu.js
+++ b/jam-ui/src/components/client/JKSessionOpenMenu.js
@@ -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 }) => {
diff --git a/jam-ui/src/components/client/JKSessionScreen.js b/jam-ui/src/components/client/JKSessionScreen.js
index 9f6ba293a..110a13032 100644
--- a/jam-ui/src/components/client/JKSessionScreen.js
+++ b/jam-ui/src/components/client/JKSessionScreen.js
@@ -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 && (
+ {
+ 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"
+ >
+ {
+ setShowMediaControlsPopup(false);
+ setMediaControlsOpened(false);
+ }} />
+
+ )}
)
}
diff --git a/jam-ui/src/components/common/WindowPortal.js b/jam-ui/src/components/common/WindowPortal.js
index 6ba26555b..9c98e0ee5 100644
--- a/jam-ui/src/components/common/WindowPortal.js
+++ b/jam-ui/src/components/common/WindowPortal.js
@@ -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;
diff --git a/jam-ui/src/components/popups/JKPopupMediaControls.js b/jam-ui/src/components/popups/JKPopupMediaControls.js
index 9275b3cb5..ae4523e5a 100644
--- a/jam-ui/src/components/popups/JKPopupMediaControls.js
+++ b/jam-ui/src/components/popups/JKPopupMediaControls.js
@@ -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 (
+
+
+
Audio File: {getFileName(backingTrack)}
+
+
+
+ setLoopEnabled(e.target.checked)}
+ />
+
+
+
+
+
+
+
+ );
+ }
+
+ // JamTrack
+ if (mediaSummary.jamTrackOpen && jamTrackState.jamTrack) {
+ const jamTrack = jamTrackState.jamTrack;
+ const selectedMixdown = jamTrack.activeMixdown;
+
+ return (
+
+
+
JamTrack: {jamTrack.name}
+
+ {selectedMixdown ? 'Custom Mix' : 'Full JamTrack'}
+ {downloadingJamTrack && ' (Loading...)'}
+
+ {selectedMixdown && {selectedMixdown.name}
}
+
+
+ {/* My Mixes Section */}
+
+
+ My Mixes {showMyMixes ? '▼' : '▶'}
+
+ {showMyMixes && (
+
+ {/* Full Track Option */}
+
+
Full JamTrack
+
+
+
+
+
+ {/* Custom Mixes */}
+ {jamTrack.mixdowns && jamTrack.mixdowns.map(mixdown => (
+
+
+ {editingMixdownId === mixdown.id ? (
+ {
+ if (e.key === 'Enter') handleMixdownSave(mixdown);
+ if (e.key === 'Escape') setEditingMixdownId(null);
+ }}
+ />
+ ) : (
+ mixdown.name
+ )}
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Create Custom Mix Section */}
+
+
+ Create Custom Mix {showCustomMixes ? '▼' : '▶'}
+
+ {showCustomMixes && (
+
+ )}
+
+
+
+
+
+
+
+ );
+ }
+
+ // Metronome
+ if (mediaSummary.metronomeOpen) {
+ return (
+
+
+
Metronome
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ // Recording
+ if (mediaSummary.recordingOpen) {
+ return (
+
+
+
Recording
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
Media Controls
+
+
No media currently open
+
+
+
+
+ );
+ };
return (
- JKPopupMediaControls
- )
-}
+
+ {renderContent()}
+
+ );
+};
-export default JKPopupMediaControls
\ No newline at end of file
+export default JKPopupMediaControls;
diff --git a/jam-ui/src/context/MediaContext.js b/jam-ui/src/context/MediaContext.js
new file mode 100644
index 000000000..338643922
--- /dev/null
+++ b/jam-ui/src/context/MediaContext.js
@@ -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 (
+
+ {children}
+
+ );
+};
+
+export const useMediaContext = () => {
+ const context = useContext(MediaContext);
+ if (!context) {
+ throw new Error('useMediaContext must be used within a MediaProvider');
+ }
+ return context;
+};
+
+export default MediaContext;
diff --git a/jam-ui/src/layouts/JKClientLayout.js b/jam-ui/src/layouts/JKClientLayout.js
index d5477e62b..4b06ef55e 100644
--- a/jam-ui/src/layouts/JKClientLayout.js
+++ b/jam-ui/src/layouts/JKClientLayout.js
@@ -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 }) => {
-
+
+
+
diff --git a/web/app/assets/javascripts/modern/scripts.js b/web/app/assets/javascripts/modern/scripts.js
index 617ba8b3f..78120ec25 100644
--- a/web/app/assets/javascripts/modern/scripts.js
+++ b/web/app/assets/javascripts/modern/scripts.js
@@ -16,7 +16,7 @@
//= require utils
//= require subscription_utils
//= require jamkazam
-//= require JamServer_copy
+//= require modern/JamServer_copy
//= require fakeJamClient
//= require fakeJamClientMessages