diff --git a/src/electron/windowManager.js b/src/electron/windowManager.js deleted file mode 100644 index 4b540f6..0000000 --- a/src/electron/windowManager.js +++ /dev/null @@ -1,1502 +0,0 @@ -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); -const sharp = require('sharp'); -const authService = require('../common/services/authService'); -const systemSettingsRepository = require('../common/repositories/systemSettings'); -const userRepository = require('../common/repositories/user'); -const fetch = require('node-fetch'); - - -/* ────────────────[ 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 layoutManager = null; -function updateLayout() { - if (layoutManager) { - layoutManager.updateLayout(); - } -} - -let movementManager = null; - -let storedProvider = 'openai'; - -const featureWindows = ['listen','ask','settings']; -function isAllowed(name) { - if (name === 'header') return true; - return featureWindows.includes(name) && currentHeaderState === 'main'; -} - -function createFeatureWindows(header) { - if (windowPool.has('listen')) return; - - const commonChildOptions = { - parent: header, - show: false, - frame: false, - transparent: true, - vibrancy: false, - hasShadow: false, - skipTaskbar: true, - hiddenInMissionControl: true, - resizable: false, - webPreferences: { nodeIntegration: true, contextIsolation: false }, - }; - - // listen - const listen = new BrowserWindow({ - ...commonChildOptions, width:400,minWidth:400,maxWidth:400, - maxHeight:700, - }); - 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(), { - cornerRadius: 12, - tintColor: '#FF00001A', // Red tint - opaque: false, - }); - if (viewId !== -1) { - liquidGlass.unstable_setVariant(viewId, 2); - // liquidGlass.unstable_setScrim(viewId, 1); - // liquidGlass.unstable_setSubdued(viewId, 1); - } - }); - } - - - windowPool.set('listen', listen); - - // 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(), { - cornerRadius: 12, - tintColor: '#FF00001A', // Red tint - opaque: false, - }); - if (viewId !== -1) { - liquidGlass.unstable_setVariant(viewId, 2); - // 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); - - // 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(), { - cornerRadius: 12, - tintColor: '#FF00001A', // Red tint - opaque: false, - }); - if (viewId !== -1) { - liquidGlass.unstable_setVariant(viewId, 2); - // liquidGlass.unstable_setScrim(viewId, 1); - // liquidGlass.unstable_setSubdued(viewId, 1); - } - }); - } - windowPool.set('settings', settings); -} - -function destroyFeatureWindows() { - 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(movementManager) { - const header = windowPool.get('header'); - if (!header) return; - - if (header.isVisible()) { - console.log('[Visibility] Smart hiding - calculating nearest edge'); - - const headerBounds = header.getBounds(); - const display = screen.getPrimaryDisplay(); - const { width: screenWidth, height: screenHeight } = display.workAreaSize; - - const centerX = headerBounds.x + headerBounds.width / 2; - const centerY = headerBounds.y + headerBounds.height / 2; - - const distances = { - top: centerY, - bottom: screenHeight - centerY, - left: centerX, - right: screenWidth - centerX, - }; - - const nearestEdge = Object.keys(distances).reduce((nearest, edge) => (distances[edge] < distances[nearest] ? edge : nearest)); - - console.log(`[Visibility] Nearest edge: ${nearestEdge} (distance: ${distances[nearestEdge].toFixed(1)}px)`); - - lastVisibleWindows.clear(); - lastVisibleWindows.add('header'); - - windowPool.forEach((win, name) => { - if (win.isVisible()) { - lastVisibleWindows.add(name); - if (name !== 'header') { - // win.webContents.send('window-hide-animation'); - // setTimeout(() => { - // if (!win.isDestroyed()) { - // win.hide(); - // } - // }, 200); - win.hide(); - } - } - }); - - console.log('[Visibility] Visible windows before hide:', Array.from(lastVisibleWindows)); - - movementManager.hideToEdge(nearestEdge, () => { - header.hide(); - console.log('[Visibility] Smart hide completed'); - }, { instant: true }); - } else { - console.log('[Visibility] Smart showing from hidden position'); - console.log('[Visibility] Restoring windows:', Array.from(lastVisibleWindows)); - - header.show(); - - movementManager.showFromEdge(() => { - lastVisibleWindows.forEach(name => { - if (name === 'header') return; - const win = windowPool.get(name); - if (win && !win.isDestroyed()) { - win.show(); - win.webContents.send('window-show-animation'); - } - }); - - setImmediate(updateLayout); - setTimeout(updateLayout, 120); - - console.log('[Visibility] Smart show completed'); - }); - } -} - - -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, - }, - }); - 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(), { - cornerRadius: 12, - tintColor: '#FF00001A', // Red tint - opaque: false, - }); - if (viewId !== -1) { - liquidGlass.unstable_setVariant(viewId, 2); - // liquidGlass.unstable_setScrim(viewId, 1); - // liquidGlass.unstable_setSubdued(viewId, 1); - } - }); - } - windowPool.set('header', header); - layoutManager = new WindowLayoutManager(windowPool); - - header.webContents.once('dom-ready', () => { - loadAndRegisterShortcuts(movementManager); - }); - - setupIpcHandlers(movementManager); - - if (currentHeaderState === 'main') { - createFeatureWindows(header); - } - - 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', updateLayout); - - // header.webContents.once('dom-ready', () => { - // loadAndRegisterShortcuts(); - // }); - - ipcMain.handle('toggle-all-windows-visibility', () => toggleAllWindowsVisibility(movementManager)); - - ipcMain.handle('toggle-feature', async (event, featureName) => { - if (!windowPool.get(featureName) && currentHeaderState === 'main') { - createFeatureWindows(windowPool.get('header')); - } - - const windowToToggle = windowPool.get(featureName); - - if (windowToToggle) { - if (featureName === 'listen') { - const listenService = global.listenService; - if (listenService && listenService.isSessionActive()) { - console.log('[WindowManager] Listen session is active, closing it via toggle.'); - await listenService.closeSession(); - return; - } - } - console.log(`[WindowManager] Toggling feature: ${featureName}`); - } - - 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'); - - setTimeout(() => { - if (!askWindow.isDestroyed()) { - askWindow.hide(); - updateLayout(); - } - }, 250); - } - } 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'); - } - } else { - const windowToToggle = windowPool.get(featureName); - - if (windowToToggle) { - if (windowToToggle.isDestroyed()) { - console.error(`Window ${featureName} is destroyed, cannot toggle`); - return; - } - - if (windowToToggle.isVisible()) { - if (featureName === 'settings') { - windowToToggle.webContents.send('settings-window-hide-animation'); - } else { - windowToToggle.webContents.send('window-hide-animation'); - } - - setTimeout(() => { - if (!windowToToggle.isDestroyed()) { - windowToToggle.hide(); - updateLayout(); - } - }, 250); - } else { - try { - windowToToggle.show(); - updateLayout(); - - if (featureName === 'listen') { - windowToToggle.webContents.send('start-listening-session'); - } - - windowToToggle.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())); - } - } - }); - - 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(); - } - }); - - // setupIpcHandlers(); - - return windowPool; -} - -function loadAndRegisterShortcuts(movementManager) { - const defaultKeybinds = getDefaultKeybinds(); - const header = windowPool.get('header'); - const sendToRenderer = (channel, ...args) => { - windowPool.forEach(win => { - try { - if (win && !win.isDestroyed()) { - win.webContents.send(channel, ...args); - } - } catch (e) {} - }); - }; - - - if (!header) { - return updateGlobalShortcuts(defaultKeybinds, undefined, sendToRenderer, movementManager); - } - - header.webContents - .executeJavaScript(`(() => localStorage.getItem('customKeybinds'))()`) - .then(saved => (saved ? JSON.parse(saved) : {})) - .then(savedKeybinds => { - const keybinds = { ...defaultKeybinds, ...savedKeybinds }; - updateGlobalShortcuts(keybinds, header, sendToRenderer, movementManager); - }) - .catch(() => updateGlobalShortcuts(defaultKeybinds, 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('show-window', (event, args) => { - const { name, bounds } = typeof args === 'object' && args !== null ? args : { name: args, bounds: null }; - const win = windowPool.get(name); - - if (win && !win.isDestroyed()) { - if (settingsHideTimer) { - clearTimeout(settingsHideTimer); - settingsHideTimer = null; - } - - if (name === 'settings') { - // 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(); - - if (name === 'settings') { - win.setAlwaysOnTop(true); - } - // updateLayout(); - } - }); - - ipcMain.on('hide-window', (event, name) => { - const window = windowPool.get(name); - if (window && !window.isDestroyed()) { - if (name === 'settings') { - if (settingsHideTimer) { - clearTimeout(settingsHideTimer); - } - settingsHideTimer = setTimeout(() => { - // window.setAlwaysOnTop(false); - // window.hide(); - if (window && !window.isDestroyed()) { - window.setAlwaysOnTop(false); - window.hide(); - } - settingsHideTimer = null; - }, 200); - } else { - window.hide(); - } - window.__lockedByButton = false; - } - }); - - ipcMain.on('cancel-hide-window', (event, name) => { - if (name === 'settings' && settingsHideTimer) { - clearTimeout(settingsHideTimer); - settingsHideTimer = null; - } - }); - - ipcMain.handle('hide-all', () => { - windowPool.forEach(win => { - if (win.isFocused()) return; - win.hide(); - }); - }); - - ipcMain.handle('quit-application', () => { - app.quit(); - }); - - ipcMain.handle('is-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(); - } - - for (const [name, win] of windowPool) { - if (!isAllowed(name) && !win.isDestroyed()) { - win.hide(); - } - if (isAllowed(name) && win.isVisible()) { - win.show(); - } - } - - const header = windowPool.get('header'); - if (header && !header.isDestroyed()) { - header.webContents - .executeJavaScript(`(() => localStorage.getItem('customKeybinds'))()`) - .then(saved => { - const defaultKeybinds = getDefaultKeybinds(); - const savedKeybinds = saved ? JSON.parse(saved) : {}; - const keybinds = { ...defaultKeybinds, ...savedKeybinds }; - - const sendToRenderer = (channel, ...args) => { - windowPool.forEach(win => { - try { - if (win && !win.isDestroyed()) { - win.webContents.send(channel, ...args); - } - } catch (e) {} - }); - }; - - updateGlobalShortcuts(keybinds, header, sendToRenderer, movementManager); - }) - .catch(console.error); - } - }); - - ipcMain.on('update-keybinds', (event, newKeybinds) => { - updateGlobalShortcuts(newKeybinds); - }); - - 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-window', () => {}); - - ipcMain.handle('resize-for-view', () => {}); - - ipcMain.handle('resize-header-window', (event, { width, height }) => { - const header = windowPool.get('header'); - if (header) { - const wasResizable = header.isResizable(); - if (!wasResizable) { - header.setResizable(true); - } - - const bounds = header.getBounds(); - const newX = bounds.x + Math.round((bounds.width - width) / 2); - - header.setBounds({ x: newX, y: bounds.y, width, height }); - - if (!wasResizable) { - header.setResizable(false); - } - return { success: true }; - } - return { success: false, error: 'Header window not found' }; - }); - - ipcMain.on('header-animation-complete', (event, state) => { - const header = windowPool.get('header'); - if (!header) return; - - if (state === 'hidden') { - header.hide(); - } else if (state === 'visible') { - lastVisibleWindows.forEach(name => { - if (name === 'header') return; - const win = windowPool.get(name); - if (win) win.show(); - }); - - setImmediate(updateLayout); - setTimeout(updateLayout, 120); - } - }); - - 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(); - - const clampedX = Math.max(workAreaX, Math.min(workAreaX + width - headerBounds.width, newX)); - const clampedY = Math.max(workAreaY, Math.min(workAreaY + height - headerBounds.height, newY)); - - 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'); - - setTimeout(() => { - if (!window.isDestroyed()) { - window.hide(); - updateLayout(); - } - }, 250); - } - }); - - 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(); - await setApiKey(null); - - windowPool.forEach(win => { - if (win && !win.isDestroyed()) { - win.webContents.send('api-key-removed'); - } - }); - }); - - 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(); - } - }); -} - - -async function setApiKey(apiKey, provider = 'openai') { - console.log('[WindowManager] Persisting API key and provider to DB'); - - try { - await userRepository.saveApiKey(apiKey, authService.getCurrentUserId(), provider); - console.log('[WindowManager] API key and provider saved to SQLite'); - - // Notify authService that the key status may have changed - await authService.updateApiKeyStatus(); - - } catch (err) { - console.error('[WindowManager] Failed to save API key to SQLite:', err); - } - - windowPool.forEach(win => { - if (win && !win.isDestroyed()) { - const js = apiKey ? ` - localStorage.setItem('openai_api_key', ${JSON.stringify(apiKey)}); - localStorage.setItem('ai_provider', ${JSON.stringify(provider)}); - ` : ` - localStorage.removeItem('openai_api_key'); - localStorage.removeItem('ai_provider'); - `; - win.webContents.executeJavaScript(js).catch(() => {}); - } - }); -} - -async function getStoredApiKey() { - const userId = authService.getCurrentUserId(); - if (!userId) return null; - const user = await userRepository.getById(userId); - return user?.api_key || null; -} - -async function getStoredProvider() { - const userId = authService.getCurrentUserId(); - if (!userId) return 'openai'; - const user = await userRepository.getById(userId); - return user?.provider || 'openai'; -} - -function setupApiKeyIPC() { - const { ipcMain } = require('electron'); - - // Both handlers now do the same thing: fetch the key from the source of truth. - ipcMain.handle('get-stored-api-key', getStoredApiKey); - - ipcMain.handle('api-key-validated', async (event, data) => { - console.log('[WindowManager] API key validation completed, saving...'); - - // Support both old format (string) and new format (object) - const apiKey = typeof data === 'string' ? data : data.apiKey; - const provider = typeof data === 'string' ? 'openai' : (data.provider || 'openai'); - - await setApiKey(apiKey, provider); - - windowPool.forEach((win, name) => { - if (win && !win.isDestroyed()) { - win.webContents.send('api-key-validated', { apiKey, provider }); - } - }); - - return { success: true }; - }); - - ipcMain.handle('remove-api-key', async () => { - console.log('[WindowManager] API key removal requested'); - await setApiKey(null); - - windowPool.forEach((win, name) => { - if (win && !win.isDestroyed()) { - win.webContents.send('api-key-removed'); - } - }); - - const settingsWindow = windowPool.get('settings'); - if (settingsWindow && settingsWindow.isVisible()) { - settingsWindow.hide(); - console.log('[WindowManager] Settings window hidden after clearing API key.'); - } - - return { success: true }; - }); - - ipcMain.handle('get-ai-provider', getStoredProvider); - - console.log('[WindowManager] API key related IPC handlers registered (SQLite-backed)'); -} - - -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) { - console.log('Updating global shortcuts with:', keybinds); - - // Unregister all existing shortcuts - globalShortcut.unregisterAll(); - - let toggleVisibilityDebounceTimer = null; - - const isMac = process.platform === 'darwin'; - const modifier = isMac ? 'Cmd' : 'Ctrl'; - - if (keybinds.toggleVisibility) { - try { - globalShortcut.register(keybinds.toggleVisibility, () => toggleAllWindowsVisibility(movementManager)); - console.log(`Registered toggleVisibility: ${keybinds.toggleVisibility}`); - } catch (error) { - console.error(`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`, error); - } - } - - 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); - } - }); - } - - if (currentHeaderState === 'apikey') { - console.log('ApiKeyHeader is active, skipping conditional shortcuts'); - return; - } - - const directions = [ - { key: `${modifier}+Left`, direction: 'left' }, - { key: `${modifier}+Right`, direction: 'right' }, - { key: `${modifier}+Up`, direction: 'up' }, - { key: `${modifier}+Down`, direction: 'down' }, - ]; - - directions.forEach(({ key, direction }) => { - try { - globalShortcut.register(key, () => { - const header = windowPool.get('header'); - if (header && header.isVisible()) { - movementManager.moveStep(direction); - } - }); - console.log(`Registered global shortcut: ${key} -> ${direction}`); - } catch (error) { - console.error(`Failed to register ${key}:`, error); - } - }); - - 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, () => { - const header = windowPool.get('header'); - if (header && header.isVisible()) { - movementManager.moveToEdge(direction); - } - }); - console.log(`Registered global shortcut: ${key} -> edge ${direction}`); - } catch (error) { - console.error(`Failed to register ${key}:`, error); - } - }); - - if (keybinds.toggleClickThrough) { - try { - globalShortcut.register(keybinds.toggleClickThrough, () => { - mouseEventsIgnored = !mouseEventsIgnored; - if (mouseEventsIgnored) { - mainWindow.setIgnoreMouseEvents(true, { forward: true }); - console.log('Mouse events ignored'); - } else { - mainWindow.setIgnoreMouseEvents(false); - console.log('Mouse events enabled'); - } - mainWindow.webContents.send('click-through-toggled', mouseEventsIgnored); - }); - console.log(`Registered toggleClickThrough: ${keybinds.toggleClickThrough}`); - } catch (error) { - console.error(`Failed to register toggleClickThrough (${keybinds.toggleClickThrough}):`, error); - } - } - - if (keybinds.nextStep) { - try { - globalShortcut.register(keybinds.nextStep, () => { - console.log('⌘/Ctrl+Enter Ask shortcut triggered'); - - const askWindow = windowPool.get('ask'); - if (!askWindow || askWindow.isDestroyed()) { - console.error('Ask window not found or destroyed'); - return; - } - - if (askWindow.isVisible()) { - askWindow.webContents.send('ask-global-send'); - } else { - try { - askWindow.show(); - - const header = windowPool.get('header'); - if (header) { - const currentHeaderPosition = header.getBounds(); - updateLayout(); - header.setPosition(currentHeaderPosition.x, currentHeaderPosition.y, false); - } - - askWindow.webContents.send('window-show-animation'); - } catch (e) { - console.error('Error showing Ask window:', e); - } - } - }); - console.log(`Registered Ask shortcut (nextStep): ${keybinds.nextStep}`); - } catch (error) { - console.error(`Failed to register Ask shortcut (${keybinds.nextStep}):`, error); - } - } - - if (keybinds.manualScreenshot) { - try { - globalShortcut.register(keybinds.manualScreenshot, () => { - console.log('Manual screenshot shortcut triggered'); - mainWindow.webContents.executeJavaScript(` - if (window.captureManualScreenshot) { - window.captureManualScreenshot(); - } else { - console.log('Manual screenshot function not available'); - } - `); - }); - console.log(`Registered manualScreenshot: ${keybinds.manualScreenshot}`); - } catch (error) { - console.error(`Failed to register manualScreenshot (${keybinds.manualScreenshot}):`, error); - } - } - - if (keybinds.previousResponse) { - try { - globalShortcut.register(keybinds.previousResponse, () => { - console.log('Previous response shortcut triggered'); - sendToRenderer('navigate-previous-response'); - }); - console.log(`Registered previousResponse: ${keybinds.previousResponse}`); - } catch (error) { - console.error(`Failed to register previousResponse (${keybinds.previousResponse}):`, error); - } - } - - if (keybinds.nextResponse) { - try { - globalShortcut.register(keybinds.nextResponse, () => { - console.log('Next response shortcut triggered'); - sendToRenderer('navigate-next-response'); - }); - console.log(`Registered nextResponse: ${keybinds.nextResponse}`); - } catch (error) { - console.error(`Failed to register nextResponse (${keybinds.nextResponse}):`, error); - } - } - - if (keybinds.scrollUp) { - try { - globalShortcut.register(keybinds.scrollUp, () => { - console.log('Scroll up shortcut triggered'); - sendToRenderer('scroll-response-up'); - }); - console.log(`Registered scrollUp: ${keybinds.scrollUp}`); - } catch (error) { - console.error(`Failed to register scrollUp (${keybinds.scrollUp}):`, error); - } - } - - if (keybinds.scrollDown) { - try { - globalShortcut.register(keybinds.scrollDown, () => { - console.log('Scroll down shortcut triggered'); - sendToRenderer('scroll-response-down'); - }); - console.log(`Registered scrollDown: ${keybinds.scrollDown}`); - } catch (error) { - console.error(`Failed to register scrollDown (${keybinds.scrollDown}):`, error); - } - } -} - - -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); - - 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 (error) { - console.error('Failed to capture and resize 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, - setApiKey, - getStoredApiKey, - getStoredProvider, - captureScreenshot, -}; \ No newline at end of file