Merge branch 'main' into feature/encryption
This commit is contained in:
		
						commit
						60a8c30157
					
				@ -96,6 +96,7 @@ module.exports = {
 | 
			
		||||
    ipcMain.handle('listen:startMacosSystemAudio', async () => await listenService.handleStartMacosAudio());
 | 
			
		||||
    ipcMain.handle('listen:stopMacosSystemAudio', async () => await listenService.handleStopMacosAudio());
 | 
			
		||||
    ipcMain.handle('update-google-search-setting', async (event, enabled) => await listenService.handleUpdateGoogleSearchSetting(enabled));
 | 
			
		||||
    ipcMain.handle('listen:isSessionActive', async () => await listenService.isSessionActive());
 | 
			
		||||
    ipcMain.handle('listen:changeSession', async (event, listenButtonText) => {
 | 
			
		||||
      console.log('[FeatureBridge] listen:changeSession from mainheader', listenButtonText);
 | 
			
		||||
      try {
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,6 @@ module.exports = {
 | 
			
		||||
    ipcMain.on('header-state-changed', (event, state) => windowManager.handleHeaderStateChanged(state));
 | 
			
		||||
    ipcMain.on('header-animation-finished', (event, state) => windowManager.handleHeaderAnimationFinished(state));
 | 
			
		||||
    ipcMain.handle('get-header-position', () => windowManager.getHeaderPosition());
 | 
			
		||||
    ipcMain.handle('move-header', (event, newX, newY) => windowManager.moveHeader(newX, newY));
 | 
			
		||||
    ipcMain.handle('move-header-to', (event, newX, newY) => windowManager.moveHeaderTo(newX, newY));
 | 
			
		||||
    ipcMain.handle('adjust-window-height', (event, targetHeight) => windowManager.adjustWindowHeight(event.sender, targetHeight));
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@ -54,53 +54,7 @@ class ListenService {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleListenRequest(listenButtonText) {
 | 
			
		||||
        const { windowPool, updateLayout } = require('../../window/windowManager');
 | 
			
		||||
        const listenWindow = windowPool.get('listen');
 | 
			
		||||
        const header = windowPool.get('header');
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            switch (listenButtonText) {
 | 
			
		||||
                case 'Listen':
 | 
			
		||||
                    console.log('[ListenService] changeSession to "Listen"');
 | 
			
		||||
                    listenWindow.show();
 | 
			
		||||
                    updateLayout();
 | 
			
		||||
                    listenWindow.webContents.send('window-show-animation');
 | 
			
		||||
                    await this.initializeSession();
 | 
			
		||||
                    listenWindow.webContents.send('session-state-changed', { isActive: true });
 | 
			
		||||
                    break;
 | 
			
		||||
        
 | 
			
		||||
                case 'Stop':
 | 
			
		||||
                    console.log('[ListenService] changeSession to "Stop"');
 | 
			
		||||
                    await this.closeSession();
 | 
			
		||||
                    listenWindow.webContents.send('session-state-changed', { isActive: false });
 | 
			
		||||
                    break;
 | 
			
		||||
        
 | 
			
		||||
                case 'Done':
 | 
			
		||||
                    console.log('[ListenService] changeSession to "Done"');
 | 
			
		||||
                    listenWindow.webContents.send('window-hide-animation');
 | 
			
		||||
                    listenWindow.webContents.send('session-state-changed', { isActive: false });
 | 
			
		||||
                    break;
 | 
			
		||||
        
 | 
			
		||||
                default:
 | 
			
		||||
                    throw new Error(`[ListenService] unknown listenButtonText: ${listenButtonText}`);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            header.webContents.send('listen:changeSessionResult', { success: true });
 | 
			
		||||
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[ListenService] error in handleListenRequest:', error);
 | 
			
		||||
            header.webContents.send('listen:changeSessionResult', { success: false });
 | 
			
		||||
            throw error; 
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    initialize() {
 | 
			
		||||
        this.setupIpcHandlers();
 | 
			
		||||
        console.log('[ListenService] Initialized and ready.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleListenRequest(listenButtonText) {
 | 
			
		||||
        const { windowPool, updateLayout } = require('../../window/windowManager');
 | 
			
		||||
        const { windowPool } = require('../../window/windowManager');
 | 
			
		||||
        const listenWindow = windowPool.get('listen');
 | 
			
		||||
        const header = windowPool.get('header');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,13 +8,11 @@ class ShortcutsService {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.lastVisibleWindows = new Set(['header']);
 | 
			
		||||
        this.mouseEventsIgnored = false;
 | 
			
		||||
        this.movementManager = null;
 | 
			
		||||
        this.windowPool = null;
 | 
			
		||||
        this.allWindowVisibility = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    initialize(movementManager, windowPool) {
 | 
			
		||||
        this.movementManager = movementManager;
 | 
			
		||||
    initialize(windowPool) {
 | 
			
		||||
        this.windowPool = windowPool;
 | 
			
		||||
        internalBridge.on('reregister-shortcuts', () => {
 | 
			
		||||
            console.log('[ShortcutsService] Reregistering shortcuts due to header state change.');
 | 
			
		||||
@ -138,7 +136,7 @@ class ShortcutsService {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async registerShortcuts(registerOnlyToggleVisibility = false) {
 | 
			
		||||
        if (!this.movementManager || !this.windowPool) {
 | 
			
		||||
        if (!this.windowPool) {
 | 
			
		||||
            console.error('[Shortcuts] Service not initialized. Cannot register shortcuts.');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@ -179,7 +177,7 @@ class ShortcutsService {
 | 
			
		||||
        if (displays.length > 1) {
 | 
			
		||||
            displays.forEach((display, index) => {
 | 
			
		||||
                const key = `${modifier}+Shift+${index + 1}`;
 | 
			
		||||
                globalShortcut.register(key, () => this.movementManager.moveToDisplay(display.id));
 | 
			
		||||
                globalShortcut.register(key, () => internalBridge.emit('window:moveToDisplay', { displayId: display.id }));
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -190,7 +188,7 @@ class ShortcutsService {
 | 
			
		||||
        ];
 | 
			
		||||
        edgeDirections.forEach(({ key, direction }) => {
 | 
			
		||||
            globalShortcut.register(key, () => {
 | 
			
		||||
                if (header && header.isVisible()) this.movementManager.moveToEdge(direction);
 | 
			
		||||
                if (header && header.isVisible()) internalBridge.emit('window:moveToEdge', { direction });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -232,16 +230,16 @@ class ShortcutsService {
 | 
			
		||||
                    };
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'moveUp':
 | 
			
		||||
                    callback = () => { if (header && header.isVisible()) this.movementManager.moveStep('up'); };
 | 
			
		||||
                    callback = () => { if (header && header.isVisible()) internalBridge.emit('window:moveStep', { direction: 'up' }); };
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'moveDown':
 | 
			
		||||
                    callback = () => { if (header && header.isVisible()) this.movementManager.moveStep('down'); };
 | 
			
		||||
                    callback = () => { if (header && header.isVisible()) internalBridge.emit('window:moveStep', { direction: 'down' }); };
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'moveLeft':
 | 
			
		||||
                    callback = () => { if (header && header.isVisible()) this.movementManager.moveStep('left'); };
 | 
			
		||||
                    callback = () => { if (header && header.isVisible()) internalBridge.emit('window:moveStep', { direction: 'left' }); };
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'moveRight':
 | 
			
		||||
                    callback = () => { if (header && header.isVisible()) this.movementManager.moveStep('right'); };
 | 
			
		||||
                    callback = () => { if (header && header.isVisible()) internalBridge.emit('window:moveStep', { direction: 'right' }); };
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'toggleClickThrough':
 | 
			
		||||
                     callback = () => {
 | 
			
		||||
 | 
			
		||||
@ -290,7 +290,7 @@ contextBridge.exposeInMainWorld('api', {
 | 
			
		||||
    stopMacosSystemAudio: () => ipcRenderer.invoke('listen:stopMacosSystemAudio'),
 | 
			
		||||
    
 | 
			
		||||
    // Session Management
 | 
			
		||||
    isSessionActive: () => ipcRenderer.invoke('is-session-active'),
 | 
			
		||||
    isSessionActive: () => ipcRenderer.invoke('listen:isSessionActive'),
 | 
			
		||||
    
 | 
			
		||||
    // Listeners
 | 
			
		||||
    onSystemAudioData: (callback) => ipcRenderer.on('system-audio-data', callback),
 | 
			
		||||
 | 
			
		||||
@ -422,6 +422,12 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        if (isMacOS) {
 | 
			
		||||
 | 
			
		||||
            const sessionActive = await window.api.listenCapture.isSessionActive();
 | 
			
		||||
            if (!sessionActive) {
 | 
			
		||||
                throw new Error('STT sessions not initialized - please wait for initialization to complete');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // On macOS, use SystemAudioDump for audio and getDisplayMedia for screen
 | 
			
		||||
            console.log('Starting macOS capture with SystemAudioDump...');
 | 
			
		||||
 | 
			
		||||
@ -466,6 +472,12 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
 | 
			
		||||
 | 
			
		||||
            console.log('macOS screen capture started - audio handled by SystemAudioDump');
 | 
			
		||||
        } else if (isLinux) {
 | 
			
		||||
 | 
			
		||||
            const sessionActive = await window.api.listenCapture.isSessionActive();
 | 
			
		||||
            if (!sessionActive) {
 | 
			
		||||
                throw new Error('STT sessions not initialized - please wait for initialization to complete');
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Linux - use display media for screen capture and getUserMedia for microphone
 | 
			
		||||
            mediaStream = await navigator.mediaDevices.getDisplayMedia({
 | 
			
		||||
                video: {
 | 
			
		||||
 | 
			
		||||
@ -171,6 +171,7 @@ export class ShortcutSettingsView extends LitElement {
 | 
			
		||||
 | 
			
		||||
    async handleSave() {
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
        this.feedback = {};
 | 
			
		||||
        const result = await window.api.shortcutSettingsView.saveShortcuts(this.shortcuts);
 | 
			
		||||
        if (!result.success) {
 | 
			
		||||
            alert('Failed to save shortcuts: ' + result.error);
 | 
			
		||||
@ -179,6 +180,7 @@ export class ShortcutSettingsView extends LitElement {
 | 
			
		||||
 | 
			
		||||
    handleClose() {
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
        this.feedback = {};
 | 
			
		||||
        window.api.shortcutSettingsView.closeShortcutSettingsWindow();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,8 @@
 | 
			
		||||
const { screen } = require('electron');
 | 
			
		||||
 | 
			
		||||
class SmoothMovementManager {
 | 
			
		||||
    constructor(windowPool, getDisplayById, getCurrentDisplay, updateLayout) {
 | 
			
		||||
    constructor(windowPool) {
 | 
			
		||||
        this.windowPool = windowPool;
 | 
			
		||||
        this.getDisplayById = getDisplayById;
 | 
			
		||||
        this.getCurrentDisplay = getCurrentDisplay;
 | 
			
		||||
        this.updateLayout = updateLayout;
 | 
			
		||||
        this.stepSize = 80;
 | 
			
		||||
        this.animationDuration = 300;
 | 
			
		||||
        this.headerPosition = { x: 0, y: 0 };
 | 
			
		||||
@ -14,6 +11,8 @@ class SmoothMovementManager {
 | 
			
		||||
        this.lastVisiblePosition = null;
 | 
			
		||||
        this.currentDisplayId = null;
 | 
			
		||||
        this.animationFrameId = null;
 | 
			
		||||
 | 
			
		||||
        this.animationTimers = new Map();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -22,164 +21,25 @@ class SmoothMovementManager {
 | 
			
		||||
     */
 | 
			
		||||
    _isWindowValid(win) {
 | 
			
		||||
        if (!win || win.isDestroyed()) {
 | 
			
		||||
            if (this.isAnimating) {
 | 
			
		||||
                console.warn('[MovementManager] Window destroyed mid-animation. Halting.');
 | 
			
		||||
                this.isAnimating = false;
 | 
			
		||||
                if (this.animationFrameId) {
 | 
			
		||||
                    clearTimeout(this.animationFrameId);
 | 
			
		||||
                    this.animationFrameId = null;
 | 
			
		||||
                }
 | 
			
		||||
            // 해당 창의 타이머가 있으면 정리
 | 
			
		||||
            if (this.animationTimers.has(win)) {
 | 
			
		||||
                clearTimeout(this.animationTimers.get(win));
 | 
			
		||||
                this.animationTimers.delete(win);
 | 
			
		||||
            }
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    moveToDisplay(displayId) {
 | 
			
		||||
        const header = this.windowPool.get('header');
 | 
			
		||||
        if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
 | 
			
		||||
 | 
			
		||||
        const targetDisplay = this.getDisplayById(displayId);
 | 
			
		||||
        if (!targetDisplay) return;
 | 
			
		||||
 | 
			
		||||
        const currentBounds = header.getBounds();
 | 
			
		||||
        const currentDisplay = this.getCurrentDisplay(header);
 | 
			
		||||
 | 
			
		||||
        if (currentDisplay.id === targetDisplay.id) return;
 | 
			
		||||
 | 
			
		||||
        const relativeX = (currentBounds.x - currentDisplay.workArea.x) / currentDisplay.workAreaSize.width;
 | 
			
		||||
        const relativeY = (currentBounds.y - currentDisplay.workArea.y) / currentDisplay.workAreaSize.height;
 | 
			
		||||
        const targetX = targetDisplay.workArea.x + targetDisplay.workAreaSize.width * relativeX;
 | 
			
		||||
        const targetY = targetDisplay.workArea.y + targetDisplay.workAreaSize.height * relativeY;
 | 
			
		||||
 | 
			
		||||
        const finalX = Math.max(targetDisplay.workArea.x, Math.min(targetDisplay.workArea.x + targetDisplay.workAreaSize.width - currentBounds.width, targetX));
 | 
			
		||||
        const finalY = Math.max(targetDisplay.workArea.y, Math.min(targetDisplay.workArea.y + targetDisplay.workAreaSize.height - currentBounds.height, targetY));
 | 
			
		||||
 | 
			
		||||
        this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
 | 
			
		||||
        this.animateToPosition(header, finalX, finalY);
 | 
			
		||||
        this.currentDisplayId = targetDisplay.id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hideToEdge(edge, callback, { instant = false } = {}) {
 | 
			
		||||
        const header = this.windowPool.get('header');
 | 
			
		||||
        if (!header || header.isDestroyed()) {
 | 
			
		||||
            if (typeof callback === 'function') callback();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
      
 | 
			
		||||
        const { x, y } = header.getBounds();
 | 
			
		||||
        this.lastVisiblePosition = { x, y };
 | 
			
		||||
        this.hiddenPosition     = { edge };
 | 
			
		||||
      
 | 
			
		||||
        if (instant) {
 | 
			
		||||
            header.hide();
 | 
			
		||||
            if (typeof callback === 'function') callback();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        header.webContents.send('window-hide-animation');
 | 
			
		||||
      
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            if (!header.isDestroyed()) header.hide();
 | 
			
		||||
            if (typeof callback === 'function') callback();
 | 
			
		||||
        }, 5);
 | 
			
		||||
    }
 | 
			
		||||
      
 | 
			
		||||
    showFromEdge(callback) {
 | 
			
		||||
        const header = this.windowPool.get('header');
 | 
			
		||||
        if (!header || header.isDestroyed()) {
 | 
			
		||||
            if (typeof callback === 'function') callback();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
      
 | 
			
		||||
        // 숨기기 전에 기억해둔 위치 복구
 | 
			
		||||
        if (this.lastVisiblePosition) {
 | 
			
		||||
            header.setPosition(
 | 
			
		||||
                this.lastVisiblePosition.x,
 | 
			
		||||
                this.lastVisiblePosition.y,
 | 
			
		||||
                false   // animate: false
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
      
 | 
			
		||||
        header.show();
 | 
			
		||||
        header.webContents.send('window-show-animation');
 | 
			
		||||
      
 | 
			
		||||
        // 내부 상태 초기화
 | 
			
		||||
        this.hiddenPosition      = null;
 | 
			
		||||
        this.lastVisiblePosition = null;
 | 
			
		||||
      
 | 
			
		||||
        if (typeof callback === 'function') callback();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    moveStep(direction) {
 | 
			
		||||
        const header = this.windowPool.get('header');
 | 
			
		||||
        if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
 | 
			
		||||
 | 
			
		||||
        const currentBounds = header.getBounds();
 | 
			
		||||
        this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
 | 
			
		||||
        let targetX = this.headerPosition.x;
 | 
			
		||||
        let targetY = this.headerPosition.y;
 | 
			
		||||
 | 
			
		||||
        console.log(`[MovementManager] Moving ${direction} from (${targetX}, ${targetY})`);
 | 
			
		||||
 | 
			
		||||
        const windowSize = {
 | 
			
		||||
            width: currentBounds.width,
 | 
			
		||||
            height: currentBounds.height
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        switch (direction) {
 | 
			
		||||
            case 'left': targetX -= this.stepSize; break;
 | 
			
		||||
            case 'right': targetX += this.stepSize; break;
 | 
			
		||||
            case 'up': targetY -= this.stepSize; break;
 | 
			
		||||
            case 'down': targetY += this.stepSize; break;
 | 
			
		||||
            default: return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Find the display that contains or is nearest to the target position
 | 
			
		||||
        const nearestDisplay = screen.getDisplayNearestPoint({ x: targetX, y: targetY });
 | 
			
		||||
        const { x: workAreaX, y: workAreaY, width: workAreaWidth, height: workAreaHeight } = nearestDisplay.workArea;
 | 
			
		||||
        
 | 
			
		||||
        // Only clamp if the target position would actually go out of bounds
 | 
			
		||||
        let clampedX = targetX;
 | 
			
		||||
        let clampedY = targetY;
 | 
			
		||||
        
 | 
			
		||||
        // Check horizontal bounds
 | 
			
		||||
        if (targetX < workAreaX) {
 | 
			
		||||
            clampedX = workAreaX;
 | 
			
		||||
        } else if (targetX + currentBounds.width > workAreaX + workAreaWidth) {
 | 
			
		||||
            clampedX = workAreaX + workAreaWidth - currentBounds.width;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Check vertical bounds
 | 
			
		||||
        if (targetY < workAreaY) {
 | 
			
		||||
            clampedY = workAreaY;
 | 
			
		||||
            console.log(`[MovementManager] Clamped Y to top edge: ${clampedY}`);
 | 
			
		||||
        } else if (targetY + currentBounds.height > workAreaY + workAreaHeight) {
 | 
			
		||||
            clampedY = workAreaY + workAreaHeight - currentBounds.height;
 | 
			
		||||
            console.log(`[MovementManager] Clamped Y to bottom edge: ${clampedY}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        console.log(`[MovementManager] Final position: (${clampedX}, ${clampedY}), Work area: ${workAreaX},${workAreaY} ${workAreaWidth}x${workAreaHeight}`);
 | 
			
		||||
 | 
			
		||||
        // Only move if there's an actual change in position
 | 
			
		||||
        if (clampedX === this.headerPosition.x && clampedY === this.headerPosition.y) {
 | 
			
		||||
            console.log(`[MovementManager] No position change, skipping animation`);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        this.animateToPosition(header, clampedX, clampedY, windowSize);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * [수정됨] 창을 목표 지점으로 부드럽게 애니메이션합니다.
 | 
			
		||||
     * 완료 콜백 및 기타 옵션을 지원합니다.
 | 
			
		||||
     * @param {BrowserWindow} win - 애니메이션할 창
 | 
			
		||||
     * @param {number} targetX - 목표 X 좌표
 | 
			
		||||
     * @param {number} targetY - 목표 Y 좌표
 | 
			
		||||
     * @param {object} [options] - 추가 옵션
 | 
			
		||||
     * @param {object} [options.sizeOverride] - 애니메이션 중 사용할 창 크기
 | 
			
		||||
     * @param {function} [options.onComplete] - 애니메이션 완료 후 실행할 콜백
 | 
			
		||||
     * @param {number} [options.duration] - 애니메이션 지속 시간 (ms)
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {BrowserWindow} win
 | 
			
		||||
     * @param {number} targetX
 | 
			
		||||
     * @param {number} targetY
 | 
			
		||||
     * @param {object} [options]
 | 
			
		||||
     * @param {object} [options.sizeOverride]
 | 
			
		||||
     * @param {function} [options.onComplete]
 | 
			
		||||
     * @param {number} [options.duration]
 | 
			
		||||
     */
 | 
			
		||||
    animateWindow(win, targetX, targetY, options = {}) {
 | 
			
		||||
        if (!this._isWindowValid(win)) {
 | 
			
		||||
@ -194,7 +54,6 @@ class SmoothMovementManager {
 | 
			
		||||
        const { width, height } = sizeOverride || start;
 | 
			
		||||
 | 
			
		||||
        const step = () => {
 | 
			
		||||
            // 애니메이션 중간에 창이 파괴될 경우 콜백을 실행하고 중단
 | 
			
		||||
            if (!this._isWindowValid(win)) {
 | 
			
		||||
                if (onComplete) onComplete();
 | 
			
		||||
                return;
 | 
			
		||||
@ -208,112 +67,116 @@ class SmoothMovementManager {
 | 
			
		||||
            win.setBounds({ x: Math.round(x), y: Math.round(y), width, height });
 | 
			
		||||
 | 
			
		||||
            if (p < 1) {
 | 
			
		||||
                setTimeout(step, 8); // requestAnimationFrame 대신 setTimeout으로 간결하게 처리
 | 
			
		||||
                setTimeout(step, 8);
 | 
			
		||||
            } else {
 | 
			
		||||
                // 애니메이션 종료
 | 
			
		||||
                this.updateLayout(); // 레이아웃 재정렬
 | 
			
		||||
                this.layoutManager.updateLayout();
 | 
			
		||||
                if (onComplete) {
 | 
			
		||||
                    onComplete(); // 완료 콜백 실행
 | 
			
		||||
                    onComplete();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        step();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    animateToPosition(header, targetX, targetY, windowSize) {
 | 
			
		||||
        if (!this._isWindowValid(header)) return;
 | 
			
		||||
        
 | 
			
		||||
        this.isAnimating = true;
 | 
			
		||||
        const startX = this.headerPosition.x;
 | 
			
		||||
        const startY = this.headerPosition.y;
 | 
			
		||||
    fade(win, { from, to, duration = 250, onComplete }) {
 | 
			
		||||
        if (!this._isWindowValid(win)) {
 | 
			
		||||
          if (onComplete) onComplete();
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        const startOpacity = from ?? win.getOpacity();
 | 
			
		||||
        const startTime = Date.now();
 | 
			
		||||
        
 | 
			
		||||
        if (!Number.isFinite(targetX) || !Number.isFinite(targetY) || !Number.isFinite(startX) || !Number.isFinite(startY)) {
 | 
			
		||||
            this.isAnimating = false;
 | 
			
		||||
            return;
 | 
			
		||||
        const step = () => {
 | 
			
		||||
            if (!this._isWindowValid(win)) {
 | 
			
		||||
                if (onComplete) onComplete(); return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        const animate = () => {
 | 
			
		||||
            if (!this._isWindowValid(header)) return;
 | 
			
		||||
 | 
			
		||||
            const elapsed = Date.now() - startTime;
 | 
			
		||||
            const progress = Math.min(elapsed / this.animationDuration, 1);
 | 
			
		||||
            const progress = Math.min(1, (Date.now() - startTime) / duration);
 | 
			
		||||
            const eased = 1 - Math.pow(1 - progress, 3);
 | 
			
		||||
            const currentX = startX + (targetX - startX) * eased;
 | 
			
		||||
            const currentY = startY + (targetY - startY) * eased;
 | 
			
		||||
 | 
			
		||||
            if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) {
 | 
			
		||||
                this.isAnimating = false;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!this._isWindowValid(header)) return;
 | 
			
		||||
            const { width, height } = windowSize || header.getBounds();
 | 
			
		||||
            header.setBounds({
 | 
			
		||||
                x: Math.round(currentX),
 | 
			
		||||
                y: Math.round(currentY),
 | 
			
		||||
                width,
 | 
			
		||||
                height
 | 
			
		||||
            });
 | 
			
		||||
            win.setOpacity(startOpacity + (to - startOpacity) * eased);
 | 
			
		||||
    
 | 
			
		||||
            if (progress < 1) {
 | 
			
		||||
                this.animationFrameId = setTimeout(animate, 8);
 | 
			
		||||
                setTimeout(step, 8);
 | 
			
		||||
            } else {
 | 
			
		||||
                this.animationFrameId = null;
 | 
			
		||||
                win.setOpacity(to);
 | 
			
		||||
                if (onComplete) onComplete();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        step();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    animateWindowBounds(win, targetBounds, options = {}) {
 | 
			
		||||
        if (this.animationTimers.has(win)) {
 | 
			
		||||
            clearTimeout(this.animationTimers.get(win));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this._isWindowValid(win)) {
 | 
			
		||||
            if (options.onComplete) options.onComplete();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.isAnimating = true;
 | 
			
		||||
 | 
			
		||||
        const startBounds = win.getBounds();
 | 
			
		||||
        const startTime = Date.now();
 | 
			
		||||
        const duration = options.duration || this.animationDuration;
 | 
			
		||||
    
 | 
			
		||||
        const step = () => {
 | 
			
		||||
            if (!this._isWindowValid(win)) {
 | 
			
		||||
                if (options.onComplete) options.onComplete();
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            const progress = Math.min(1, (Date.now() - startTime) / duration);
 | 
			
		||||
            const eased = 1 - Math.pow(1 - progress, 3);
 | 
			
		||||
    
 | 
			
		||||
            const newBounds = {
 | 
			
		||||
                x: Math.round(startBounds.x + (targetBounds.x - startBounds.x) * eased),
 | 
			
		||||
                y: Math.round(startBounds.y + (targetBounds.y - startBounds.y) * eased),
 | 
			
		||||
                width: Math.round(startBounds.width + ((targetBounds.width ?? startBounds.width) - startBounds.width) * eased),
 | 
			
		||||
                height: Math.round(startBounds.height + ((targetBounds.height ?? startBounds.height) - startBounds.height) * eased),
 | 
			
		||||
            };
 | 
			
		||||
            win.setBounds(newBounds);
 | 
			
		||||
    
 | 
			
		||||
            if (progress < 1) {
 | 
			
		||||
                const timerId = setTimeout(step, 8);
 | 
			
		||||
                this.animationTimers.set(win, timerId);
 | 
			
		||||
            } else {
 | 
			
		||||
                win.setBounds(targetBounds);
 | 
			
		||||
                this.animationTimers.delete(win);
 | 
			
		||||
                
 | 
			
		||||
                if (this.animationTimers.size === 0) {
 | 
			
		||||
                    this.isAnimating = false;
 | 
			
		||||
                if (Number.isFinite(targetX) && Number.isFinite(targetY)) {
 | 
			
		||||
                    if (!this._isWindowValid(header)) return;
 | 
			
		||||
                    header.setPosition(Math.round(targetX), Math.round(targetY));
 | 
			
		||||
                    // Update header position to the actual final position
 | 
			
		||||
                    this.headerPosition = { x: Math.round(targetX), y: Math.round(targetY) };
 | 
			
		||||
                }
 | 
			
		||||
                this.updateLayout();
 | 
			
		||||
                
 | 
			
		||||
                if (options.onComplete) options.onComplete();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        animate();
 | 
			
		||||
        step();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    moveToEdge(direction) {
 | 
			
		||||
        const header = this.windowPool.get('header');
 | 
			
		||||
        if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
 | 
			
		||||
 | 
			
		||||
        const display = this.getCurrentDisplay(header);
 | 
			
		||||
        const { width, height } = display.workAreaSize;
 | 
			
		||||
        const { x: workAreaX, y: workAreaY } = display.workArea;
 | 
			
		||||
        const currentBounds = header.getBounds();
 | 
			
		||||
        
 | 
			
		||||
        const windowSize = {
 | 
			
		||||
            width: currentBounds.width,
 | 
			
		||||
            height: currentBounds.height
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let targetX = currentBounds.x;
 | 
			
		||||
        let targetY = currentBounds.y;
 | 
			
		||||
 | 
			
		||||
        switch (direction) {
 | 
			
		||||
            case 'left': 
 | 
			
		||||
                targetX = workAreaX; 
 | 
			
		||||
                break;
 | 
			
		||||
            case 'right': 
 | 
			
		||||
                targetX = workAreaX + width - windowSize.width; 
 | 
			
		||||
                break;
 | 
			
		||||
            case 'up': 
 | 
			
		||||
                targetY = workAreaY; 
 | 
			
		||||
                break;
 | 
			
		||||
            case 'down': 
 | 
			
		||||
                targetY = workAreaY + height - windowSize.height; 
 | 
			
		||||
                break;
 | 
			
		||||
    animateWindowPosition(win, targetPosition, options = {}) {
 | 
			
		||||
        if (!this._isWindowValid(win)) {
 | 
			
		||||
            if (options.onComplete) options.onComplete();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        const currentBounds = win.getBounds();
 | 
			
		||||
        const targetBounds = { ...currentBounds, ...targetPosition };
 | 
			
		||||
        this.animateWindowBounds(win, targetBounds, options);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
        header.setBounds({
 | 
			
		||||
            x: Math.round(targetX),
 | 
			
		||||
            y: Math.round(targetY),
 | 
			
		||||
            width: windowSize.width,
 | 
			
		||||
            height: windowSize.height
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.headerPosition = { x: targetX, y: targetY };
 | 
			
		||||
        this.updateLayout();
 | 
			
		||||
    animateLayout(layout, animated = true) {
 | 
			
		||||
        if (!layout) return;
 | 
			
		||||
        for (const winName in layout) {
 | 
			
		||||
            const win = this.windowPool.get(winName);
 | 
			
		||||
            const targetBounds = layout[winName];
 | 
			
		||||
            if (win && !win.isDestroyed() && targetBounds) {
 | 
			
		||||
                if (animated) {
 | 
			
		||||
                    this.animateWindowBounds(win, targetBounds);
 | 
			
		||||
                } else {
 | 
			
		||||
                    win.setBounds(targetBounds);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroy() {
 | 
			
		||||
 | 
			
		||||
@ -27,130 +27,15 @@ class WindowLayoutManager {
 | 
			
		||||
        this.PADDING = 80;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateLayout() {
 | 
			
		||||
        if (this.isUpdating) return;
 | 
			
		||||
        this.isUpdating = true;
 | 
			
		||||
 | 
			
		||||
        setImmediate(() => {
 | 
			
		||||
            this.positionWindows();
 | 
			
		||||
            this.isUpdating = false;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {object} [visibilityOverride] - { listen: true, ask: true }
 | 
			
		||||
     * @returns {{listen: {x:number, y:number}|null, ask: {x:number, y:number}|null}}
 | 
			
		||||
     */
 | 
			
		||||
    getTargetBoundsForFeatureWindows(visibilityOverride = {}) {
 | 
			
		||||
    getHeaderPosition = () => {
 | 
			
		||||
        const header = this.windowPool.get('header');
 | 
			
		||||
        if (!header?.getBounds) return {};
 | 
			
		||||
 
 | 
			
		||||
        const headerBounds = header.getBounds();
 | 
			
		||||
        const display = getCurrentDisplay(header);
 | 
			
		||||
        const { width: screenWidth, height: screenHeight } = display.workAreaSize;
 | 
			
		||||
        const { x: workAreaX, y: workAreaY } = display.workArea;
 | 
			
		||||
 
 | 
			
		||||
        const ask = this.windowPool.get('ask');
 | 
			
		||||
        const listen = this.windowPool.get('listen');
 | 
			
		||||
 
 | 
			
		||||
        const askVis = visibilityOverride.ask !== undefined ?
 | 
			
		||||
            visibilityOverride.ask :
 | 
			
		||||
            (ask && ask.isVisible() && !ask.isDestroyed());
 | 
			
		||||
        const listenVis = visibilityOverride.listen !== undefined ?
 | 
			
		||||
            visibilityOverride.listen :
 | 
			
		||||
            (listen && listen.isVisible() && !listen.isDestroyed());
 | 
			
		||||
 
 | 
			
		||||
        if (!askVis && !listenVis) return {};
 | 
			
		||||
 
 | 
			
		||||
        const PAD = 8;
 | 
			
		||||
        const headerTopRel = headerBounds.y - workAreaY;
 | 
			
		||||
        const headerBottomRel = headerTopRel + headerBounds.height;
 | 
			
		||||
        const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2;
 | 
			
		||||
        
 | 
			
		||||
        const relativeX = headerCenterXRel / screenWidth;
 | 
			
		||||
        const relativeY = (headerBounds.y - workAreaY) / screenHeight;
 | 
			
		||||
        const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY, workAreaX, workAreaY);
 | 
			
		||||
 
 | 
			
		||||
        const askB = ask ? ask.getBounds() : null;
 | 
			
		||||
        const listenB = listen ? listen.getBounds() : null;
 | 
			
		||||
 
 | 
			
		||||
        const result = { listen: null, ask: null };
 | 
			
		||||
 
 | 
			
		||||
        if (askVis && listenVis) {
 | 
			
		||||
            let askXRel = headerCenterXRel - (askB.width / 2);
 | 
			
		||||
            let listenXRel = askXRel - listenB.width - PAD;
 | 
			
		||||
 
 | 
			
		||||
            if (listenXRel < PAD) {
 | 
			
		||||
                listenXRel = PAD;
 | 
			
		||||
                askXRel = listenXRel + listenB.width + PAD;
 | 
			
		||||
            }
 | 
			
		||||
            if (askXRel + askB.width > screenWidth - PAD) {
 | 
			
		||||
                askXRel = screenWidth - PAD - askB.width;
 | 
			
		||||
                listenXRel = askXRel - listenB.width - PAD;
 | 
			
		||||
        if (header) {
 | 
			
		||||
            const [x, y] = header.getPosition();
 | 
			
		||||
            return { x, y };
 | 
			
		||||
        }
 | 
			
		||||
        return { x: 0, y: 0 };
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
            // [수정] 'above'일 경우 하단 정렬, 'below'일 경우 상단 정렬
 | 
			
		||||
            if (strategy.primary === 'above') {
 | 
			
		||||
                const windowBottomAbs = headerBounds.y - PAD;
 | 
			
		||||
                const askY = windowBottomAbs - askB.height;
 | 
			
		||||
                const listenY = windowBottomAbs - listenB.height;
 | 
			
		||||
                result.ask = { x: Math.round(askXRel + workAreaX), y: Math.round(askY) };
 | 
			
		||||
                result.listen = { x: Math.round(listenXRel + workAreaX), y: Math.round(listenY) };
 | 
			
		||||
            } else { // 'below'
 | 
			
		||||
                const yPos = headerBottomRel + PAD;
 | 
			
		||||
                const yAbs = yPos + workAreaY;
 | 
			
		||||
                result.ask = { x: Math.round(askXRel + workAreaX), y: Math.round(yAbs) };
 | 
			
		||||
                result.listen = { x: Math.round(listenXRel + workAreaX), y: Math.round(yAbs) };
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
        } else { // 한 창만 보일 때는 기존 로직 유지 (정상 동작 확인)
 | 
			
		||||
            const winB = askVis ? askB : listenB;
 | 
			
		||||
            let xRel = headerCenterXRel - winB.width / 2;
 | 
			
		||||
            xRel = Math.max(PAD, Math.min(screenWidth - winB.width - PAD, xRel));
 | 
			
		||||
 | 
			
		||||
            let yPos;
 | 
			
		||||
            if (strategy.primary === 'above') {
 | 
			
		||||
                const windowBottomRel = headerTopRel - PAD;
 | 
			
		||||
                yPos = windowBottomRel - winB.height;
 | 
			
		||||
            } else { // 'below'
 | 
			
		||||
                yPos = headerBottomRel + PAD;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            const abs = { x: Math.round(xRel + workAreaX), y: Math.round(yPos + workAreaY) };
 | 
			
		||||
            if (askVis) result.ask = abs;
 | 
			
		||||
            if (listenVis) result.listen = abs;
 | 
			
		||||
        }
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    positionWindows() {
 | 
			
		||||
        const header = this.windowPool.get('header');
 | 
			
		||||
        if (!header?.getBounds) return;
 | 
			
		||||
 | 
			
		||||
        const headerBounds = header.getBounds();
 | 
			
		||||
        const display = getCurrentDisplay(header);
 | 
			
		||||
        const { width: screenWidth, height: screenHeight } = display.workAreaSize;
 | 
			
		||||
        const { x: workAreaX, y: workAreaY } = display.workArea;
 | 
			
		||||
 | 
			
		||||
        const headerCenterX = headerBounds.x - workAreaX + headerBounds.width / 2;
 | 
			
		||||
        const headerCenterY = headerBounds.y - workAreaY + headerBounds.height / 2;
 | 
			
		||||
 | 
			
		||||
        const relativeX = headerCenterX / screenWidth;
 | 
			
		||||
        const relativeY = headerCenterY / screenHeight;
 | 
			
		||||
 | 
			
		||||
        const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY, workAreaX, workAreaY);
 | 
			
		||||
 | 
			
		||||
        this.positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY);
 | 
			
		||||
        const settings = this.windowPool.get('settings');
 | 
			
		||||
        if (settings && !settings.isDestroyed() && settings.isVisible()) {
 | 
			
		||||
            const settingPos = this.calculateSettingsWindowPosition();
 | 
			
		||||
            if (settingPos) {
 | 
			
		||||
                const { width, height } = settings.getBounds();
 | 
			
		||||
                settings.setBounds({ x: settingPos.x, y: settingPos.y, width, height });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
@ -179,67 +64,6 @@ class WindowLayoutManager {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY) {
 | 
			
		||||
        const ask = this.windowPool.get('ask');
 | 
			
		||||
        const listen = this.windowPool.get('listen');
 | 
			
		||||
        const askVisible = ask && ask.isVisible() && !ask.isDestroyed();
 | 
			
		||||
        const listenVisible = listen && listen.isVisible() && !listen.isDestroyed();
 | 
			
		||||
 | 
			
		||||
        if (!askVisible && !listenVisible) return;
 | 
			
		||||
 | 
			
		||||
        const PAD = 8;
 | 
			
		||||
        const headerTopRel = headerBounds.y - workAreaY;
 | 
			
		||||
        const headerBottomRel = headerTopRel + headerBounds.height;
 | 
			
		||||
        const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2;
 | 
			
		||||
        
 | 
			
		||||
        let askBounds = askVisible ? ask.getBounds() : null;
 | 
			
		||||
        let listenBounds = listenVisible ? listen.getBounds() : null;
 | 
			
		||||
 | 
			
		||||
        if (askVisible && listenVisible) {
 | 
			
		||||
            let askXRel = headerCenterXRel - (askBounds.width / 2);
 | 
			
		||||
            let listenXRel = askXRel - listenBounds.width - PAD;
 | 
			
		||||
 | 
			
		||||
            if (listenXRel < PAD) {
 | 
			
		||||
                listenXRel = PAD;
 | 
			
		||||
                askXRel = listenXRel + listenBounds.width + PAD;
 | 
			
		||||
            }
 | 
			
		||||
            if (askXRel + askBounds.width > screenWidth - PAD) {
 | 
			
		||||
                askXRel = screenWidth - PAD - askBounds.width;
 | 
			
		||||
                listenXRel = askXRel - listenBounds.width - PAD;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // [수정] 'above'일 경우 하단 정렬, 'below'일 경우 상단 정렬
 | 
			
		||||
            if (strategy.primary === 'above') {
 | 
			
		||||
                const windowBottomAbs = headerBounds.y - PAD;
 | 
			
		||||
                const askY = windowBottomAbs - askBounds.height;
 | 
			
		||||
                const listenY = windowBottomAbs - listenBounds.height;
 | 
			
		||||
                ask.setBounds({ x: Math.round(askXRel + workAreaX), y: Math.round(askY), width: askBounds.width, height: askBounds.height });
 | 
			
		||||
                listen.setBounds({ x: Math.round(listenXRel + workAreaX), y: Math.round(listenY), width: listenBounds.width, height: listenBounds.height });
 | 
			
		||||
            } else { // 'below'
 | 
			
		||||
                const yPos = headerBottomRel + PAD;
 | 
			
		||||
                const yAbs = yPos + workAreaY;
 | 
			
		||||
                ask.setBounds({ x: Math.round(askXRel + workAreaX), y: Math.round(yAbs), width: askBounds.width, height: askBounds.height });
 | 
			
		||||
                listen.setBounds({ x: Math.round(listenXRel + workAreaX), y: Math.round(yAbs), width: listenBounds.width, height: listenBounds.height });
 | 
			
		||||
            }
 | 
			
		||||
        
 | 
			
		||||
        } else { // 한 창만 보일 때는 기존 로직 유지 (정상 동작 확인)
 | 
			
		||||
            const win = askVisible ? ask : listen;
 | 
			
		||||
            const winBounds = askVisible ? askBounds : listenBounds;
 | 
			
		||||
            let xRel = headerCenterXRel - winBounds.width / 2;
 | 
			
		||||
            xRel = Math.max(PAD, Math.min(screenWidth - winBounds.width - PAD, xRel));
 | 
			
		||||
 | 
			
		||||
            let yPos;
 | 
			
		||||
            if (strategy.primary === 'above') {
 | 
			
		||||
                const windowBottomRel = headerTopRel - PAD;
 | 
			
		||||
                yPos = windowBottomRel - winBounds.height;
 | 
			
		||||
            } else { // 'below'
 | 
			
		||||
                yPos = headerBottomRel + PAD;
 | 
			
		||||
            }
 | 
			
		||||
            const yAbs = yPos + workAreaY;
 | 
			
		||||
 | 
			
		||||
            win.setBounds({ x: Math.round(xRel + workAreaX), y: Math.round(yAbs), width: winBounds.width, height: winBounds.height });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @returns {{x: number, y: number} | null}
 | 
			
		||||
@ -269,18 +93,139 @@ class WindowLayoutManager {
 | 
			
		||||
        return { x: Math.round(clampedX), y: Math.round(clampedY) };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    positionShortcutSettingsWindow() {
 | 
			
		||||
 | 
			
		||||
    calculateHeaderResize(header, { width, height }) {
 | 
			
		||||
        if (!header) return null;
 | 
			
		||||
        const currentBounds = header.getBounds();
 | 
			
		||||
        const centerX = currentBounds.x + currentBounds.width / 2;
 | 
			
		||||
        const newX = Math.round(centerX - width / 2);
 | 
			
		||||
        const display = getCurrentDisplay(header);
 | 
			
		||||
        const { x: workAreaX, width: workAreaWidth } = display.workArea;
 | 
			
		||||
        const clampedX = Math.max(workAreaX, Math.min(workAreaX + workAreaWidth - width, newX));
 | 
			
		||||
        return { x: clampedX, y: currentBounds.y, width, height };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    calculateClampedPosition(header, { x: newX, y: newY }) {
 | 
			
		||||
        if (!header) return null;
 | 
			
		||||
        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(newX, workAreaX + width - headerBounds.width));
 | 
			
		||||
        const clampedY = Math.max(workAreaY, Math.min(newY, workAreaY + height - headerBounds.height));
 | 
			
		||||
        return { x: clampedX, y: clampedY };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    calculateWindowHeightAdjustment(senderWindow, targetHeight) {
 | 
			
		||||
        if (!senderWindow) return null;
 | 
			
		||||
        const currentBounds = senderWindow.getBounds();
 | 
			
		||||
        const minHeight = senderWindow.getMinimumSize()[1];
 | 
			
		||||
        const maxHeight = senderWindow.getMaximumSize()[1];
 | 
			
		||||
        let adjustedHeight = Math.max(minHeight, targetHeight);
 | 
			
		||||
        if (maxHeight > 0) {
 | 
			
		||||
            adjustedHeight = Math.min(maxHeight, adjustedHeight);
 | 
			
		||||
        }
 | 
			
		||||
        return { ...currentBounds, height: adjustedHeight };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 기존 getTargetBoundsForFeatureWindows를 이 함수로 대체합니다.
 | 
			
		||||
    calculateFeatureWindowLayout(visibility, headerBoundsOverride = null) {
 | 
			
		||||
        const header = this.windowPool.get('header');
 | 
			
		||||
        const headerBounds = headerBoundsOverride || (header ? header.getBounds() : null);
 | 
			
		||||
 | 
			
		||||
        if (!headerBounds) return {};
 | 
			
		||||
 | 
			
		||||
        let display;
 | 
			
		||||
        if (headerBoundsOverride) {
 | 
			
		||||
            const boundsCenter = {
 | 
			
		||||
                x: headerBounds.x + headerBounds.width / 2,
 | 
			
		||||
                y: headerBounds.y + headerBounds.height / 2,
 | 
			
		||||
            };
 | 
			
		||||
            display = screen.getDisplayNearestPoint(boundsCenter);
 | 
			
		||||
        } else {
 | 
			
		||||
            display = getCurrentDisplay(header);
 | 
			
		||||
        }
 | 
			
		||||
    
 | 
			
		||||
        const { width: screenWidth, height: screenHeight, x: workAreaX, y: workAreaY } = display.workArea;
 | 
			
		||||
    
 | 
			
		||||
        const ask = this.windowPool.get('ask');
 | 
			
		||||
        const listen = this.windowPool.get('listen');
 | 
			
		||||
    
 | 
			
		||||
        const askVis = visibility.ask && ask && !ask.isDestroyed();
 | 
			
		||||
        const listenVis = visibility.listen && listen && !listen.isDestroyed();
 | 
			
		||||
    
 | 
			
		||||
        if (!askVis && !listenVis) return {};
 | 
			
		||||
    
 | 
			
		||||
        const PAD = 8;
 | 
			
		||||
        const headerTopRel = headerBounds.y - workAreaY;
 | 
			
		||||
        const headerBottomRel = headerTopRel + headerBounds.height;
 | 
			
		||||
        const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2;
 | 
			
		||||
        
 | 
			
		||||
        const relativeX = headerCenterXRel / screenWidth;
 | 
			
		||||
        const relativeY = (headerBounds.y - workAreaY) / screenHeight;
 | 
			
		||||
        const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY, workAreaX, workAreaY);
 | 
			
		||||
    
 | 
			
		||||
        const askB = askVis ? ask.getBounds() : null;
 | 
			
		||||
        const listenB = listenVis ? listen.getBounds() : null;
 | 
			
		||||
 | 
			
		||||
        if (askVis) {
 | 
			
		||||
            console.log(`[Layout Debug] Ask Window Bounds: height=${askB.height}, width=${askB.width}`);
 | 
			
		||||
        }
 | 
			
		||||
        if (listenVis) {
 | 
			
		||||
            console.log(`[Layout Debug] Listen Window Bounds: height=${listenB.height}, width=${listenB.width}`);
 | 
			
		||||
        }
 | 
			
		||||
    
 | 
			
		||||
        const layout = {};
 | 
			
		||||
    
 | 
			
		||||
        if (askVis && listenVis) {
 | 
			
		||||
            let askXRel = headerCenterXRel - (askB.width / 2);
 | 
			
		||||
            let listenXRel = askXRel - listenB.width - PAD;
 | 
			
		||||
    
 | 
			
		||||
            if (listenXRel < PAD) {
 | 
			
		||||
                listenXRel = PAD;
 | 
			
		||||
                askXRel = listenXRel + listenB.width + PAD;
 | 
			
		||||
            }
 | 
			
		||||
            if (askXRel + askB.width > screenWidth - PAD) {
 | 
			
		||||
                askXRel = screenWidth - PAD - askB.width;
 | 
			
		||||
                listenXRel = askXRel - listenB.width - PAD;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            if (strategy.primary === 'above') {
 | 
			
		||||
                const windowBottomAbs = headerBounds.y - PAD;
 | 
			
		||||
                layout.ask = { x: Math.round(askXRel + workAreaX), y: Math.round(windowBottomAbs - askB.height), width: askB.width, height: askB.height };
 | 
			
		||||
                layout.listen = { x: Math.round(listenXRel + workAreaX), y: Math.round(windowBottomAbs - listenB.height), width: listenB.width, height: listenB.height };
 | 
			
		||||
            } else { // 'below'
 | 
			
		||||
                const yAbs = headerBounds.y + headerBounds.height + PAD;
 | 
			
		||||
                layout.ask = { x: Math.round(askXRel + workAreaX), y: Math.round(yAbs), width: askB.width, height: askB.height };
 | 
			
		||||
                layout.listen = { x: Math.round(listenXRel + workAreaX), y: Math.round(yAbs), width: listenB.width, height: listenB.height };
 | 
			
		||||
            }
 | 
			
		||||
        } else { // Single window
 | 
			
		||||
            const winName = askVis ? 'ask' : 'listen';
 | 
			
		||||
            const winB = askVis ? askB : listenB;
 | 
			
		||||
            if (!winB) return {};
 | 
			
		||||
    
 | 
			
		||||
            let xRel = headerCenterXRel - winB.width / 2;
 | 
			
		||||
            xRel = Math.max(PAD, Math.min(screenWidth - winB.width - PAD, xRel));
 | 
			
		||||
    
 | 
			
		||||
            let yPos;
 | 
			
		||||
            if (strategy.primary === 'above') {
 | 
			
		||||
                yPos = (headerBounds.y - workAreaY) - PAD - winB.height;
 | 
			
		||||
            } else { // 'below'
 | 
			
		||||
                yPos = (headerBounds.y - workAreaY) + headerBounds.height + PAD;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            layout[winName] = { x: Math.round(xRel + workAreaX), y: Math.round(yPos + workAreaY), width: winB.width, height: winB.height };
 | 
			
		||||
        }
 | 
			
		||||
        return layout;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    calculateShortcutSettingsWindowPosition() {
 | 
			
		||||
        const header = this.windowPool.get('header');
 | 
			
		||||
        const shortcutSettings = this.windowPool.get('shortcut-settings');
 | 
			
		||||
 | 
			
		||||
        if (!header || header.isDestroyed() || !shortcutSettings || shortcutSettings.isDestroyed()) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (!header || !shortcutSettings) return null;
 | 
			
		||||
    
 | 
			
		||||
        const headerBounds = header.getBounds();
 | 
			
		||||
        const shortcutBounds = shortcutSettings.getBounds();
 | 
			
		||||
        const display = getCurrentDisplay(header);
 | 
			
		||||
        const { workArea } = display;
 | 
			
		||||
        const { workArea } = getCurrentDisplay(header);
 | 
			
		||||
    
 | 
			
		||||
        let newX = Math.round(headerBounds.x + (headerBounds.width / 2) - (shortcutBounds.width / 2));
 | 
			
		||||
        let newY = Math.round(headerBounds.y);
 | 
			
		||||
@ -288,7 +233,65 @@ class WindowLayoutManager {
 | 
			
		||||
        newX = Math.max(workArea.x, Math.min(newX, workArea.x + workArea.width - shortcutBounds.width));
 | 
			
		||||
        newY = Math.max(workArea.y, Math.min(newY, workArea.y + workArea.height - shortcutBounds.height));
 | 
			
		||||
    
 | 
			
		||||
        shortcutSettings.setBounds({ x: newX, y: newY, width: shortcutBounds.width, height: shortcutBounds.height });
 | 
			
		||||
        return { x: newX, y: newY, width: shortcutBounds.width, height: shortcutBounds.height };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    calculateStepMovePosition(header, direction) {
 | 
			
		||||
        if (!header) return null;
 | 
			
		||||
        const currentBounds = header.getBounds();
 | 
			
		||||
        const stepSize = 80; // 이동 간격
 | 
			
		||||
        let targetX = currentBounds.x;
 | 
			
		||||
        let targetY = currentBounds.y;
 | 
			
		||||
    
 | 
			
		||||
        switch (direction) {
 | 
			
		||||
            case 'left': targetX -= stepSize; break;
 | 
			
		||||
            case 'right': targetX += stepSize; break;
 | 
			
		||||
            case 'up': targetY -= stepSize; break;
 | 
			
		||||
            case 'down': targetY += stepSize; break;
 | 
			
		||||
        }
 | 
			
		||||
    
 | 
			
		||||
        return this.calculateClampedPosition(header, { x: targetX, y: targetY });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    calculateEdgePosition(header, direction) {
 | 
			
		||||
        if (!header) return null;
 | 
			
		||||
        const display = getCurrentDisplay(header);
 | 
			
		||||
        const { workArea } = display;
 | 
			
		||||
        const currentBounds = header.getBounds();
 | 
			
		||||
    
 | 
			
		||||
        let targetX = currentBounds.x;
 | 
			
		||||
        let targetY = currentBounds.y;
 | 
			
		||||
    
 | 
			
		||||
        switch (direction) {
 | 
			
		||||
            case 'left': targetX = workArea.x; break;
 | 
			
		||||
            case 'right': targetX = workArea.x + workArea.width - currentBounds.width; break;
 | 
			
		||||
            case 'up': targetY = workArea.y; break;
 | 
			
		||||
            case 'down': targetY = workArea.y + workArea.height - currentBounds.height; break;
 | 
			
		||||
        }
 | 
			
		||||
        return { x: targetX, y: targetY };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    calculateNewPositionForDisplay(window, targetDisplayId) {
 | 
			
		||||
        if (!window) return null;
 | 
			
		||||
    
 | 
			
		||||
        const targetDisplay = screen.getAllDisplays().find(d => d.id === targetDisplayId);
 | 
			
		||||
        if (!targetDisplay) return null;
 | 
			
		||||
    
 | 
			
		||||
        const currentBounds = window.getBounds();
 | 
			
		||||
        const currentDisplay = getCurrentDisplay(window);
 | 
			
		||||
    
 | 
			
		||||
        if (currentDisplay.id === targetDisplay.id) return { x: currentBounds.x, y: currentBounds.y };
 | 
			
		||||
    
 | 
			
		||||
        const relativeX = (currentBounds.x - currentDisplay.workArea.x) / currentDisplay.workArea.width;
 | 
			
		||||
        const relativeY = (currentBounds.y - currentDisplay.workArea.y) / currentDisplay.workArea.height;
 | 
			
		||||
        
 | 
			
		||||
        const targetX = targetDisplay.workArea.x + targetDisplay.workArea.width * relativeX;
 | 
			
		||||
        const targetY = targetDisplay.workArea.y + targetDisplay.workArea.height * relativeY;
 | 
			
		||||
    
 | 
			
		||||
        const clampedX = Math.max(targetDisplay.workArea.x, Math.min(targetX, targetDisplay.workArea.x + targetDisplay.workArea.width - currentBounds.width));
 | 
			
		||||
        const clampedY = Math.max(targetDisplay.workArea.y, Math.min(targetY, targetDisplay.workArea.y + targetDisplay.workArea.height - currentBounds.height));
 | 
			
		||||
    
 | 
			
		||||
        return { x: Math.round(clampedX), y: Math.round(clampedY) };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -30,8 +30,6 @@ if (shouldUseLiquidGlass) {
 | 
			
		||||
 | 
			
		||||
let isContentProtectionOn = true;
 | 
			
		||||
let lastVisibleWindows = new Set(['header']);
 | 
			
		||||
const HEADER_HEIGHT = 47;
 | 
			
		||||
const DEFAULT_WINDOW_WIDTH = 353;
 | 
			
		||||
 | 
			
		||||
let currentHeaderState = 'apikey';
 | 
			
		||||
const windowPool = new Map();
 | 
			
		||||
@ -40,50 +38,26 @@ let settingsHideTimer = null;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
let layoutManager = null;
 | 
			
		||||
function updateLayout() {
 | 
			
		||||
    if (layoutManager) {
 | 
			
		||||
        layoutManager.updateLayout();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
let movementManager = null;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const FADE_DURATION = 250;
 | 
			
		||||
const FADE_FPS      = 60;
 | 
			
		||||
function updateChildWindowLayouts(animated = true) {
 | 
			
		||||
    if (movementManager.isAnimating) return;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 윈도우 투명도를 서서히 변경한다.
 | 
			
		||||
 * @param {BrowserWindow} win
 | 
			
		||||
 * @param {number} from
 | 
			
		||||
 * @param {number} to
 | 
			
		||||
 * @param {number} duration
 | 
			
		||||
 * @param {Function=} onComplete 
 | 
			
		||||
 */
 | 
			
		||||
function fadeWindow(win, from, to, duration = FADE_DURATION, onComplete) {
 | 
			
		||||
  if (!win || win.isDestroyed()) return;
 | 
			
		||||
 | 
			
		||||
  const steps       = Math.max(1, Math.round(duration / (1000 / FADE_FPS)));
 | 
			
		||||
  let   currentStep = 0;
 | 
			
		||||
 | 
			
		||||
  win.setOpacity(from);
 | 
			
		||||
 | 
			
		||||
  const timer = setInterval(() => {
 | 
			
		||||
    if (win.isDestroyed()) { clearInterval(timer); return; }
 | 
			
		||||
 | 
			
		||||
    currentStep += 1;
 | 
			
		||||
    const progress = currentStep / steps;
 | 
			
		||||
    const eased    = progress < 1
 | 
			
		||||
      ? 1 - Math.pow(1 - progress, 3)
 | 
			
		||||
      : 1;
 | 
			
		||||
 | 
			
		||||
    win.setOpacity(from + (to - from) * eased);
 | 
			
		||||
 | 
			
		||||
    if (currentStep >= steps) {
 | 
			
		||||
      clearInterval(timer);
 | 
			
		||||
      win.setOpacity(to);
 | 
			
		||||
      onComplete && onComplete();
 | 
			
		||||
    const visibleWindows = {};
 | 
			
		||||
    const listenWin = windowPool.get('listen');
 | 
			
		||||
    const askWin = windowPool.get('ask');
 | 
			
		||||
    if (listenWin && !listenWin.isDestroyed() && listenWin.isVisible()) {
 | 
			
		||||
        visibleWindows.listen = true;
 | 
			
		||||
    }
 | 
			
		||||
  }, 1000 / FADE_FPS);
 | 
			
		||||
    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 = () => {
 | 
			
		||||
@ -98,6 +72,34 @@ 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 }) => {
 | 
			
		||||
@ -106,6 +108,109 @@ function setupWindowController(windowPool, layoutManager, movementManager) {
 | 
			
		||||
    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) {
 | 
			
		||||
@ -220,7 +325,10 @@ async function handleWindowVisibilityRequest(windowPool, layoutManager, movement
 | 
			
		||||
 | 
			
		||||
    if (name === 'shortcut-settings') {
 | 
			
		||||
        if (shouldBeVisible) {
 | 
			
		||||
            layoutManager.positionShortcutSettingsWindow();
 | 
			
		||||
            // layoutManager.positionShortcutSettingsWindow();
 | 
			
		||||
            const newBounds = layoutManager.calculateShortcutSettingsWindowPosition();
 | 
			
		||||
            if (newBounds) win.setBounds(newBounds);
 | 
			
		||||
            
 | 
			
		||||
            if (process.platform === 'darwin') {
 | 
			
		||||
                win.setAlwaysOnTop(true, 'screen-saver');
 | 
			
		||||
            } else {
 | 
			
		||||
@ -242,91 +350,55 @@ async function handleWindowVisibilityRequest(windowPool, layoutManager, movement
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 = 100; 
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
            if (name === 'listen') {
 | 
			
		||||
                if (!isOtherWinVisible) {
 | 
			
		||||
                    const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: false });
 | 
			
		||||
                    if (!targets.listen) return;
 | 
			
		||||
 | 
			
		||||
                    const startPos = { x: targets.listen.x - ANIM_OFFSET_X, y: targets.listen.y };
 | 
			
		||||
            win.setBounds(startPos);
 | 
			
		||||
            win.show();
 | 
			
		||||
                    fadeWindow(win, 0, 1);
 | 
			
		||||
                    movementManager.animateWindow(win, targets.listen.x, targets.listen.y);
 | 
			
		||||
 | 
			
		||||
            movementManager.fade(win, { to: 1 });
 | 
			
		||||
            movementManager.animateLayout(targetLayout);
 | 
			
		||||
 | 
			
		||||
        } else {
 | 
			
		||||
                    const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: true });
 | 
			
		||||
                    if (!targets.listen || !targets.ask) return;
 | 
			
		||||
            if (!win || !win.isVisible()) return;
 | 
			
		||||
 | 
			
		||||
                    const startListenPos = { x: targets.listen.x - ANIM_OFFSET_X, y: targets.listen.y };
 | 
			
		||||
                    win.setBounds(startListenPos);
 | 
			
		||||
 | 
			
		||||
                    win.show();
 | 
			
		||||
                    fadeWindow(win, 0, 1);
 | 
			
		||||
                    movementManager.animateWindow(otherWin, targets.ask.x, targets.ask.y);
 | 
			
		||||
                    movementManager.animateWindow(win, targets.listen.x, targets.listen.y);
 | 
			
		||||
                }
 | 
			
		||||
            } else if (name === 'ask') {
 | 
			
		||||
                if (!isOtherWinVisible) {
 | 
			
		||||
                    const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: false, ask: true });
 | 
			
		||||
                    if (!targets.ask) return;
 | 
			
		||||
 | 
			
		||||
                    const startPos = { x: targets.ask.x, y: targets.ask.y - ANIM_OFFSET_Y };
 | 
			
		||||
                    win.setBounds(startPos);
 | 
			
		||||
                    win.show();
 | 
			
		||||
                    fadeWindow(win, 0, 1);
 | 
			
		||||
                    movementManager.animateWindow(win, targets.ask.x, targets.ask.y);
 | 
			
		||||
 | 
			
		||||
                } else {
 | 
			
		||||
                    const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: true });
 | 
			
		||||
                    if (!targets.listen || !targets.ask) return;
 | 
			
		||||
 | 
			
		||||
                    const startAskPos = { x: targets.ask.x, y: targets.ask.y - ANIM_OFFSET_Y };
 | 
			
		||||
                    win.setBounds(startAskPos);
 | 
			
		||||
 | 
			
		||||
                    win.show();
 | 
			
		||||
                    fadeWindow(win, 0, 1);
 | 
			
		||||
                    movementManager.animateWindow(otherWin, targets.listen.x, targets.listen.y);
 | 
			
		||||
                    movementManager.animateWindow(win, targets.ask.x, targets.ask.y);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            const currentBounds = win.getBounds();
 | 
			
		||||
            fadeWindow(
 | 
			
		||||
                win, 1, 0, FADE_DURATION,
 | 
			
		||||
                () => win.hide()
 | 
			
		||||
            );
 | 
			
		||||
            if (name === 'listen') {
 | 
			
		||||
                if (!isOtherWinVisible) {
 | 
			
		||||
                    const targetX = currentBounds.x - ANIM_OFFSET_X;
 | 
			
		||||
                    movementManager.animateWindow(win, targetX, currentBounds.y);
 | 
			
		||||
                } else {
 | 
			
		||||
                    const targetX = currentBounds.x - currentBounds.width;
 | 
			
		||||
                    movementManager.animateWindow(win, targetX, currentBounds.y);
 | 
			
		||||
                }
 | 
			
		||||
            } else if (name === 'ask') {
 | 
			
		||||
                if (!isOtherWinVisible) {
 | 
			
		||||
                    const targetY = currentBounds.y - ANIM_OFFSET_Y;
 | 
			
		||||
                    movementManager.animateWindow(win, currentBounds.x, targetY);
 | 
			
		||||
                } else {
 | 
			
		||||
                    const targetAskY = currentBounds.y - ANIM_OFFSET_Y;
 | 
			
		||||
                    movementManager.animateWindow(win, currentBounds.x, targetAskY);
 | 
			
		||||
            const targetPos = { ...currentBounds };
 | 
			
		||||
            if (name === 'listen') targetPos.x -= ANIM_OFFSET_X;
 | 
			
		||||
            else if (name === 'ask') targetPos.y -= ANIM_OFFSET_Y;
 | 
			
		||||
 | 
			
		||||
                    const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: false });
 | 
			
		||||
                    if (targets.listen) {
 | 
			
		||||
                        movementManager.animateWindow(otherWin, targets.listen.x, targets.listen.y);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            movementManager.fade(win, { to: 0, onComplete: () => win.hide() });
 | 
			
		||||
            movementManager.animateWindowPosition(win, targetPos);
 | 
			
		||||
            
 | 
			
		||||
            // 다른 창들도 새 레이아웃으로 애니메이션
 | 
			
		||||
            const otherWindowsLayout = { ...targetLayout };
 | 
			
		||||
            delete otherWindowsLayout[name];
 | 
			
		||||
            movementManager.animateLayout(otherWindowsLayout);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -350,52 +422,6 @@ const toggleContentProtection = () => {
 | 
			
		||||
    return newStatus;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const resizeHeaderWindow = ({ width, height }) => {
 | 
			
		||||
    const header = windowPool.get('header');
 | 
			
		||||
    if (header) {
 | 
			
		||||
      console.log(`[WindowManager] Resize request: ${width}x${height}`);
 | 
			
		||||
      
 | 
			
		||||
      if (movementManager && movementManager.isAnimating) {
 | 
			
		||||
        console.log('[WindowManager] Skipping resize during animation');
 | 
			
		||||
        return { success: false, error: 'Cannot resize during animation' };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const currentBounds = header.getBounds();
 | 
			
		||||
      console.log(`[WindowManager] Current bounds: ${currentBounds.width}x${currentBounds.height} at (${currentBounds.x}, ${currentBounds.y})`);
 | 
			
		||||
      
 | 
			
		||||
      if (currentBounds.width === width && currentBounds.height === height) {
 | 
			
		||||
        console.log('[WindowManager] Already at target size, skipping resize');
 | 
			
		||||
        return { success: true };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const wasResizable = header.isResizable();
 | 
			
		||||
      if (!wasResizable) {
 | 
			
		||||
        header.setResizable(true);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const centerX = currentBounds.x + currentBounds.width / 2;
 | 
			
		||||
      const newX = Math.round(centerX - width / 2);
 | 
			
		||||
 | 
			
		||||
      const display = getCurrentDisplay(header);
 | 
			
		||||
      const { x: workAreaX, width: workAreaWidth } = display.workArea;
 | 
			
		||||
      
 | 
			
		||||
      const clampedX = Math.max(workAreaX, Math.min(workAreaX + workAreaWidth - width, newX));
 | 
			
		||||
 | 
			
		||||
      header.setBounds({ x: clampedX, y: currentBounds.y, width, height });
 | 
			
		||||
 | 
			
		||||
      if (!wasResizable) {
 | 
			
		||||
        header.setResizable(false);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      if (updateLayout) {
 | 
			
		||||
        updateLayout();
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      return { success: true };
 | 
			
		||||
    }
 | 
			
		||||
    return { success: false, error: 'Header window not found' };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const openLoginPage = () => {
 | 
			
		||||
    const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000';
 | 
			
		||||
@ -404,12 +430,6 @@ const openLoginPage = () => {
 | 
			
		||||
    console.log('Opening personalization page:', personalizeUrl);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const moveWindowStep = (direction) => {
 | 
			
		||||
    if (movementManager) {
 | 
			
		||||
        movementManager.moveStep(direction);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function createFeatureWindows(header, namesToCreate) {
 | 
			
		||||
    // if (windowPool.has('listen')) return;
 | 
			
		||||
@ -423,7 +443,7 @@ function createFeatureWindows(header, namesToCreate) {
 | 
			
		||||
        hasShadow: false,
 | 
			
		||||
        skipTaskbar: true,
 | 
			
		||||
        hiddenInMissionControl: true,
 | 
			
		||||
        resizable: true,
 | 
			
		||||
        resizable: false,
 | 
			
		||||
        webPreferences: {
 | 
			
		||||
            nodeIntegration: false,
 | 
			
		||||
            contextIsolation: true,
 | 
			
		||||
@ -614,23 +634,17 @@ function getCurrentDisplay(window) {
 | 
			
		||||
    return screen.getDisplayNearestPoint(windowCenter);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getDisplayById(displayId) {
 | 
			
		||||
    const displays = screen.getAllDisplays();
 | 
			
		||||
    return displays.find(d => d.id === displayId) || screen.getPrimaryDisplay();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function createWindows() {
 | 
			
		||||
    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;
 | 
			
		||||
    movementManager = new SmoothMovementManager(windowPool, getDisplayById, getCurrentDisplay, updateLayout);
 | 
			
		||||
        
 | 
			
		||||
    const header = new BrowserWindow({
 | 
			
		||||
        width: DEFAULT_WINDOW_WIDTH,
 | 
			
		||||
@ -681,15 +695,23 @@ function createWindows() {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    windowPool.set('header', header);
 | 
			
		||||
    header.on('moved', updateLayout);
 | 
			
		||||
    layoutManager = new WindowLayoutManager(windowPool);
 | 
			
		||||
    movementManager = new SmoothMovementManager(windowPool);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    header.on('moved', () => {
 | 
			
		||||
        if (movementManager.isAnimating) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        updateChildWindowLayouts(false);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    header.webContents.once('dom-ready', () => {
 | 
			
		||||
        shortcutsService.initialize(movementManager, windowPool);
 | 
			
		||||
        shortcutsService.initialize(windowPool);
 | 
			
		||||
        shortcutsService.registerShortcuts();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    setupIpcHandlers(movementManager);
 | 
			
		||||
    setupIpcHandlers(windowPool, layoutManager);
 | 
			
		||||
    setupWindowController(windowPool, layoutManager, movementManager);
 | 
			
		||||
 | 
			
		||||
    if (currentHeaderState === 'main') {
 | 
			
		||||
@ -721,16 +743,13 @@ function createWindows() {
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    header.on('resize', () => {
 | 
			
		||||
        console.log('[WindowManager] Header resize event triggered');
 | 
			
		||||
        updateLayout();
 | 
			
		||||
    });
 | 
			
		||||
    header.on('resize', () => updateChildWindowLayouts(false));
 | 
			
		||||
 | 
			
		||||
    return windowPool;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setupIpcHandlers(movementManager) {
 | 
			
		||||
    // quit-application handler moved to windowBridge.js to avoid duplication
 | 
			
		||||
 | 
			
		||||
function setupIpcHandlers(windowPool, layoutManager) {
 | 
			
		||||
    screen.on('display-added', (event, newDisplay) => {
 | 
			
		||||
        console.log('[Display] New display added:', newDisplay.id);
 | 
			
		||||
    });
 | 
			
		||||
@ -738,18 +757,25 @@ function setupIpcHandlers(movementManager) {
 | 
			
		||||
    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);
 | 
			
		||||
            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) => {
 | 
			
		||||
        // console.log('[Display] Display metrics changed:', display.id, changedMetrics);
 | 
			
		||||
        updateLayout();
 | 
			
		||||
        // 레이아웃 업데이트 함수를 새 버전으로 호출
 | 
			
		||||
        updateChildWindowLayouts(false);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const handleHeaderStateChanged = (state) => {
 | 
			
		||||
    console.log(`[WindowManager] Header state changed to: ${state}`);
 | 
			
		||||
    currentHeaderState = state;
 | 
			
		||||
@ -762,96 +788,8 @@ const handleHeaderStateChanged = (state) => {
 | 
			
		||||
    internalBridge.emit('reregister-shortcuts');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleHeaderAnimationFinished = (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();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getHeaderPosition = () => {
 | 
			
		||||
    const header = windowPool.get('header');
 | 
			
		||||
    if (header) {
 | 
			
		||||
        const [x, y] = header.getPosition();
 | 
			
		||||
        return { x, y };
 | 
			
		||||
    }
 | 
			
		||||
    return { x: 0, y: 0 };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const moveHeader = (newX, newY) => {
 | 
			
		||||
    const header = windowPool.get('header');
 | 
			
		||||
    if (header) {
 | 
			
		||||
        const currentY = newY !== undefined ? newY : header.getBounds().y;
 | 
			
		||||
        header.setPosition(newX, currentY, false);
 | 
			
		||||
        updateLayout();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const moveHeaderTo = (newX, newY) => {
 | 
			
		||||
    const header = windowPool.get('header');
 | 
			
		||||
    if (header) {
 | 
			
		||||
        const targetDisplay = screen.getDisplayNearestPoint({ x: newX, y: newY });
 | 
			
		||||
        const { x: workAreaX, y: workAreaY, width, height } = targetDisplay.workArea;
 | 
			
		||||
        const headerBounds = header.getBounds();
 | 
			
		||||
 | 
			
		||||
        let clampedX = newX;
 | 
			
		||||
        let clampedY = newY;
 | 
			
		||||
        
 | 
			
		||||
        if (newX < workAreaX) {
 | 
			
		||||
            clampedX = workAreaX;
 | 
			
		||||
        } else if (newX + headerBounds.width > workAreaX + width) {
 | 
			
		||||
            clampedX = workAreaX + width - headerBounds.width;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        if (newY < workAreaY) {
 | 
			
		||||
            clampedY = workAreaY;
 | 
			
		||||
        } else if (newY + headerBounds.height > workAreaY + height) {
 | 
			
		||||
            clampedY = workAreaY + height - headerBounds.height;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        header.setPosition(clampedX, clampedY, false);
 | 
			
		||||
        updateLayout();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const adjustWindowHeight = (sender, targetHeight) => {
 | 
			
		||||
    const senderWindow = BrowserWindow.fromWebContents(sender);
 | 
			
		||||
    if (senderWindow) {
 | 
			
		||||
        const wasResizable = senderWindow.isResizable();
 | 
			
		||||
        if (!wasResizable) {
 | 
			
		||||
            senderWindow.setResizable(true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const currentBounds = senderWindow.getBounds();
 | 
			
		||||
        const minHeight = senderWindow.getMinimumSize()[1];
 | 
			
		||||
        const maxHeight = senderWindow.getMaximumSize()[1];
 | 
			
		||||
        
 | 
			
		||||
        let adjustedHeight;
 | 
			
		||||
        if (maxHeight === 0) {
 | 
			
		||||
            adjustedHeight = Math.max(minHeight, targetHeight);
 | 
			
		||||
        } else {
 | 
			
		||||
            adjustedHeight = Math.max(minHeight, Math.min(maxHeight, targetHeight));
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        senderWindow.setSize(currentBounds.width, adjustedHeight, false);
 | 
			
		||||
 | 
			
		||||
        if (!wasResizable) {
 | 
			
		||||
            senderWindow.setResizable(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        updateLayout();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    updateLayout,
 | 
			
		||||
    createWindows,
 | 
			
		||||
    windowPool,
 | 
			
		||||
    toggleContentProtection,
 | 
			
		||||
@ -865,7 +803,6 @@ module.exports = {
 | 
			
		||||
    handleHeaderStateChanged,
 | 
			
		||||
    handleHeaderAnimationFinished,
 | 
			
		||||
    getHeaderPosition,
 | 
			
		||||
    moveHeader,
 | 
			
		||||
    moveHeaderTo,
 | 
			
		||||
    adjustWindowHeight,
 | 
			
		||||
};
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user