const { BrowserWindow, globalShortcut, ipcMain, screen, app, shell, desktopCapturer } = require('electron'); const WindowLayoutManager = require('./windowLayoutManager'); const SmoothMovementManager = require('./smoothMovementManager'); const path = require('node:path'); const fs = require('node:fs'); const os = require('os'); const util = require('util'); const execFile = util.promisify(require('child_process').execFile); // Try to load sharp, but don't fail if it's not available let sharp; try { sharp = require('sharp'); console.log('[WindowManager] Sharp module loaded successfully'); } catch (error) { console.warn('[WindowManager] Sharp module not available:', error.message); console.warn('[WindowManager] Screenshot functionality will work with reduced image processing capabilities'); sharp = null; } const authService = require('../common/services/authService'); const systemSettingsRepository = require('../common/repositories/systemSettings'); const userRepository = require('../common/repositories/user'); const fetch = require('node-fetch'); const Store = require('electron-store'); const shortCutStore = new Store({ name: 'user-preferences', defaults: { customKeybinds: {} } }); /* ────────────────[ GLASS BYPASS ]─────────────── */ let liquidGlass; const isLiquidGlassSupported = () => { if (process.platform !== 'darwin') { return false; } const majorVersion = parseInt(os.release().split('.')[0], 10); // return majorVersion >= 25; // macOS 26+ (Darwin 25+) return majorVersion >= 26; // See you soon! }; let shouldUseLiquidGlass = isLiquidGlassSupported(); if (shouldUseLiquidGlass) { try { liquidGlass = require('electron-liquid-glass'); } catch (e) { console.warn('Could not load optional dependency "electron-liquid-glass". The feature will be disabled.'); shouldUseLiquidGlass = false; } } /* ────────────────[ GLASS BYPASS ]─────────────── */ let isContentProtectionOn = true; let currentDisplayId = null; let mouseEventsIgnored = false; let lastVisibleWindows = new Set(['header']); const HEADER_HEIGHT = 47; const DEFAULT_WINDOW_WIDTH = 353; let currentHeaderState = 'apikey'; const windowPool = new Map(); let fixedYPosition = 0; let lastScreenshot = null; let settingsHideTimer = null; let selectedCaptureSourceId = null; // let shortcutEditorWindow = null; let layoutManager = null; function updateLayout() { if (layoutManager) { layoutManager.updateLayout(); } } let movementManager = null; const windowBridge = require('../bridge/windowBridge'); async function toggleFeature(featureName) { if (!windowPool.get(featureName) && currentHeaderState === 'main') { createFeatureWindows(windowPool.get('header')); } const header = windowPool.get('header'); if (featureName === 'listen') { console.log(`[WindowManager] Toggling feature: ${featureName}`); const listenWindow = windowPool.get(featureName); const listenService = global.listenService; if (listenService && listenService.isSessionActive()) { console.log('[WindowManager] Listen session is active, closing it via toggle.'); await listenService.closeSession(); listenWindow.webContents.send('session-state-changed', { isActive: false }); header.webContents.send('session-state-text', 'Done'); // return; } else { if (listenWindow.isVisible()) { listenWindow.webContents.send('window-hide-animation'); listenWindow.webContents.send('session-state-changed', { isActive: false }); header.webContents.send('session-state-text', 'Listen'); } else { listenWindow.show(); updateLayout(); listenWindow.webContents.send('window-show-animation'); await listenService.initializeSession(); listenWindow.webContents.send('session-state-changed', { isActive: true }); header.webContents.send('session-state-text', 'Stop'); } } } if (featureName === 'ask') { let askWindow = windowPool.get('ask'); if (!askWindow || askWindow.isDestroyed()) { console.log('[WindowManager] Ask window not found, creating new one'); return; } if (askWindow.isVisible()) { try { const hasResponse = await askWindow.webContents.executeJavaScript(` (() => { try { // PickleGlassApp의 Shadow DOM 내부로 접근 const pickleApp = document.querySelector('pickle-glass-app'); if (!pickleApp || !pickleApp.shadowRoot) { console.log('PickleGlassApp not found'); return false; } // PickleGlassApp의 shadowRoot 내부에서 ask-view 찾기 const askView = pickleApp.shadowRoot.querySelector('ask-view'); if (!askView) { console.log('AskView not found in PickleGlassApp shadow DOM'); return false; } console.log('AskView found, checking state...'); console.log('currentResponse:', askView.currentResponse); console.log('isLoading:', askView.isLoading); console.log('isStreaming:', askView.isStreaming); const hasContent = !!(askView.currentResponse || askView.isLoading || askView.isStreaming); if (!hasContent && askView.shadowRoot) { const responseContainer = askView.shadowRoot.querySelector('.response-container'); if (responseContainer && !responseContainer.classList.contains('hidden')) { const textContent = responseContainer.textContent.trim(); const hasActualContent = textContent && !textContent.includes('Ask a question to see the response here') && textContent.length > 0; console.log('Response container content check:', hasActualContent); return hasActualContent; } } return hasContent; } catch (error) { console.error('Error checking AskView state:', error); return false; } })() `); console.log(`[WindowManager] Ask window visible, hasResponse: ${hasResponse}`); if (hasResponse) { askWindow.webContents.send('toggle-text-input'); console.log('[WindowManager] Sent toggle-text-input command'); } else { console.log('[WindowManager] No response found, closing window'); askWindow.webContents.send('window-hide-animation'); } } catch (error) { console.error('[WindowManager] Error checking Ask window state:', error); console.log('[WindowManager] Falling back to toggle text input'); askWindow.webContents.send('toggle-text-input'); } } else { console.log('[WindowManager] Showing hidden Ask window'); askWindow.show(); updateLayout(); askWindow.webContents.send('window-show-animation'); askWindow.webContents.send('window-did-show'); } } if (featureName === 'settings') { const settingsWindow = windowPool.get(featureName); if (settingsWindow) { if (settingsWindow.isDestroyed()) { console.error(`Window ${featureName} is destroyed, cannot toggle`); return; } if (settingsWindow.isVisible()) { if (featureName === 'settings') { settingsWindow.webContents.send('settings-window-hide-animation'); } else { settingsWindow.webContents.send('window-hide-animation'); } } else { try { settingsWindow.show(); updateLayout(); settingsWindow.webContents.send('window-show-animation'); } catch (e) { console.error('Error showing window:', e); } } } else { console.error(`Window not found for feature: ${featureName}`); console.error('Available windows:', Array.from(windowPool.keys())); } } } function createFeatureWindows(header, namesToCreate) { // if (windowPool.has('listen')) return; const commonChildOptions = { parent: header, show: false, frame: false, transparent: true, vibrancy: false, hasShadow: false, skipTaskbar: true, hiddenInMissionControl: true, resizable: true, webPreferences: { nodeIntegration: true, contextIsolation: false }, }; const createFeatureWindow = (name) => { if (windowPool.has(name)) return; switch (name) { case 'listen': { const listen = new BrowserWindow({ ...commonChildOptions, width:400,minWidth:400,maxWidth:900, maxHeight:900, }); listen.setContentProtection(isContentProtectionOn); listen.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); if (process.platform === 'darwin') { listen.setWindowButtonVisibility(false); } const listenLoadOptions = { query: { view: 'listen' } }; if (!shouldUseLiquidGlass) { listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions); } else { listenLoadOptions.query.glass = 'true'; listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions); listen.webContents.once('did-finish-load', () => { const viewId = liquidGlass.addView(listen.getNativeWindowHandle()); if (viewId !== -1) { liquidGlass.unstable_setVariant(viewId, liquidGlass.GlassMaterialVariant.bubbles); // liquidGlass.unstable_setScrim(viewId, 1); // liquidGlass.unstable_setSubdued(viewId, 1); } }); } if (!app.isPackaged) { listen.webContents.openDevTools({ mode: 'detach' }); } windowPool.set('listen', listen); break; } // ask case 'ask': { const ask = new BrowserWindow({ ...commonChildOptions, width:600 }); ask.setContentProtection(isContentProtectionOn); ask.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); if (process.platform === 'darwin') { ask.setWindowButtonVisibility(false); } const askLoadOptions = { query: { view: 'ask' } }; if (!shouldUseLiquidGlass) { ask.loadFile(path.join(__dirname, '../app/content.html'), askLoadOptions); } else { askLoadOptions.query.glass = 'true'; ask.loadFile(path.join(__dirname, '../app/content.html'), askLoadOptions); ask.webContents.once('did-finish-load', () => { const viewId = liquidGlass.addView(ask.getNativeWindowHandle()); if (viewId !== -1) { liquidGlass.unstable_setVariant(viewId, liquidGlass.GlassMaterialVariant.bubbles); // liquidGlass.unstable_setScrim(viewId, 1); // liquidGlass.unstable_setSubdued(viewId, 1); } }); } ask.on('blur',()=>ask.webContents.send('window-blur')); // Open DevTools in development if (!app.isPackaged) { ask.webContents.openDevTools({ mode: 'detach' }); } windowPool.set('ask', ask); break; } // settings case 'settings': { const settings = new BrowserWindow({ ...commonChildOptions, width:240, maxHeight:400, parent:undefined }); settings.setContentProtection(isContentProtectionOn); settings.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); if (process.platform === 'darwin') { settings.setWindowButtonVisibility(false); } const settingsLoadOptions = { query: { view: 'settings' } }; if (!shouldUseLiquidGlass) { settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions) .catch(console.error); } else { settingsLoadOptions.query.glass = 'true'; settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions) .catch(console.error); settings.webContents.once('did-finish-load', () => { const viewId = liquidGlass.addView(settings.getNativeWindowHandle()); if (viewId !== -1) { liquidGlass.unstable_setVariant(viewId, liquidGlass.GlassMaterialVariant.bubbles); // liquidGlass.unstable_setScrim(viewId, 1); // liquidGlass.unstable_setSubdued(viewId, 1); } }); } windowPool.set('settings', settings); break; } case 'shortcut-settings': { const shortcutEditor = new BrowserWindow({ ...commonChildOptions, width: 420, height: 720, modal: false, parent: undefined, alwaysOnTop: true, titleBarOverlay: false, }); if (process.platform === 'darwin') { shortcutEditor.setAlwaysOnTop(true, 'screen-saver'); } else { shortcutEditor.setAlwaysOnTop(true); } /* ──────────[ ① 다른 창 클릭 차단 ]────────── */ const disableClicks = () => { for (const [name, win] of windowPool) { if (win !== shortcutEditor && !win.isDestroyed()) { win.setIgnoreMouseEvents(true, { forward: true }); } } }; const restoreClicks = () => { for (const [, win] of windowPool) { if (!win.isDestroyed()) win.setIgnoreMouseEvents(false); } }; const header = windowPool.get('header'); if (header && !header.isDestroyed()) { const { x, y, width } = header.getBounds(); shortcutEditor.setBounds({ x, y, width }); } shortcutEditor.once('ready-to-show', () => { disableClicks(); shortcutEditor.show(); }); const loadOptions = { query: { view: 'shortcut-settings' } }; if (!shouldUseLiquidGlass) { shortcutEditor.loadFile(path.join(__dirname, '../app/content.html'), loadOptions); } else { loadOptions.query.glass = 'true'; shortcutEditor.loadFile(path.join(__dirname, '../app/content.html'), loadOptions); shortcutEditor.webContents.once('did-finish-load', () => { const viewId = liquidGlass.addView(shortcutEditor.getNativeWindowHandle()); if (viewId !== -1) { liquidGlass.unstable_setVariant(viewId, liquidGlass.GlassMaterialVariant.bubbles); } }); } shortcutEditor.on('closed', () => { restoreClicks(); windowPool.delete('shortcut-settings'); console.log('[Shortcuts] Re-enabled after editing.'); loadAndRegisterShortcuts(movementManager); }); shortcutEditor.webContents.once('dom-ready', async () => { const savedKeybinds = shortCutStore.get('customKeybinds', {}); const defaultKeybinds = getDefaultKeybinds(); const keybinds = { ...defaultKeybinds, ...savedKeybinds }; shortcutEditor.webContents.send('load-shortcuts', keybinds); }); if (!app.isPackaged) { shortcutEditor.webContents.openDevTools({ mode: 'detach' }); } windowPool.set('shortcut-settings', shortcutEditor); break; } } }; if (Array.isArray(namesToCreate)) { namesToCreate.forEach(name => createFeatureWindow(name)); } else if (typeof namesToCreate === 'string') { createFeatureWindow(namesToCreate); } else { createFeatureWindow('listen'); createFeatureWindow('ask'); createFeatureWindow('settings'); } } function destroyFeatureWindows() { const featureWindows = ['listen','ask','settings','shortcut-settings']; if (settingsHideTimer) { clearTimeout(settingsHideTimer); settingsHideTimer = null; } featureWindows.forEach(name=>{ const win = windowPool.get(name); if (win && !win.isDestroyed()) win.destroy(); windowPool.delete(name); }); } 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(); } function toggleAllWindowsVisibility() { const header = windowPool.get('header'); if (!header) return; if (header.isVisible()) { lastVisibleWindows.clear(); windowPool.forEach((win, name) => { if (win && !win.isDestroyed() && win.isVisible()) { lastVisibleWindows.add(name); } }); lastVisibleWindows.forEach(name => { if (name === 'header') return; const win = windowPool.get(name); if (win && !win.isDestroyed()) win.hide(); }); header.hide(); return; } lastVisibleWindows.forEach(name => { const win = windowPool.get(name); if (win && !win.isDestroyed()) win.show(); }); } function createWindows() { const primaryDisplay = screen.getPrimaryDisplay(); const { y: workAreaY, width: screenWidth } = primaryDisplay.workArea; 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, x: initialX, y: initialY, frame: false, transparent: true, vibrancy: false, alwaysOnTop: true, skipTaskbar: true, hiddenInMissionControl: true, resizable: false, focusable: true, acceptFirstMouse: true, webPreferences: { nodeIntegration: true, contextIsolation: false, backgroundThrottling: false, webSecurity: false, enableRemoteModule: false, // Ensure proper rendering and prevent pixelation experimentalFeatures: false, }, // Prevent pixelation and ensure proper rendering useContentSize: true, disableAutoHideCursor: true, }); if (process.platform === 'darwin') { header.setWindowButtonVisibility(false); } const headerLoadOptions = {}; if (!shouldUseLiquidGlass) { header.loadFile(path.join(__dirname, '../app/header.html'), headerLoadOptions); } else { headerLoadOptions.query = { glass: 'true' }; header.loadFile(path.join(__dirname, '../app/header.html'), headerLoadOptions); header.webContents.once('did-finish-load', () => { const viewId = liquidGlass.addView(header.getNativeWindowHandle()); if (viewId !== -1) { liquidGlass.unstable_setVariant(viewId, liquidGlass.GlassMaterialVariant.bubbles); // liquidGlass.unstable_setScrim(viewId, 1); // liquidGlass.unstable_setSubdued(viewId, 1); } }); } windowPool.set('header', header); header.on('moved', updateLayout); layoutManager = new WindowLayoutManager(windowPool); header.webContents.once('dom-ready', () => { loadAndRegisterShortcuts(movementManager); }); setupIpcHandlers(movementManager); if (currentHeaderState === 'main') { createFeatureWindows(header, ['listen', 'ask', 'settings', 'shortcut-settings']); } header.setContentProtection(isContentProtectionOn); header.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); // header.loadFile(path.join(__dirname, '../app/header.html')); // Open DevTools in development if (!app.isPackaged) { header.webContents.openDevTools({ mode: 'detach' }); } header.on('focus', () => { console.log('[WindowManager] Header gained focus'); }); header.on('blur', () => { console.log('[WindowManager] Header lost focus'); }); header.webContents.on('before-input-event', (event, input) => { if (input.type === 'mouseDown') { const target = input.target; if (target && (target.includes('input') || target.includes('apikey'))) { header.focus(); } } }); header.on('resize', () => { console.log('[WindowManager] Header resize event triggered'); updateLayout(); }); ipcMain.handle('toggle-all-windows-visibility', () => toggleAllWindowsVisibility()); ipcMain.handle('toggle-feature', async (event, featureName) => { return toggleFeature(featureName); }); ipcMain.handle('send-question-to-ask', (event, question) => { const askWindow = windowPool.get('ask'); if (askWindow && !askWindow.isDestroyed()) { console.log('📨 Main process: Sending question to AskView', question); askWindow.webContents.send('receive-question-from-assistant', question); return { success: true }; } else { console.error('❌ Cannot find AskView window'); return { success: false, error: 'AskView window not found' }; } }); ipcMain.handle('adjust-window-height', (event, targetHeight) => { const senderWindow = BrowserWindow.fromWebContents(event.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(); } }); ipcMain.on('session-did-close', () => { const listenWindow = windowPool.get('listen'); if (listenWindow && listenWindow.isVisible()) { console.log('[WindowManager] Session closed, hiding listen window.'); listenWindow.hide(); } }); return windowPool; } function loadAndRegisterShortcuts(movementManager) { if (windowPool.has('shortcut-settings')) { console.log('[Shortcuts] Editing in progress, skipping registration.'); return; } const defaultKeybinds = getDefaultKeybinds(); const savedKeybinds = shortCutStore.get('customKeybinds', {}); const keybinds = { ...defaultKeybinds, ...savedKeybinds }; const sendToRenderer = (channel, ...args) => { windowPool.forEach(win => { if (win && !win.isDestroyed()) { try { win.webContents.send(channel, ...args); } catch (e) { // 창이 이미 닫혔을 수 있으므로 오류를 무시합니다. } } }); }; updateGlobalShortcuts(keybinds, windowPool.get('header'), sendToRenderer, movementManager); } function setupIpcHandlers(movementManager) { screen.on('display-added', (event, newDisplay) => { console.log('[Display] New display added:', newDisplay.id); }); screen.on('display-removed', (event, oldDisplay) => { console.log('[Display] Display removed:', oldDisplay.id); const header = windowPool.get('header'); if (header && getCurrentDisplay(header).id === oldDisplay.id) { const primaryDisplay = screen.getPrimaryDisplay(); movementManager.moveToDisplay(primaryDisplay.id); } }); screen.on('display-metrics-changed', (event, display, changedMetrics) => { console.log('[Display] Display metrics changed:', display.id, changedMetrics); updateLayout(); }); // 1. 스트리밍 데이터 조각(chunk)을 받아서 ask 창으로 전달 ipcMain.on('ask-response-chunk', (event, { token }) => { const askWindow = windowPool.get('ask'); if (askWindow && !askWindow.isDestroyed()) { // renderer.js가 보낸 토큰을 AskView.js로 그대로 전달합니다. askWindow.webContents.send('ask-response-chunk', { token }); } }); // 2. 스트리밍 종료 신호를 받아서 ask 창으로 전달 ipcMain.on('ask-response-stream-end', () => { const askWindow = windowPool.get('ask'); if (askWindow && !askWindow.isDestroyed()) { askWindow.webContents.send('ask-response-stream-end'); } }); ipcMain.on('animation-finished', (event) => { const win = BrowserWindow.fromWebContents(event.sender); if (win && !win.isDestroyed()) { console.log(`[WindowManager] Hiding window after animation.`); win.hide(); } }); ipcMain.on('show-settings-window', (event, bounds) => { if (!bounds) return; const win = windowPool.get('settings'); if (win && !win.isDestroyed()) { if (settingsHideTimer) { clearTimeout(settingsHideTimer); settingsHideTimer = null; } // Adjust position based on button bounds const header = windowPool.get('header'); const headerBounds = header?.getBounds() ?? { x: 0, y: 0 }; const settingsBounds = win.getBounds(); const disp = getCurrentDisplay(header); const { x: waX, y: waY, width: waW, height: waH } = disp.workArea; let x = Math.round(headerBounds.x + (bounds?.x ?? 0) + (bounds?.width ?? 0) / 2 - settingsBounds.width / 2); let y = Math.round(headerBounds.y + (bounds?.y ?? 0) + (bounds?.height ?? 0) + 31); x = Math.max(waX + 10, Math.min(waX + waW - settingsBounds.width - 10, x)); y = Math.max(waY + 10, Math.min(waY + waH - settingsBounds.height - 10, y)); win.setBounds({ x, y }); win.__lockedByButton = true; console.log(`[WindowManager] Positioning settings window at (${x}, ${y}) based on button bounds.`); win.show(); win.moveTop(); win.setAlwaysOnTop(true); } }); ipcMain.on('hide-settings-window', (event) => { const window = windowPool.get("settings"); if (window && !window.isDestroyed()) { if (settingsHideTimer) { clearTimeout(settingsHideTimer); } settingsHideTimer = setTimeout(() => { if (window && !window.isDestroyed()) { window.setAlwaysOnTop(false); window.hide(); } settingsHideTimer = null; }, 200); window.__lockedByButton = false; } }); ipcMain.on('cancel-hide-settings-window', (event) => { if (settingsHideTimer) { clearTimeout(settingsHideTimer); settingsHideTimer = null; } }); ipcMain.handle('quit-application', () => { app.quit(); }); ipcMain.handle('is-ask-window-visible', (event, windowName) => { const window = windowPool.get(windowName); if (window && !window.isDestroyed()) { return window.isVisible(); } return false; }); ipcMain.handle('toggle-content-protection', () => { isContentProtectionOn = !isContentProtectionOn; console.log(`[Protection] Content protection toggled to: ${isContentProtectionOn}`); windowPool.forEach(win => { if (win && !win.isDestroyed()) { win.setContentProtection(isContentProtectionOn); } }); return isContentProtectionOn; }); ipcMain.handle('get-content-protection-status', () => { return isContentProtectionOn; }); ipcMain.on('header-state-changed', (event, state) => { console.log(`[WindowManager] Header state changed to: ${state}`); currentHeaderState = state; if (state === 'main') { createFeatureWindows(windowPool.get('header')); } else { // 'apikey' | 'permission' destroyFeatureWindows(); } loadAndRegisterShortcuts(movementManager); }); ipcMain.on('update-keybinds', (event, newKeybinds) => { updateGlobalShortcuts(newKeybinds); }); ipcMain.handle('get-current-shortcuts', () => { const defaultKeybinds = getDefaultKeybinds(); const savedKeybinds = shortCutStore.get('customKeybinds', {}); return { ...defaultKeybinds, ...savedKeybinds }; }); ipcMain.handle('open-shortcut-editor', () => { const header = windowPool.get('header'); if (!header) return; // 편집기 열기 전 모든 단축키 비활성화 globalShortcut.unregisterAll(); console.log('[Shortcuts] Disabled for editing.'); createFeatureWindows(header, 'shortcut-settings'); }); ipcMain.handle('get-default-shortcuts', () => { shortCutStore.set('customKeybinds', {}); return getDefaultKeybinds(); }); ipcMain.handle('save-shortcuts', async (event, newKeybinds) => { try { const defaultKeybinds = getDefaultKeybinds(); const customKeybinds = {}; for (const key in newKeybinds) { if (newKeybinds[key] && newKeybinds[key] !== defaultKeybinds[key]) { customKeybinds[key] = newKeybinds[key]; } } shortCutStore.set('customKeybinds', customKeybinds); console.log('[Shortcuts] Custom keybinds saved to store:', customKeybinds); const editor = windowPool.get('shortcut-settings'); if (editor && !editor.isDestroyed()) { editor.close(); } else { loadAndRegisterShortcuts(movementManager); } return { success: true }; } catch (error) { console.error("Failed to save shortcuts:", error); loadAndRegisterShortcuts(movementManager); return { success: false, error: error.message }; } }); ipcMain.on('close-shortcut-editor', () => { const editor = windowPool.get('shortcut-settings'); if (editor && !editor.isDestroyed()) { editor.close(); } }); ipcMain.handle('open-login-page', () => { const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000'; const personalizeUrl = `${webUrl}/personalize?desktop=true`; shell.openExternal(personalizeUrl); console.log('Opening personalization page:', personalizeUrl); }); setupApiKeyIPC(); ipcMain.handle('resize-header-window', (event, { width, height }) => { const header = windowPool.get('header'); if (header) { console.log(`[WindowManager] Resize request: ${width}x${height}`); // Prevent resizing during animations or if already at target size 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})`); // Skip if already at target size to prevent unnecessary operations 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); } // Calculate the center point of the current window const centerX = currentBounds.x + currentBounds.width / 2; // Calculate new X position to keep the window centered const newX = Math.round(centerX - width / 2); // Get the current display to ensure we stay within bounds const display = getCurrentDisplay(header); const { x: workAreaX, width: workAreaWidth } = display.workArea; // Clamp the new position to stay within display bounds 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); } // Update layout after resize updateLayout(); return { success: true }; } return { success: false, error: 'Header window not found' }; }); ipcMain.on('header-animation-finished', (event, state) => { const header = windowPool.get('header'); if (!header || header.isDestroyed()) return; if (state === 'hidden') { header.hide(); console.log('[WindowManager] Header hidden after animation.'); } else if (state === 'visible') { console.log('[WindowManager] Header shown after animation.'); updateLayout(); } }); ipcMain.handle('get-header-position', () => { const header = windowPool.get('header'); if (header) { const [x, y] = header.getPosition(); return { x, y }; } return { x: 0, y: 0 }; }); ipcMain.handle('move-header', (event, newX, newY) => { const header = windowPool.get('header'); if (header) { const currentY = newY !== undefined ? newY : header.getBounds().y; header.setPosition(newX, currentY, false); updateLayout(); } }); ipcMain.handle('move-header-to', (event, 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(); // Only clamp if the new position would actually go out of bounds // This prevents progressive restriction of movement let clampedX = newX; let clampedY = newY; // Check if we need to clamp X position if (newX < workAreaX) { clampedX = workAreaX; } else if (newX + headerBounds.width > workAreaX + width) { clampedX = workAreaX + width - headerBounds.width; } // Check if we need to clamp Y position if (newY < workAreaY) { clampedY = workAreaY; } else if (newY + headerBounds.height > workAreaY + height) { clampedY = workAreaY + height - headerBounds.height; } header.setPosition(clampedX, clampedY, false); updateLayout(); } }); ipcMain.handle('move-window-step', (event, direction) => { if (movementManager) { movementManager.moveStep(direction); } }); ipcMain.handle('force-close-window', (event, windowName) => { const window = windowPool.get(windowName); if (window && !window.isDestroyed()) { console.log(`[WindowManager] Force closing window: ${windowName}`); window.webContents.send('window-hide-animation'); } }); ipcMain.handle('start-screen-capture', async () => { try { isCapturing = true; console.log('Starting screen capture in main process'); return { success: true }; } catch (error) { console.error('Failed to start screen capture:', error); return { success: false, error: error.message }; } }); ipcMain.handle('stop-screen-capture', async () => { try { isCapturing = false; lastScreenshot = null; console.log('Stopped screen capture in main process'); return { success: true }; } catch (error) { console.error('Failed to stop screen capture:', error); return { success: false, error: error.message }; } }); ipcMain.handle('capture-screenshot', async (event, options = {}) => { return captureScreenshot(options); }); ipcMain.handle('get-current-screenshot', async event => { try { if (lastScreenshot && Date.now() - lastScreenshot.timestamp < 1000) { console.log('Returning cached screenshot'); return { success: true, base64: lastScreenshot.base64, width: lastScreenshot.width, height: lastScreenshot.height, }; } return { success: false, error: 'No screenshot available', }; } catch (error) { console.error('Failed to get current screenshot:', error); return { success: false, error: error.message, }; } }); ipcMain.handle('firebase-logout', async () => { console.log('[WindowManager] Received request to log out.'); await authService.signOut(); }); ipcMain.handle('check-system-permissions', async () => { const { systemPreferences } = require('electron'); const permissions = { microphone: 'unknown', screen: 'unknown', needsSetup: true }; try { if (process.platform === 'darwin') { // Check microphone permission on macOS const micStatus = systemPreferences.getMediaAccessStatus('microphone'); console.log('[Permissions] Microphone status:', micStatus); permissions.microphone = micStatus; // Check screen recording permission using the system API const screenStatus = systemPreferences.getMediaAccessStatus('screen'); console.log('[Permissions] Screen status:', screenStatus); permissions.screen = screenStatus; permissions.needsSetup = micStatus !== 'granted' || screenStatus !== 'granted'; } else { permissions.microphone = 'granted'; permissions.screen = 'granted'; permissions.needsSetup = false; } console.log('[Permissions] System permissions status:', permissions); return permissions; } catch (error) { console.error('[Permissions] Error checking permissions:', error); return { microphone: 'unknown', screen: 'unknown', needsSetup: true, error: error.message }; } }); ipcMain.handle('request-microphone-permission', async () => { if (process.platform !== 'darwin') { return { success: true }; } const { systemPreferences } = require('electron'); try { const status = systemPreferences.getMediaAccessStatus('microphone'); console.log('[Permissions] Microphone status:', status); if (status === 'granted') { return { success: true, status: 'granted' }; } // Req mic permission const granted = await systemPreferences.askForMediaAccess('microphone'); return { success: granted, status: granted ? 'granted' : 'denied' }; } catch (error) { console.error('[Permissions] Error requesting microphone permission:', error); return { success: false, error: error.message }; } }); ipcMain.handle('open-system-preferences', async (event, section) => { if (process.platform !== 'darwin') { return { success: false, error: 'Not supported on this platform' }; } try { if (section === 'screen-recording') { // First trigger screen capture request to register the app in system preferences try { console.log('[Permissions] Triggering screen capture request to register app...'); await desktopCapturer.getSources({ types: ['screen'], thumbnailSize: { width: 1, height: 1 } }); console.log('[Permissions] App registered for screen recording'); } catch (captureError) { console.log('[Permissions] Screen capture request triggered (expected to fail):', captureError.message); } // Then open system preferences // await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'); } // if (section === 'microphone') { // await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone'); // } return { success: true }; } catch (error) { console.error('[Permissions] Error opening system preferences:', error); return { success: false, error: error.message }; } }); ipcMain.handle('mark-permissions-completed', async () => { try { // This is a system-level setting, not user-specific. await systemSettingsRepository.markPermissionsAsCompleted(); console.log('[Permissions] Marked permissions as completed'); return { success: true }; } catch (error) { console.error('[Permissions] Error marking permissions as completed:', error); return { success: false, error: error.message }; } }); ipcMain.handle('check-permissions-completed', async () => { try { const completed = await systemSettingsRepository.checkPermissionsCompleted(); console.log('[Permissions] Permissions completed status:', completed); return completed; } catch (error) { console.error('[Permissions] Error checking permissions completed status:', error); return false; } }); ipcMain.handle('close-ask-window-if-empty', async () => { const askWindow = windowPool.get('ask'); if (askWindow && !askWindow.isFocused()) { askWindow.hide(); } }); } //////// after_modelStateService //////// async function getStoredApiKey() { if (global.modelStateService) { const provider = await getStoredProvider(); return global.modelStateService.getApiKey(provider); } return null; // Fallback } async function getStoredProvider() { if (global.modelStateService) { return global.modelStateService.getCurrentProvider('llm'); } return 'openai'; // Fallback } /** * * @param {IpcMainInvokeEvent} event * @param {{type: 'llm' | 'stt'}} */ async function getCurrentModelInfo(event, { type }) { if (global.modelStateService && (type === 'llm' || type === 'stt')) { return global.modelStateService.getCurrentModelInfo(type); } return null; } function setupApiKeyIPC() { const { ipcMain } = require('electron'); ipcMain.handle('get-stored-api-key', getStoredApiKey); ipcMain.handle('get-ai-provider', getStoredProvider); ipcMain.handle('get-current-model-info', getCurrentModelInfo); ipcMain.handle('api-key-validated', async (event, data) => { console.warn("[DEPRECATED] 'api-key-validated' IPC was called. This logic is now handled by 'model:validate-key'."); return { success: true }; }); ipcMain.handle('remove-api-key', async () => { console.warn("[DEPRECATED] 'remove-api-key' IPC was called. This is now handled by 'model:remove-api-key'."); return { success: true }; }); console.log('[WindowManager] API key related IPC handlers have been updated for ModelStateService.'); } //////// after_modelStateService //////// function getDefaultKeybinds() { const isMac = process.platform === 'darwin'; return { moveUp: isMac ? 'Cmd+Up' : 'Ctrl+Up', moveDown: isMac ? 'Cmd+Down' : 'Ctrl+Down', moveLeft: isMac ? 'Cmd+Left' : 'Ctrl+Left', moveRight: isMac ? 'Cmd+Right' : 'Ctrl+Right', toggleVisibility: isMac ? 'Cmd+\\' : 'Ctrl+\\', toggleClickThrough: isMac ? 'Cmd+M' : 'Ctrl+M', nextStep: isMac ? 'Cmd+Enter' : 'Ctrl+Enter', manualScreenshot: isMac ? 'Cmd+Shift+S' : 'Ctrl+Shift+S', previousResponse: isMac ? 'Cmd+[' : 'Ctrl+[', nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]', scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up', scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down', }; } function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementManager) { globalShortcut.unregisterAll(); if (sendToRenderer) { sendToRenderer('shortcuts-updated', keybinds); console.log('[Shortcuts] Broadcasted updated shortcuts to all windows.'); } // ✨ 하드코딩된 단축키 등록을 위해 변수 유지 const isMac = process.platform === 'darwin'; const modifier = isMac ? 'Cmd' : 'Ctrl'; const header = windowPool.get('header'); const state = header?.currentHeaderState || currentHeaderState; // ✨ 기능 1: 사용자가 설정할 수 없는 '모니터 이동' 단축키 (기존 로직 유지) const displays = screen.getAllDisplays(); if (displays.length > 1) { displays.forEach((display, index) => { const key = `${modifier}+Shift+${index + 1}`; try { globalShortcut.register(key, () => movementManager.moveToDisplay(display.id)); console.log(`Registered display switch shortcut: ${key} -> Display ${index + 1}`); } catch (error) { console.error(`Failed to register display switch ${key}:`, error); } }); } // API 키 입력 상태에서는 필수 단축키(toggleVisibility) 외에는 아무것도 등록하지 않음 if (state === 'apikey') { if (keybinds.toggleVisibility) { try { globalShortcut.register(keybinds.toggleVisibility, () => toggleAllWindowsVisibility()); } catch (error) { console.error(`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`, error); } } console.log('ApiKeyHeader is active, skipping conditional shortcuts'); return; } // ✨ 기능 2: 사용자가 설정할 수 없는 '화면 가장자리 이동' 단축키 (기존 로직 유지) const edgeDirections = [ { key: `${modifier}+Shift+Left`, direction: 'left' }, { key: `${modifier}+Shift+Right`, direction: 'right' }, // { key: `${modifier}+Shift+Up`, direction: 'up' }, // { key: `${modifier}+Shift+Down`, direction: 'down' }, ]; edgeDirections.forEach(({ key, direction }) => { try { globalShortcut.register(key, () => { if (header && header.isVisible()) movementManager.moveToEdge(direction); }); } catch (error) { console.error(`Failed to register edge move for ${key}:`, error); } }); // ✨ 기능 3: 사용자가 설정 가능한 모든 단축키를 동적으로 등록 (새로운 방식 적용) for (const action in keybinds) { const accelerator = keybinds[action]; if (!accelerator) continue; try { let callback; switch(action) { case 'toggleVisibility': callback = () => toggleAllWindowsVisibility(); break; case 'nextStep': callback = () => toggleFeature('ask'); break; case 'scrollUp': callback = () => { // 'ask' 창을 명시적으로 가져옵니다. const askWindow = windowPool.get('ask'); // 'ask' 창이 존재하고, 파괴되지 않았으며, 보이는 경우에만 이벤트를 전송합니다. if (askWindow && !askWindow.isDestroyed() && askWindow.isVisible()) { askWindow.webContents.send('scroll-response-up'); } }; break; case 'scrollDown': callback = () => { // 'ask' 창을 명시적으로 가져옵니다. const askWindow = windowPool.get('ask'); // 'ask' 창이 존재하고, 파괴되지 않았으며, 보이는 경우에만 이벤트를 전송합니다. if (askWindow && !askWindow.isDestroyed() && askWindow.isVisible()) { askWindow.webContents.send('scroll-response-down'); } }; break; case 'moveUp': callback = () => { if (header && header.isVisible()) movementManager.moveStep('up'); }; break; case 'moveDown': callback = () => { if (header && header.isVisible()) movementManager.moveStep('down'); }; break; case 'moveLeft': callback = () => { if (header && header.isVisible()) movementManager.moveStep('left'); }; break; case 'moveRight': callback = () => { if (header && header.isVisible()) movementManager.moveStep('right'); }; break; case 'toggleClickThrough': callback = () => { mouseEventsIgnored = !mouseEventsIgnored; if(mainWindow && !mainWindow.isDestroyed()){ mainWindow.setIgnoreMouseEvents(mouseEventsIgnored, { forward: true }); mainWindow.webContents.send('click-through-toggled', mouseEventsIgnored); } }; break; case 'manualScreenshot': callback = () => { if(mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.executeJavaScript('window.captureManualScreenshot && window.captureManualScreenshot();'); } }; break; case 'previousResponse': callback = () => sendToRenderer('navigate-previous-response'); break; case 'nextResponse': callback = () => sendToRenderer('navigate-next-response'); break; } if (callback) { globalShortcut.register(accelerator, callback); } } catch(e) { console.error(`Failed to register shortcut for "${action}" (${accelerator}):`, e.message); } } } async function captureScreenshot(options = {}) { if (process.platform === 'darwin') { try { const tempPath = path.join(os.tmpdir(), `screenshot-${Date.now()}.jpg`); await execFile('screencapture', ['-x', '-t', 'jpg', tempPath]); const imageBuffer = await fs.promises.readFile(tempPath); await fs.promises.unlink(tempPath); if (sharp) { try { // Try using sharp for optimal image processing const resizedBuffer = await sharp(imageBuffer) // .resize({ height: 1080 }) .resize({ height: 384 }) .jpeg({ quality: 80 }) .toBuffer(); const base64 = resizedBuffer.toString('base64'); const metadata = await sharp(resizedBuffer).metadata(); lastScreenshot = { base64, width: metadata.width, height: metadata.height, timestamp: Date.now(), }; return { success: true, base64, width: metadata.width, height: metadata.height }; } catch (sharpError) { console.warn('Sharp module failed, falling back to basic image processing:', sharpError.message); } } // Fallback: Return the original image without resizing console.log('[WindowManager] Using fallback image processing (no resize/compression)'); const base64 = imageBuffer.toString('base64'); lastScreenshot = { base64, width: null, // We don't have metadata without sharp height: null, timestamp: Date.now(), }; return { success: true, base64, width: null, height: null }; } catch (error) { console.error('Failed to capture screenshot:', error); return { success: false, error: error.message }; } } try { const sources = await desktopCapturer.getSources({ types: ['screen'], thumbnailSize: { width: 1920, height: 1080, }, }); if (sources.length === 0) { throw new Error('No screen sources available'); } const source = sources[0]; const buffer = source.thumbnail.toJPEG(70); const base64 = buffer.toString('base64'); const size = source.thumbnail.getSize(); return { success: true, base64, width: size.width, height: size.height, }; } catch (error) { console.error('Failed to capture screenshot using desktopCapturer:', error); return { success: false, error: error.message, }; } } module.exports = { createWindows, windowPool, fixedYPosition, getStoredApiKey, getStoredProvider, getCurrentModelInfo, captureScreenshot, };