diff --git a/src/bridge/featureBridge.js b/src/bridge/featureBridge.js index be6d348..b7f1a6c 100644 --- a/src/bridge/featureBridge.js +++ b/src/bridge/featureBridge.js @@ -91,6 +91,7 @@ module.exports = { ipcMain.handle('listen:startMacosSystemAudio', async () => await listenService.handleStartMacosAudio()); ipcMain.handle('listen:stopMacosSystemAudio', async () => await listenService.handleStopMacosAudio()); ipcMain.handle('update-google-search-setting', async (event, enabled) => await listenService.handleUpdateGoogleSearchSetting(enabled)); + ipcMain.handle('listen:isSessionActive', async () => await listenService.isSessionActive()); ipcMain.handle('listen:changeSession', async (event, listenButtonText) => { console.log('[FeatureBridge] listen:changeSession from mainheader', listenButtonText); try { diff --git a/src/bridge/windowBridge.js b/src/bridge/windowBridge.js index 313273c..054ce0b 100644 --- a/src/bridge/windowBridge.js +++ b/src/bridge/windowBridge.js @@ -20,7 +20,6 @@ module.exports = { ipcMain.on('header-state-changed', (event, state) => windowManager.handleHeaderStateChanged(state)); ipcMain.on('header-animation-finished', (event, state) => windowManager.handleHeaderAnimationFinished(state)); ipcMain.handle('get-header-position', () => windowManager.getHeaderPosition()); - ipcMain.handle('move-header', (event, newX, newY) => windowManager.moveHeader(newX, newY)); ipcMain.handle('move-header-to', (event, newX, newY) => windowManager.moveHeaderTo(newX, newY)); ipcMain.handle('adjust-window-height', (event, targetHeight) => windowManager.adjustWindowHeight(event.sender, targetHeight)); }, diff --git a/src/features/listen/listenService.js b/src/features/listen/listenService.js index d879d60..e3a32c7 100644 --- a/src/features/listen/listenService.js +++ b/src/features/listen/listenService.js @@ -54,53 +54,7 @@ class ListenService { } async handleListenRequest(listenButtonText) { - const { windowPool, updateLayout } = require('../../window/windowManager'); - const listenWindow = windowPool.get('listen'); - const header = windowPool.get('header'); - - try { - switch (listenButtonText) { - case 'Listen': - console.log('[ListenService] changeSession to "Listen"'); - listenWindow.show(); - updateLayout(); - listenWindow.webContents.send('window-show-animation'); - await this.initializeSession(); - listenWindow.webContents.send('session-state-changed', { isActive: true }); - break; - - case 'Stop': - console.log('[ListenService] changeSession to "Stop"'); - await this.closeSession(); - listenWindow.webContents.send('session-state-changed', { isActive: false }); - break; - - case 'Done': - console.log('[ListenService] changeSession to "Done"'); - listenWindow.webContents.send('window-hide-animation'); - listenWindow.webContents.send('session-state-changed', { isActive: false }); - break; - - default: - throw new Error(`[ListenService] unknown listenButtonText: ${listenButtonText}`); - } - - header.webContents.send('listen:changeSessionResult', { success: true }); - - } catch (error) { - console.error('[ListenService] error in handleListenRequest:', error); - header.webContents.send('listen:changeSessionResult', { success: false }); - throw error; - } - } - - initialize() { - this.setupIpcHandlers(); - console.log('[ListenService] Initialized and ready.'); - } - - async handleListenRequest(listenButtonText) { - const { windowPool, updateLayout } = require('../../window/windowManager'); + const { windowPool } = require('../../window/windowManager'); const listenWindow = windowPool.get('listen'); const header = windowPool.get('header'); diff --git a/src/preload.js b/src/preload.js index 04b1046..2f4112c 100644 --- a/src/preload.js +++ b/src/preload.js @@ -273,7 +273,7 @@ contextBridge.exposeInMainWorld('api', { stopMacosSystemAudio: () => ipcRenderer.invoke('listen:stopMacosSystemAudio'), // Session Management - isSessionActive: () => ipcRenderer.invoke('is-session-active'), + isSessionActive: () => ipcRenderer.invoke('listen:isSessionActive'), // Listeners onSystemAudioData: (callback) => ipcRenderer.on('system-audio-data', callback), diff --git a/src/ui/listen/audioCore/listenCapture.js b/src/ui/listen/audioCore/listenCapture.js index 2f52f25..ade889e 100644 --- a/src/ui/listen/audioCore/listenCapture.js +++ b/src/ui/listen/audioCore/listenCapture.js @@ -422,6 +422,12 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu try { if (isMacOS) { + + const sessionActive = await window.api.listenCapture.isSessionActive(); + if (!sessionActive) { + throw new Error('STT sessions not initialized - please wait for initialization to complete'); + } + // On macOS, use SystemAudioDump for audio and getDisplayMedia for screen console.log('Starting macOS capture with SystemAudioDump...'); @@ -466,6 +472,12 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu console.log('macOS screen capture started - audio handled by SystemAudioDump'); } else if (isLinux) { + + const sessionActive = await window.api.listenCapture.isSessionActive(); + if (!sessionActive) { + throw new Error('STT sessions not initialized - please wait for initialization to complete'); + } + // Linux - use display media for screen capture and getUserMedia for microphone mediaStream = await navigator.mediaDevices.getDisplayMedia({ video: { diff --git a/src/window/smoothMovementManager.js b/src/window/smoothMovementManager.js index c81f0a4..044fb00 100644 --- a/src/window/smoothMovementManager.js +++ b/src/window/smoothMovementManager.js @@ -1,11 +1,27 @@ const { screen } = require('electron'); + +function getCurrentDisplay(window) { + if (!window || window.isDestroyed()) return screen.getPrimaryDisplay(); + + const windowBounds = window.getBounds(); + const windowCenter = { + x: windowBounds.x + windowBounds.width / 2, + y: windowBounds.y + windowBounds.height / 2, + }; + + return screen.getDisplayNearestPoint(windowCenter); +} + +function getDisplayById(displayId) { + const displays = screen.getAllDisplays(); + return displays.find(d => d.id === displayId) || screen.getPrimaryDisplay(); +} + class SmoothMovementManager { - constructor(windowPool, getDisplayById, getCurrentDisplay, updateLayout) { + constructor(windowPool, layoutManager) { this.windowPool = windowPool; - this.getDisplayById = getDisplayById; - this.getCurrentDisplay = getCurrentDisplay; - this.updateLayout = updateLayout; + this.layoutManager = layoutManager; this.stepSize = 80; this.animationDuration = 300; this.headerPosition = { x: 0, y: 0 }; @@ -39,11 +55,11 @@ class SmoothMovementManager { const header = this.windowPool.get('header'); if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return; - const targetDisplay = this.getDisplayById(displayId); + const targetDisplay = getDisplayById(displayId); if (!targetDisplay) return; const currentBounds = header.getBounds(); - const currentDisplay = this.getCurrentDisplay(header); + const currentDisplay = getCurrentDisplay(header); if (currentDisplay.id === targetDisplay.id) return; @@ -60,56 +76,6 @@ class SmoothMovementManager { this.currentDisplayId = targetDisplay.id; } - hideToEdge(edge, callback, { instant = false } = {}) { - const header = this.windowPool.get('header'); - if (!header || header.isDestroyed()) { - if (typeof callback === 'function') callback(); - return; - } - - const { x, y } = header.getBounds(); - this.lastVisiblePosition = { x, y }; - this.hiddenPosition = { edge }; - - if (instant) { - header.hide(); - if (typeof callback === 'function') callback(); - return; - } - - header.webContents.send('window-hide-animation'); - - setTimeout(() => { - if (!header.isDestroyed()) header.hide(); - if (typeof callback === 'function') callback(); - }, 5); - } - - showFromEdge(callback) { - const header = this.windowPool.get('header'); - if (!header || header.isDestroyed()) { - if (typeof callback === 'function') callback(); - return; - } - - // 숨기기 전에 기억해둔 위치 복구 - if (this.lastVisiblePosition) { - header.setPosition( - this.lastVisiblePosition.x, - this.lastVisiblePosition.y, - false // animate: false - ); - } - - header.show(); - header.webContents.send('window-show-animation'); - - // 내부 상태 초기화 - this.hiddenPosition = null; - this.lastVisiblePosition = null; - - if (typeof callback === 'function') callback(); - } moveStep(direction) { const header = this.windowPool.get('header'); @@ -211,7 +177,7 @@ class SmoothMovementManager { setTimeout(step, 8); // requestAnimationFrame 대신 setTimeout으로 간결하게 처리 } else { // 애니메이션 종료 - this.updateLayout(); // 레이아웃 재정렬 + this.layoutManager.updateLayout(); // 레이아웃 재정렬 if (onComplete) { onComplete(); // 완료 콜백 실행 } @@ -267,7 +233,7 @@ class SmoothMovementManager { // Update header position to the actual final position this.headerPosition = { x: Math.round(targetX), y: Math.round(targetY) }; } - this.updateLayout(); + this.layoutManager.updateLayout(); } }; animate(); @@ -277,7 +243,7 @@ class SmoothMovementManager { const header = this.windowPool.get('header'); if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return; - const display = this.getCurrentDisplay(header); + const display = getCurrentDisplay(header); const { width, height } = display.workAreaSize; const { x: workAreaX, y: workAreaY } = display.workArea; const currentBounds = header.getBounds(); @@ -313,7 +279,7 @@ class SmoothMovementManager { }); this.headerPosition = { x: targetX, y: targetY }; - this.updateLayout(); + this.layoutManager.updateLayout(); } destroy() { diff --git a/src/window/windowLayoutManager.js b/src/window/windowLayoutManager.js index cdd0ef3..f4ba051 100644 --- a/src/window/windowLayoutManager.js +++ b/src/window/windowLayoutManager.js @@ -37,6 +37,110 @@ class WindowLayoutManager { }); } + getHeaderPosition = () => { + const header = this.windowPool.get('header'); + if (header) { + const [x, y] = header.getPosition(); + return { x, y }; + } + return { x: 0, y: 0 }; + }; + + resizeHeaderWindow = ({ width, height }) => { + const header = this.windowPool.get('header'); + if (header) { + console.log(`[WindowManager] Resize request: ${width}x${height}`); + + const currentBounds = header.getBounds(); + console.log(`[WindowManager] Current bounds: ${currentBounds.width}x${currentBounds.height} at (${currentBounds.x}, ${currentBounds.y})`); + + if (currentBounds.width === width && currentBounds.height === height) { + console.log('[WindowManager] Already at target size, skipping resize'); + return { success: true }; + } + + const wasResizable = header.isResizable(); + if (!wasResizable) { + header.setResizable(true); + } + + const centerX = currentBounds.x + currentBounds.width / 2; + const newX = Math.round(centerX - width / 2); + + const display = getCurrentDisplay(header); + const { x: workAreaX, width: workAreaWidth } = display.workArea; + + const clampedX = Math.max(workAreaX, Math.min(workAreaX + workAreaWidth - width, newX)); + + header.setBounds({ x: clampedX, y: currentBounds.y, width, height }); + + if (!wasResizable) { + header.setResizable(false); + } + + this.updateLayout(); + + return { success: true }; + } + return { success: false, error: 'Header window not found' }; + }; + + moveHeaderTo = (newX, newY) => { + const header = this.windowPool.get('header'); + if (header) { + const targetDisplay = screen.getDisplayNearestPoint({ x: newX, y: newY }); + const { x: workAreaX, y: workAreaY, width, height } = targetDisplay.workArea; + const headerBounds = header.getBounds(); + + let clampedX = newX; + let clampedY = newY; + + if (newX < workAreaX) { + clampedX = workAreaX; + } else if (newX + headerBounds.width > workAreaX + width) { + clampedX = workAreaX + width - headerBounds.width; + } + + if (newY < workAreaY) { + clampedY = workAreaY; + } else if (newY + headerBounds.height > workAreaY + height) { + clampedY = workAreaY + height - headerBounds.height; + } + + header.setPosition(clampedX, clampedY, false); + this.updateLayout(); + } + }; + + adjustWindowHeight = (sender, targetHeight) => { + const senderWindow = this.windowPool.get(sender); + if (senderWindow) { + const wasResizable = senderWindow.isResizable(); + if (!wasResizable) { + senderWindow.setResizable(true); + } + + const currentBounds = senderWindow.getBounds(); + const minHeight = senderWindow.getMinimumSize()[1]; + const maxHeight = senderWindow.getMaximumSize()[1]; + + let adjustedHeight; + if (maxHeight === 0) { + adjustedHeight = Math.max(minHeight, targetHeight); + } else { + adjustedHeight = Math.max(minHeight, Math.min(maxHeight, targetHeight)); + } + + senderWindow.setSize(currentBounds.width, adjustedHeight, false); + + if (!wasResizable) { + senderWindow.setResizable(false); + } + + this.updateLayout(); + } + }; + /** * * @param {object} [visibilityOverride] - { listen: true, ask: true } diff --git a/src/window/windowManager.js b/src/window/windowManager.js index ac558db..f57dcd5 100644 --- a/src/window/windowManager.js +++ b/src/window/windowManager.js @@ -38,11 +38,6 @@ let settingsHideTimer = null; let layoutManager = null; -function updateLayout() { - if (layoutManager) { - layoutManager.updateLayout(); - } -} let movementManager = null; /** @@ -92,6 +87,30 @@ const cancelHideSettingsWindow = () => { internalBridge.emit('window:requestVisibility', { name: 'settings', visible: true }); }; +const moveWindowStep = (direction) => { + internalBridge.emit('window:moveStep', { direction }); +}; + +const resizeHeaderWindow = ({ width, height }) => { + internalBridge.emit('window:resizeHeaderWindow', { width, height }); +}; + +const handleHeaderAnimationFinished = (state) => { + internalBridge.emit('window:headerAnimationFinished', state); +}; + +const getHeaderPosition = () => { + internalBridge.emit('window:getHeaderPosition'); +}; + +const moveHeaderTo = (newX, newY) => { + internalBridge.emit('window:moveHeaderTo', { newX, newY }); +}; + +const adjustWindowHeight = (sender, targetHeight) => { + internalBridge.emit('window:adjustWindowHeight', { sender, targetHeight }); +}; + function setupWindowController(windowPool, layoutManager, movementManager) { internalBridge.on('window:requestVisibility', ({ name, visible }) => { @@ -109,6 +128,21 @@ function setupWindowController(windowPool, layoutManager, movementManager) { internalBridge.on('window:moveStep', ({ direction }) => { movementManager.moveStep(direction); }); + internalBridge.on('window:resizeHeaderWindow', ({ width, height }) => { + resizingHeaderWindow(layoutManager, movementManager, { width, height }); + }); + internalBridge.on('window:headerAnimationFinished', (state) => { + handlingHeaderAnimationFinished(windowPool, layoutManager, state); + }); + internalBridge.on('window:getHeaderPosition', () => { + gettingHeaderPosition(layoutManager); + }); + internalBridge.on('window:moveHeaderTo', ({ newX, newY }) => { + movingHeaderTo(layoutManager, newX, newY); + }); + internalBridge.on('window:adjustWindowHeight', ({ sender, targetHeight }) => { + adjustingWindowHeight(layoutManager, sender, targetHeight); + }); } function changeAllWindowsVisibility(windowPool, targetVisibility) { @@ -249,8 +283,8 @@ async function handleWindowVisibilityRequest(windowPool, layoutManager, movement const otherWin = windowPool.get(otherName); const isOtherWinVisible = otherWin && !otherWin.isDestroyed() && otherWin.isVisible(); - const ANIM_OFFSET_X = 100; - const ANIM_OFFSET_Y = 20; + const ANIM_OFFSET_X = 50; + const ANIM_OFFSET_Y = 20; if (shouldBeVisible) { win.setOpacity(0); @@ -305,7 +339,7 @@ async function handleWindowVisibilityRequest(windowPool, layoutManager, movement } else { const currentBounds = win.getBounds(); fadeWindow( - win, 1, 0, undefined, + win, 1, 0, 250, () => win.hide() ); if (name === 'listen') { @@ -313,7 +347,7 @@ async function handleWindowVisibilityRequest(windowPool, layoutManager, movement const targetX = currentBounds.x - ANIM_OFFSET_X; movementManager.animateWindow(win, targetX, currentBounds.y); } else { - const targetX = currentBounds.x - currentBounds.width; + const targetX = currentBounds.x - ANIM_OFFSET_X; movementManager.animateWindow(win, targetX, currentBounds.y); } } else if (name === 'ask') { @@ -353,52 +387,6 @@ const toggleContentProtection = () => { return newStatus; }; -const resizeHeaderWindow = ({ width, height }) => { - const header = windowPool.get('header'); - if (header) { - console.log(`[WindowManager] Resize request: ${width}x${height}`); - - if (movementManager && movementManager.isAnimating) { - console.log('[WindowManager] Skipping resize during animation'); - return { success: false, error: 'Cannot resize during animation' }; - } - - const currentBounds = header.getBounds(); - console.log(`[WindowManager] Current bounds: ${currentBounds.width}x${currentBounds.height} at (${currentBounds.x}, ${currentBounds.y})`); - - if (currentBounds.width === width && currentBounds.height === height) { - console.log('[WindowManager] Already at target size, skipping resize'); - return { success: true }; - } - - const wasResizable = header.isResizable(); - if (!wasResizable) { - header.setResizable(true); - } - - const centerX = currentBounds.x + currentBounds.width / 2; - const newX = Math.round(centerX - width / 2); - - const display = getCurrentDisplay(header); - const { x: workAreaX, width: workAreaWidth } = display.workArea; - - const clampedX = Math.max(workAreaX, Math.min(workAreaX + workAreaWidth - width, newX)); - - header.setBounds({ x: clampedX, y: currentBounds.y, width, height }); - - if (!wasResizable) { - header.setResizable(false); - } - - if (updateLayout) { - updateLayout(); - } - - return { success: true }; - } - return { success: false, error: 'Header window not found' }; -}; - const openLoginPage = () => { const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000'; @@ -407,12 +395,6 @@ const openLoginPage = () => { console.log('Opening personalization page:', personalizeUrl); }; -const moveWindowStep = (direction) => { - if (movementManager) { - movementManager.moveStep(direction); - } -}; - function createFeatureWindows(header, namesToCreate) { // if (windowPool.has('listen')) return; @@ -617,14 +599,6 @@ function getCurrentDisplay(window) { return screen.getDisplayNearestPoint(windowCenter); } -function getDisplayById(displayId) { - const displays = screen.getAllDisplays(); - return displays.find(d => d.id === displayId) || screen.getPrimaryDisplay(); -} - - - - function createWindows() { @@ -636,8 +610,7 @@ function createWindows() { const initialX = Math.round((screenWidth - DEFAULT_WINDOW_WIDTH) / 2); const initialY = workAreaY + 21; - movementManager = new SmoothMovementManager(windowPool, getDisplayById, getCurrentDisplay, updateLayout); - + const header = new BrowserWindow({ width: DEFAULT_WINDOW_WIDTH, height: HEADER_HEIGHT, @@ -687,15 +660,17 @@ function createWindows() { }); } windowPool.set('header', header); - header.on('moved', updateLayout); layoutManager = new WindowLayoutManager(windowPool); + movementManager = new SmoothMovementManager(windowPool, layoutManager); + + header.on('moved', () => layoutManager.updateLayout()); header.webContents.once('dom-ready', () => { shortcutsService.initialize(windowPool); shortcutsService.registerShortcuts(); }); - setupIpcHandlers(movementManager); + setupIpcHandlers(movementManager, layoutManager); setupWindowController(windowPool, layoutManager, movementManager); if (currentHeaderState === 'main') { @@ -729,13 +704,13 @@ function createWindows() { header.on('resize', () => { console.log('[WindowManager] Header resize event triggered'); - updateLayout(); + layoutManager.updateLayout(); }); return windowPool; } -function setupIpcHandlers(movementManager) { +function setupIpcHandlers(movementManager, layoutManager) { // quit-application handler moved to windowBridge.js to avoid duplication screen.on('display-added', (event, newDisplay) => { console.log('[Display] New display added:', newDisplay.id); @@ -752,10 +727,11 @@ function setupIpcHandlers(movementManager) { screen.on('display-metrics-changed', (event, display, changedMetrics) => { // console.log('[Display] Display metrics changed:', display.id, changedMetrics); - updateLayout(); + layoutManager.updateLayout(); }); } + const handleHeaderStateChanged = (state) => { console.log(`[WindowManager] Header state changed to: ${state}`); currentHeaderState = state; @@ -768,7 +744,17 @@ const handleHeaderStateChanged = (state) => { internalBridge.emit('reregister-shortcuts'); }; -const handleHeaderAnimationFinished = (state) => { + +const resizingHeaderWindow = (layoutManager, movementManager, { width, height }) => { + if (movementManager.isAnimating) { + console.log('[WindowManager] Skipping resize during animation'); + return { success: false, error: 'Cannot resize during animation' }; + } + + return layoutManager.resizeHeaderWindow({ width, height }); +}; + +const handlingHeaderAnimationFinished = (windowPool, layoutManager, state) => { const header = windowPool.get('header'); if (!header || header.isDestroyed()) return; @@ -777,87 +763,24 @@ const handleHeaderAnimationFinished = (state) => { console.log('[WindowManager] Header hidden after animation.'); } else if (state === 'visible') { console.log('[WindowManager] Header shown after animation.'); - updateLayout(); + layoutManager.updateLayout(); } }; -const getHeaderPosition = () => { - const header = windowPool.get('header'); - if (header) { - const [x, y] = header.getPosition(); - return { x, y }; - } - return { x: 0, y: 0 }; +const gettingHeaderPosition = (layoutManager) => { + return layoutManager.getHeaderPosition(); }; -const moveHeader = (newX, newY) => { - const header = windowPool.get('header'); - if (header) { - const currentY = newY !== undefined ? newY : header.getBounds().y; - header.setPosition(newX, currentY, false); - updateLayout(); - } +const movingHeaderTo = (layoutManager, newX, newY) => { + layoutManager.moveHeaderTo(newX, newY); }; -const moveHeaderTo = (newX, newY) => { - const header = windowPool.get('header'); - if (header) { - const targetDisplay = screen.getDisplayNearestPoint({ x: newX, y: newY }); - const { x: workAreaX, y: workAreaY, width, height } = targetDisplay.workArea; - const headerBounds = header.getBounds(); - - let clampedX = newX; - let clampedY = newY; - - if (newX < workAreaX) { - clampedX = workAreaX; - } else if (newX + headerBounds.width > workAreaX + width) { - clampedX = workAreaX + width - headerBounds.width; - } - - if (newY < workAreaY) { - clampedY = workAreaY; - } else if (newY + headerBounds.height > workAreaY + height) { - clampedY = workAreaY + height - headerBounds.height; - } - - header.setPosition(clampedX, clampedY, false); - updateLayout(); - } -}; - -const adjustWindowHeight = (sender, targetHeight) => { - const senderWindow = BrowserWindow.fromWebContents(sender); - if (senderWindow) { - const wasResizable = senderWindow.isResizable(); - if (!wasResizable) { - senderWindow.setResizable(true); - } - - const currentBounds = senderWindow.getBounds(); - const minHeight = senderWindow.getMinimumSize()[1]; - const maxHeight = senderWindow.getMaximumSize()[1]; - - let adjustedHeight; - if (maxHeight === 0) { - adjustedHeight = Math.max(minHeight, targetHeight); - } else { - adjustedHeight = Math.max(minHeight, Math.min(maxHeight, targetHeight)); - } - - senderWindow.setSize(currentBounds.width, adjustedHeight, false); - - if (!wasResizable) { - senderWindow.setResizable(false); - } - - updateLayout(); - } +const adjustingWindowHeight = (layoutManager, sender, targetHeight) => { + layoutManager.adjustWindowHeight(sender, targetHeight); }; module.exports = { - updateLayout, createWindows, windowPool, toggleContentProtection, @@ -871,7 +794,6 @@ module.exports = { handleHeaderStateChanged, handleHeaderAnimationFinished, getHeaderPosition, - moveHeader, moveHeaderTo, adjustWindowHeight, }; \ No newline at end of file