diff --git a/electron/main.ts b/electron/main.ts index 4e7a47b9..6b2bec0c 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -205,6 +205,11 @@ async function videoWindow() { justify-content: center; align-items: center; } + .actions-nav.disabled .action-button:not(.connect-button) { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; + } .action-button { background: none; border: none; @@ -212,20 +217,16 @@ async function videoWindow() { cursor: pointer; padding: 8px; border-radius: 50%; - transition: background-color 0.2s; + transition: all 0.2s ease-in-out; display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; } - .action-button:hover { + .action-button:not(.disabled):hover { background-color: rgba(255, 255, 255, 0.1); } - .action-button.disabled { - opacity: 0.5; - cursor: not-allowed; - } .material-symbols-outlined { font-family: 'Material Symbols Outlined'; font-weight: normal; @@ -243,13 +244,91 @@ async function videoWindow() { .filled { font-variation-settings: 'FILL' 1; } + .message-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease-in-out; + z-index: 1000; + } + .message-overlay.visible { + opacity: 1; + pointer-events: auto; + } + .message-content { + background: rgba(255, 255, 255, 0.1); + padding: 16px 24px; + border-radius: 8px; + color: white; + font-size: 14px; + text-align: center; + max-width: 80%; + backdrop-filter: blur(8px); + } + .carousel-container { + display: flex; + align-items: center; + gap: 8px; + margin-top: auto; + width: 100%; + padding: 10px 0; + } + .carousel-button { + position: relative; + width: 15%; + height: 32px; + background: transparent; + border: none; + color: white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s; + border-radius: 4px; + } + .carousel-button:hover { + background-color: rgba(255, 255, 255, 0.1); + } + .carousel-content { + width: 70%; + text-align: center; + justify-content: center; + } + .carousel-text { + color: white; + font-size: 14px; + opacity: 0.9; + } + .control-tray-container { + display: flex; + flex-direction: column; + gap: 20px; + width: 100%; + max-width: 400px; + padding: 16px; + } +
+
+ Please select a screen to share from the main window +
+
+
-
@@ -276,34 +371,75 @@ async function videoWindow() { const screenButton = document.querySelector('.screen-button'); const webcamButton = document.querySelector('.webcam-button'); const connectButton = document.querySelector('.connect-button'); + const actionsNav = document.querySelector('.actions-nav'); + const messageOverlay = document.querySelector('.message-overlay'); + const prevButton = document.querySelector('.prev-button'); + const nextButton = document.querySelector('.next-button'); + const carouselText = document.querySelector('.carousel-text'); let isMuted = false; let isScreenSharing = false; let isWebcamOn = false; let isConnected = false; + let isConnecting = false; + + // Carousel handlers + prevButton.addEventListener('click', () => { + ipcRenderer.send('carousel-action', 'prev'); + }); + + nextButton.addEventListener('click', () => { + ipcRenderer.send('carousel-action', 'next'); + }); + + // Handle carousel updates + ipcRenderer.on('update-carousel', (event, modeName) => { + carouselText.textContent = modeName; + }); micButton.addEventListener('click', () => { + if (!isConnected) return; isMuted = !isMuted; micButton.querySelector('span').textContent = isMuted ? 'mic_off' : 'mic'; ipcRenderer.send('control-action', { type: 'mic', value: !isMuted }); }); screenButton.addEventListener('click', () => { - isScreenSharing = !isScreenSharing; - screenButton.querySelector('span').textContent = isScreenSharing ? 'cancel_presentation' : 'present_to_all'; - ipcRenderer.send('control-action', { type: 'screen', value: isScreenSharing }); + if (!isConnected) return; + if (isScreenSharing) { + isScreenSharing = false; + screenButton.querySelector('span').textContent = 'present_to_all'; + screenButton.querySelector('span').classList.remove('filled'); + ipcRenderer.send('control-action', { type: 'screen', value: false }); + messageOverlay.classList.remove('visible'); + } else { + ipcRenderer.send('control-action', { type: 'screen', value: true }); + messageOverlay.classList.add('visible'); + } }); webcamButton.addEventListener('click', () => { + if (!isConnected) return; isWebcamOn = !isWebcamOn; webcamButton.querySelector('span').textContent = isWebcamOn ? 'videocam_off' : 'videocam'; ipcRenderer.send('control-action', { type: 'webcam', value: isWebcamOn }); }); connectButton.addEventListener('click', () => { - isConnected = !isConnected; - connectButton.querySelector('span').textContent = isConnected ? 'pause' : 'play_arrow'; - ipcRenderer.send('control-action', { type: 'connect', value: isConnected }); + if (!isConnecting) { + isConnecting = true; + ipcRenderer.send('control-action', { type: 'connect', value: !isConnected }); + } + }); + + // Handle screen share result + ipcRenderer.on('screen-share-result', (event, success) => { + messageOverlay.classList.remove('visible'); + if (success) { + isScreenSharing = true; + screenButton.querySelector('span').textContent = 'cancel_presentation'; + screenButton.querySelector('span').classList.add('filled'); + } }); // Handle state updates from main process @@ -312,6 +448,12 @@ async function videoWindow() { isScreenSharing = state.isScreenSharing; isWebcamOn = state.isWebcamOn; isConnected = state.isConnected; + isConnecting = false; + + // If screen sharing was stopped from main window, hide the message + if (!isScreenSharing) { + messageOverlay.classList.remove('visible'); + } // Update button states micButton.querySelector('span').textContent = isMuted ? 'mic_off' : 'mic'; @@ -326,7 +468,7 @@ async function videoWindow() { connectButton.querySelector('span').classList.toggle('filled', isConnected); // Update disabled state of the nav - document.querySelector('.actions-nav').classList.toggle('disabled', !isConnected); + actionsNav.classList.toggle('disabled', !isConnected); }); @@ -469,7 +611,7 @@ ipcMain.on('write-text', async (event, content) => { // Add this after the other ipcMain handlers ipcMain.on('control-action', (event, action) => { - // Forward the control action to the main window + // Forward all control actions to the main window if (mainWindow) { mainWindow.webContents.send('control-action', action); } @@ -485,6 +627,31 @@ ipcMain.on('update-control-state', (event, state) => { } }); +// Add this to handle screen selection result +ipcMain.on('screen-share-result', (event, success) => { + const windows = BrowserWindow.getAllWindows(); + const videoWindow = windows.find(win => win !== mainWindow && win !== overlayWindow); + if (videoWindow) { + videoWindow.webContents.send('screen-share-result', success); + } +}); + +// Add this to handle carousel actions +ipcMain.on('carousel-action', (event, direction) => { + if (mainWindow) { + mainWindow.webContents.send('carousel-action', direction); + } +}); + +// Add this to handle carousel updates +ipcMain.on('update-carousel', (event, modeName) => { + const windows = BrowserWindow.getAllWindows(); + const videoWindow = windows.find(win => win !== mainWindow && win !== overlayWindow); + if (videoWindow) { + videoWindow.webContents.send('update-carousel', modeName); + } +}); + app.whenReady().then(createWindow); app.on('window-all-closed', () => { diff --git a/src/components/control-tray/ControlTray.tsx b/src/components/control-tray/ControlTray.tsx index be3e6015..34dab6d2 100644 --- a/src/components/control-tray/ControlTray.tsx +++ b/src/components/control-tray/ControlTray.tsx @@ -159,6 +159,10 @@ function ControlTray({ const mediaStream = await next.start(); setActiveVideoStream(mediaStream); onVideoStreamChange(mediaStream); + // Send success result for screen sharing + if (next === screenCapture) { + ipcRenderer.send('screen-share-result', true); + } } catch (error) { // Silently handle cancellation, but still log other errors if (!(error instanceof Error && error.message === 'Selection cancelled')) { @@ -166,6 +170,10 @@ function ControlTray({ } setActiveVideoStream(null); onVideoStreamChange(null); + // Send failure result for screen sharing + if (next === screenCapture) { + ipcRenderer.send('screen-share-result', false); + } } } else { setActiveVideoStream(null); @@ -177,7 +185,12 @@ function ControlTray({ useEffect(() => { setSelectedOption(modes[carouselIndex]); + // Send carousel update to control window + const mode = modes[carouselIndex].value as keyof typeof assistantConfigs; + const modeName = assistantConfigs[mode].display_name; + ipcRenderer.send('update-carousel', modeName); }, [carouselIndex, modes, setSelectedOption]); + const handleCarouselChange = (direction: 'next' | 'prev') => { setCarouselIndex(prevIndex => { const newIndex = direction === 'next' @@ -187,6 +200,18 @@ function ControlTray({ }); }; + // Handle carousel actions from control window + useEffect(() => { + const handleCarouselAction = (event: any, direction: 'next' | 'prev') => { + handleCarouselChange(direction); + }; + + ipcRenderer.on('carousel-action', handleCarouselAction); + return () => { + ipcRenderer.removeListener('carousel-action', handleCarouselAction); + }; + }, []); + // Handle control actions from video window useEffect(() => { const handleControlAction = (event: any, action: { type: string; value: boolean }) => { @@ -196,8 +221,10 @@ function ControlTray({ break; case 'screen': if (action.value) { + // Start screen sharing changeStreams(screenCapture)(); } else { + // Stop screen sharing changeStreams()(); } break; @@ -222,7 +249,7 @@ function ControlTray({ return () => { ipcRenderer.removeListener('control-action', handleControlAction); }; - }, [connect, disconnect, webcam, screenCapture]); + }, [connect, disconnect, webcam, screenCapture, changeStreams]); // Send state updates to video window useEffect(() => {