const { BrowserWindow, globalShortcut, screen, app, shell } = require('electron'); const WindowLayoutManager = require('./windowLayoutManager'); const SmoothMovementManager = require('./smoothMovementManager'); const path = require('node:path'); const os = require('os'); const shortcutsService = require('../features/shortcuts/shortcutsService'); const internalBridge = require('../bridge/internalBridge'); const permissionRepository = require('../features/common/repositories/permission'); /* ────────────────[ 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 lastVisibleWindows = new Set(['header']); let currentHeaderState = 'apikey'; const windowPool = new Map(); let settingsHideTimer = null; let layoutManager = null; let movementManager = null; function updateChildWindowLayouts(animated = true) { if (movementManager.isAnimating) return; const visibleWindows = {}; const listenWin = windowPool.get('listen'); const askWin = windowPool.get('ask'); if (listenWin && !listenWin.isDestroyed() && listenWin.isVisible()) { visibleWindows.listen = true; } if (askWin && !askWin.isDestroyed() && askWin.isVisible()) { visibleWindows.ask = true; } if (Object.keys(visibleWindows).length === 0) return; const newLayout = layoutManager.calculateFeatureWindowLayout(visibleWindows); movementManager.animateLayout(newLayout, animated); } const showSettingsWindow = () => { internalBridge.emit('window:requestVisibility', { name: 'settings', visible: true }); }; const hideSettingsWindow = () => { internalBridge.emit('window:requestVisibility', { name: 'settings', visible: false }); }; 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 = () => { return new Promise((resolve) => { internalBridge.emit('window:getHeaderPosition', (position) => { resolve(position); }); }); }; 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 }) => { handleWindowVisibilityRequest(windowPool, layoutManager, movementManager, name, visible); }); internalBridge.on('window:requestToggleAllWindowsVisibility', ({ targetVisibility }) => { changeAllWindowsVisibility(windowPool, targetVisibility); }); internalBridge.on('window:moveToDisplay', ({ displayId }) => { // movementManager.moveToDisplay(displayId); const header = windowPool.get('header'); if (header) { const newPosition = layoutManager.calculateNewPositionForDisplay(header, displayId); if (newPosition) { movementManager.animateWindowPosition(header, newPosition, { onComplete: () => updateChildWindowLayouts(true) }); } } }); internalBridge.on('window:moveToEdge', ({ direction }) => { const header = windowPool.get('header'); if (header) { const newPosition = layoutManager.calculateEdgePosition(header, direction); movementManager.animateWindowPosition(header, newPosition, { onComplete: () => updateChildWindowLayouts(true) }); } }); internalBridge.on('window:moveStep', ({ direction }) => { const header = windowPool.get('header'); if (header) { const newHeaderPosition = layoutManager.calculateStepMovePosition(header, direction); if (!newHeaderPosition) return; const futureHeaderBounds = { ...header.getBounds(), ...newHeaderPosition }; const visibleWindows = {}; const listenWin = windowPool.get('listen'); const askWin = windowPool.get('ask'); if (listenWin && !listenWin.isDestroyed() && listenWin.isVisible()) { visibleWindows.listen = true; } if (askWin && !askWin.isDestroyed() && askWin.isVisible()) { visibleWindows.ask = true; } const newChildLayout = layoutManager.calculateFeatureWindowLayout(visibleWindows, futureHeaderBounds); movementManager.animateWindowPosition(header, newHeaderPosition); movementManager.animateLayout(newChildLayout); } }); internalBridge.on('window:resizeHeaderWindow', ({ width, height }) => { const header = windowPool.get('header'); if (!header || movementManager.isAnimating) return; const newHeaderBounds = layoutManager.calculateHeaderResize(header, { width, height }); const wasResizable = header.isResizable(); if (!wasResizable) header.setResizable(true); movementManager.animateWindowBounds(header, newHeaderBounds, { onComplete: () => { if (!wasResizable) header.setResizable(false); updateChildWindowLayouts(true); } }); }); internalBridge.on('window:headerAnimationFinished', (state) => { const header = windowPool.get('header'); if (!header || header.isDestroyed()) return; if (state === 'hidden') { header.hide(); } else if (state === 'visible') { updateChildWindowLayouts(false); } }); internalBridge.on('window:getHeaderPosition', (reply) => { const header = windowPool.get('header'); if (header && !header.isDestroyed()) { reply(header.getBounds()); } else { reply({ x: 0, y: 0, width: 0, height: 0 }); } }); internalBridge.on('window:moveHeaderTo', ({ newX, newY }) => { const header = windowPool.get('header'); if (header) { const newPosition = layoutManager.calculateClampedPosition(header, { x: newX, y: newY }); header.setPosition(newPosition.x, newPosition.y); } }); internalBridge.on('window:adjustWindowHeight', ({ sender, targetHeight }) => { const senderWindow = windowPool.get(sender); if (senderWindow) { const newBounds = layoutManager.calculateWindowHeightAdjustment(senderWindow, targetHeight); const wasResizable = senderWindow.isResizable(); if (!wasResizable) senderWindow.setResizable(true); movementManager.animateWindowBounds(senderWindow, newBounds, { onComplete: () => { if (!wasResizable) senderWindow.setResizable(false); updateChildWindowLayouts(true); } }); } }); } function changeAllWindowsVisibility(windowPool, targetVisibility) { const header = windowPool.get('header'); if (!header) return; if (typeof targetVisibility === 'boolean' && header.isVisible() === targetVisibility) { 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(); }); } /** * * @param {Map} windowPool * @param {WindowLayoutManager} layoutManager * @param {SmoothMovementManager} movementManager * @param {'listen' | 'ask' | 'settings' | 'shortcut-settings'} name * @param {boolean} shouldBeVisible */ async function handleWindowVisibilityRequest(windowPool, layoutManager, movementManager, name, shouldBeVisible) { console.log(`[WindowManager] Request: set '${name}' visibility to ${shouldBeVisible}`); const win = windowPool.get(name); if (!win || win.isDestroyed()) { console.warn(`[WindowManager] Window '${name}' not found or destroyed.`); return; } if (name !== 'settings') { const isCurrentlyVisible = win.isVisible(); if (isCurrentlyVisible === shouldBeVisible) { console.log(`[WindowManager] Window '${name}' is already in the desired state.`); return; } } const disableClicks = (selectedWindow) => { for (const [name, win] of windowPool) { if (win !== selectedWindow && !win.isDestroyed()) { win.setIgnoreMouseEvents(true, { forward: true }); } } }; const restoreClicks = () => { for (const [, win] of windowPool) { if (!win.isDestroyed()) win.setIgnoreMouseEvents(false); } }; if (name === 'settings') { if (shouldBeVisible) { // Cancel any pending hide operations if (settingsHideTimer) { clearTimeout(settingsHideTimer); settingsHideTimer = null; } const position = layoutManager.calculateSettingsWindowPosition(); if (position) { win.setBounds(position); win.__lockedByButton = true; win.show(); win.moveTop(); win.setAlwaysOnTop(true); } else { console.warn('[WindowManager] Could not calculate settings window position.'); } } else { // Hide after a delay if (settingsHideTimer) { clearTimeout(settingsHideTimer); } settingsHideTimer = setTimeout(() => { if (win && !win.isDestroyed()) { win.setAlwaysOnTop(false); win.hide(); } settingsHideTimer = null; }, 200); win.__lockedByButton = false; } return; } if (name === 'shortcut-settings') { if (shouldBeVisible) { // layoutManager.positionShortcutSettingsWindow(); const newBounds = layoutManager.calculateShortcutSettingsWindowPosition(); if (newBounds) win.setBounds(newBounds); if (process.platform === 'darwin') { win.setAlwaysOnTop(true, 'screen-saver'); } else { win.setAlwaysOnTop(true); } // globalShortcut.unregisterAll(); disableClicks(win); win.show(); } else { if (process.platform === 'darwin') { win.setAlwaysOnTop(false, 'screen-saver'); } else { win.setAlwaysOnTop(false); } restoreClicks(); win.hide(); } return; } if (name === 'listen' || name === 'ask') { const win = windowPool.get(name); const otherName = name === 'listen' ? 'ask' : 'listen'; const otherWin = windowPool.get(otherName); const isOtherWinVisible = otherWin && !otherWin.isDestroyed() && otherWin.isVisible(); const ANIM_OFFSET_X = 50; const ANIM_OFFSET_Y = 20; const finalVisibility = { listen: (name === 'listen' && shouldBeVisible) || (otherName === 'listen' && isOtherWinVisible), ask: (name === 'ask' && shouldBeVisible) || (otherName === 'ask' && isOtherWinVisible), }; if (!shouldBeVisible) { finalVisibility[name] = false; } const targetLayout = layoutManager.calculateFeatureWindowLayout(finalVisibility); if (shouldBeVisible) { if (!win) return; const targetBounds = targetLayout[name]; if (!targetBounds) return; const startPos = { ...targetBounds }; if (name === 'listen') startPos.x -= ANIM_OFFSET_X; else if (name === 'ask') startPos.y -= ANIM_OFFSET_Y; win.setOpacity(0); win.setBounds(startPos); win.show(); movementManager.fade(win, { to: 1 }); movementManager.animateLayout(targetLayout); } else { if (!win || !win.isVisible()) return; const currentBounds = win.getBounds(); const targetPos = { ...currentBounds }; if (name === 'listen') targetPos.x -= ANIM_OFFSET_X; else if (name === 'ask') targetPos.y -= ANIM_OFFSET_Y; movementManager.fade(win, { to: 0, onComplete: () => win.hide() }); movementManager.animateWindowPosition(win, targetPos); // 다른 창들도 새 레이아웃으로 애니메이션 const otherWindowsLayout = { ...targetLayout }; delete otherWindowsLayout[name]; movementManager.animateLayout(otherWindowsLayout); } } } const setContentProtection = (status) => { isContentProtectionOn = status; console.log(`[Protection] Content protection toggled to: ${isContentProtectionOn}`); windowPool.forEach(win => { if (win && !win.isDestroyed()) { win.setContentProtection(isContentProtectionOn); } }); }; const getContentProtectionStatus = () => isContentProtectionOn; const toggleContentProtection = () => { const newStatus = !getContentProtectionStatus(); setContentProtection(newStatus); return newStatus; }; const openLoginPage = () => { 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); }; 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: false, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, '../preload.js'), }, }; 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, '../ui/app/content.html'), listenLoadOptions); } else { listenLoadOptions.query.glass = 'true'; listen.loadFile(path.join(__dirname, '../ui/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, '../ui/app/content.html'), askLoadOptions); } else { askLoadOptions.query.glass = 'true'; ask.loadFile(path.join(__dirname, '../ui/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); } }); } // 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,'../ui/app/content.html'), settingsLoadOptions) .catch(console.error); } else { settingsLoadOptions.query.glass = 'true'; settings.loadFile(path.join(__dirname,'../ui/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); if (!app.isPackaged) { settings.webContents.openDevTools({ mode: 'detach' }); } break; } case 'shortcut-settings': { const shortcutEditor = new BrowserWindow({ ...commonChildOptions, width: 353, height: 720, modal: false, parent: undefined, alwaysOnTop: true, titleBarOverlay: false, }); shortcutEditor.setContentProtection(isContentProtectionOn); shortcutEditor.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); if (process.platform === 'darwin') { shortcutEditor.setWindowButtonVisibility(false); } const loadOptions = { query: { view: 'shortcut-settings' } }; if (!shouldUseLiquidGlass) { shortcutEditor.loadFile(path.join(__dirname, '../ui/app/content.html'), loadOptions); } else { loadOptions.query.glass = 'true'; shortcutEditor.loadFile(path.join(__dirname, '../ui/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); } }); } windowPool.set('shortcut-settings', shortcutEditor); if (!app.isPackaged) { shortcutEditor.webContents.openDevTools({ mode: 'detach' }); } break; } } }; if (Array.isArray(namesToCreate)) { namesToCreate.forEach(name => createFeatureWindow(name)); } else if (typeof namesToCreate === 'string') { createFeatureWindow(namesToCreate); } else { createFeatureWindow('listen'); createFeatureWindow('ask'); createFeatureWindow('settings'); createFeatureWindow('shortcut-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 createWindows() { const HEADER_HEIGHT = 47; const DEFAULT_WINDOW_WIDTH = 353; const primaryDisplay = screen.getPrimaryDisplay(); const { y: workAreaY, width: screenWidth } = primaryDisplay.workArea; const initialX = Math.round((screenWidth - DEFAULT_WINDOW_WIDTH) / 2); const initialY = workAreaY + 21; const header = new BrowserWindow({ width: DEFAULT_WINDOW_WIDTH, height: HEADER_HEIGHT, x: initialX, y: initialY, frame: false, transparent: true, vibrancy: false, hasShadow: false, alwaysOnTop: true, skipTaskbar: true, hiddenInMissionControl: true, resizable: false, focusable: true, acceptFirstMouse: true, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, '../preload.js'), 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, '../ui/app/header.html'), headerLoadOptions); } else { headerLoadOptions.query = { glass: 'true' }; header.loadFile(path.join(__dirname, '../ui/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); layoutManager = new WindowLayoutManager(windowPool); movementManager = new SmoothMovementManager(windowPool); header.on('moved', () => { if (movementManager.isAnimating) { return; } updateChildWindowLayouts(false); }); header.webContents.once('dom-ready', () => { shortcutsService.initialize(windowPool); shortcutsService.registerShortcuts(); }); setupIpcHandlers(windowPool, layoutManager); setupWindowController(windowPool, layoutManager, movementManager); if (currentHeaderState === 'main') { createFeatureWindows(header, ['listen', 'ask', 'settings', 'shortcut-settings']); } header.setContentProtection(isContentProtectionOn); header.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); // 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', () => updateChildWindowLayouts(false)); return windowPool; } function setupIpcHandlers(windowPool, layoutManager) { 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(); const newPosition = layoutManager.calculateNewPositionForDisplay(header, primaryDisplay.id); if (newPosition) { // 복구 상황이므로 애니메이션 없이 즉시 이동 header.setPosition(newPosition.x, newPosition.y, false); updateChildWindowLayouts(false); } } }); screen.on('display-metrics-changed', (event, display, changedMetrics) => { // 레이아웃 업데이트 함수를 새 버전으로 호출 updateChildWindowLayouts(false); }); } const handleHeaderStateChanged = (state) => { console.log(`[WindowManager] Header state changed to: ${state}`); currentHeaderState = state; if (state === 'main') { createFeatureWindows(windowPool.get('header')); } else { // 'apikey' | 'permission' destroyFeatureWindows(); } internalBridge.emit('reregister-shortcuts'); }; module.exports = { createWindows, windowPool, toggleContentProtection, resizeHeaderWindow, getContentProtectionStatus, showSettingsWindow, hideSettingsWindow, cancelHideSettingsWindow, openLoginPage, moveWindowStep, handleHeaderStateChanged, handleHeaderAnimationFinished, getHeaderPosition, moveHeaderTo, adjustWindowHeight, };