fix window animation
This commit is contained in:
		
							parent
							
								
									7d33ea9ca8
								
							
						
					
					
						commit
						8da13dcb27
					
				
							
								
								
									
										2
									
								
								aec
									
									
									
									
									
								
							
							
								
								
								
								
								
								
									
									
								
							
						
						
									
										2
									
								
								aec
									
									
									
									
									
								
							@ -1 +1 @@
 | 
				
			|||||||
Subproject commit f00bb1fb948053c752b916adfee19f90644a0b2f
 | 
					Subproject commit 9e11f4f95707714464194bdfc9db0222ec5c6163
 | 
				
			||||||
@ -76,7 +76,8 @@ module.exports = {
 | 
				
			|||||||
    ipcMain.handle('ask:sendQuestionFromAsk', async (event, userPrompt) => await askService.sendMessage(userPrompt));
 | 
					    ipcMain.handle('ask:sendQuestionFromAsk', async (event, userPrompt) => await askService.sendMessage(userPrompt));
 | 
				
			||||||
    ipcMain.handle('ask:sendQuestionFromSummary', async (event, userPrompt) => await askService.sendMessage(userPrompt));
 | 
					    ipcMain.handle('ask:sendQuestionFromSummary', async (event, userPrompt) => await askService.sendMessage(userPrompt));
 | 
				
			||||||
    ipcMain.handle('ask:toggleAskButton', async () => await askService.toggleAskButton());
 | 
					    ipcMain.handle('ask:toggleAskButton', async () => await askService.toggleAskButton());
 | 
				
			||||||
 | 
					    ipcMain.handle('ask:closeAskWindow',  async () => await askService.closeAskWindow());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    // Listen
 | 
					    // Listen
 | 
				
			||||||
    ipcMain.handle('listen:sendMicAudio', async (event, { data, mimeType }) => await listenService.handleSendMicAudioContent(data, mimeType));
 | 
					    ipcMain.handle('listen:sendMicAudio', async (event, { data, mimeType }) => await listenService.handleSendMicAudioContent(data, mimeType));
 | 
				
			||||||
    ipcMain.handle('listen:sendSystemAudio', async (event, { data, mimeType }) => {
 | 
					    ipcMain.handle('listen:sendSystemAudio', async (event, { data, mimeType }) => {
 | 
				
			||||||
 | 
				
			|||||||
@ -24,7 +24,7 @@ module.exports = {
 | 
				
			|||||||
    ipcMain.handle('adjust-window-height', (event, targetHeight) => windowManager.adjustWindowHeight(event.sender, targetHeight));
 | 
					    ipcMain.handle('adjust-window-height', (event, targetHeight) => windowManager.adjustWindowHeight(event.sender, targetHeight));
 | 
				
			||||||
    ipcMain.handle('toggle-all-windows-visibility', () => windowManager.toggleAllWindowsVisibility());
 | 
					    ipcMain.handle('toggle-all-windows-visibility', () => windowManager.toggleAllWindowsVisibility());
 | 
				
			||||||
    ipcMain.on('animation-finished', (event) => windowManager.handleAnimationFinished(event.sender));
 | 
					    ipcMain.on('animation-finished', (event) => windowManager.handleAnimationFinished(event.sender));
 | 
				
			||||||
    ipcMain.handle('ask:closeAskWindow', () => windowManager.closeAskWindow());
 | 
					    // ipcMain.handle('ask:closeAskWindow', () => windowManager.closeAskWindow());
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  notifyFocusChange(win, isFocused) {
 | 
					  notifyFocusChange(win, isFocused) {
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,8 @@ const { BrowserWindow } = require('electron');
 | 
				
			|||||||
const { createStreamingLLM } = require('../common/ai/factory');
 | 
					const { createStreamingLLM } = require('../common/ai/factory');
 | 
				
			||||||
// Lazy require helper to avoid circular dependency issues
 | 
					// Lazy require helper to avoid circular dependency issues
 | 
				
			||||||
const getWindowManager = () => require('../../window/windowManager');
 | 
					const getWindowManager = () => require('../../window/windowManager');
 | 
				
			||||||
 | 
					const internalBridge = require('../../bridge/internalBridge');
 | 
				
			||||||
 | 
					const { EVENTS } = internalBridge;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getWindowPool = () => {
 | 
					const getWindowPool = () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
@ -10,8 +12,6 @@ const getWindowPool = () => {
 | 
				
			|||||||
        return null;
 | 
					        return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
const updateLayout = () => getWindowManager().updateLayout();
 | 
					 | 
				
			||||||
const ensureAskWindowVisible = () => getWindowManager().ensureAskWindowVisible();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const sessionRepository = require('../common/repositories/session');
 | 
					const sessionRepository = require('../common/repositories/session');
 | 
				
			||||||
const askRepository = require('./repositories');
 | 
					const askRepository = require('./repositories');
 | 
				
			||||||
@ -148,32 +148,47 @@ class AskService {
 | 
				
			|||||||
    async toggleAskButton() {
 | 
					    async toggleAskButton() {
 | 
				
			||||||
        const askWindow = getWindowPool()?.get('ask');
 | 
					        const askWindow = getWindowPool()?.get('ask');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // 답변이 있거나 스트리밍 중일 때
 | 
					 | 
				
			||||||
        const hasContent = this.state.isStreaming || (this.state.currentResponse && this.state.currentResponse.length > 0);
 | 
					        const hasContent = this.state.isStreaming || (this.state.currentResponse && this.state.currentResponse.length > 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (askWindow && askWindow.isVisible() && hasContent) {
 | 
					        if (askWindow && askWindow.isVisible() && hasContent) {
 | 
				
			||||||
            // 창을 닫는 대신, 텍스트 입력창만 토글합니다.
 | 
					 | 
				
			||||||
            this.state.showTextInput = !this.state.showTextInput;
 | 
					            this.state.showTextInput = !this.state.showTextInput;
 | 
				
			||||||
            this._broadcastState(); // 변경된 상태 전파
 | 
					            this._broadcastState();
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            // 기존의 창 보이기/숨기기 로직
 | 
					 | 
				
			||||||
            if (askWindow && askWindow.isVisible()) {
 | 
					            if (askWindow && askWindow.isVisible()) {
 | 
				
			||||||
                askWindow.webContents.send('window-hide-animation');
 | 
					                internalBridge.emit('request-window-visibility', { name: 'ask', visible: false });
 | 
				
			||||||
                this.state.isVisible = false;
 | 
					                this.state.isVisible = false;
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                console.log('[AskService] Showing hidden Ask window');
 | 
					                console.log('[AskService] Showing hidden Ask window');
 | 
				
			||||||
 | 
					                internalBridge.emit('request-window-visibility', { name: 'ask', visible: true });
 | 
				
			||||||
                this.state.isVisible = true;
 | 
					                this.state.isVisible = true;
 | 
				
			||||||
                askWindow?.show();
 | 
					 | 
				
			||||||
                updateLayout();
 | 
					 | 
				
			||||||
                askWindow?.webContents.send('window-show-animation');
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            // 창이 다시 열릴 때를 대비해 상태를 초기화하고 전파합니다.
 | 
					 | 
				
			||||||
            if (this.state.isVisible) {
 | 
					            if (this.state.isVisible) {
 | 
				
			||||||
                this.state.showTextInput = true;
 | 
					                this.state.showTextInput = true;
 | 
				
			||||||
                this._broadcastState();
 | 
					                this._broadcastState();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async closeAskWindow () {
 | 
				
			||||||
 | 
					            if (this.abortController) {
 | 
				
			||||||
 | 
					                this.abortController.abort('Window closed by user');
 | 
				
			||||||
 | 
					                this.abortController = null;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					            this.state = {
 | 
				
			||||||
 | 
					                isVisible      : false,
 | 
				
			||||||
 | 
					                isLoading      : false,
 | 
				
			||||||
 | 
					                isStreaming    : false,
 | 
				
			||||||
 | 
					                currentQuestion: '',
 | 
				
			||||||
 | 
					                currentResponse: '',
 | 
				
			||||||
 | 
					                showTextInput  : true,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            this._broadcastState();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					            internalBridge.emit('request-window-visibility', { name: 'ask', visible: false });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					            return { success: true };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -195,7 +210,7 @@ class AskService {
 | 
				
			|||||||
     * @returns {Promise<{success: boolean, response?: string, error?: string}>}
 | 
					     * @returns {Promise<{success: boolean, response?: string, error?: string}>}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    async sendMessage(userPrompt, conversationHistoryRaw=[]) {
 | 
					    async sendMessage(userPrompt, conversationHistoryRaw=[]) {
 | 
				
			||||||
        ensureAskWindowVisible();
 | 
					        // ensureAskWindowVisible();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (this.abortController) {
 | 
					        if (this.abortController) {
 | 
				
			||||||
            this.abortController.abort('New request received.');
 | 
					            this.abortController.abort('New request received.');
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,8 @@ const SummaryService = require('./summary/summaryService');
 | 
				
			|||||||
const authService = require('../common/services/authService');
 | 
					const authService = require('../common/services/authService');
 | 
				
			||||||
const sessionRepository = require('../common/repositories/session');
 | 
					const sessionRepository = require('../common/repositories/session');
 | 
				
			||||||
const sttRepository = require('./stt/repositories');
 | 
					const sttRepository = require('./stt/repositories');
 | 
				
			||||||
 | 
					const internalBridge = require('../../bridge/internalBridge');
 | 
				
			||||||
 | 
					const { EVENTS } = internalBridge;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ListenService {
 | 
					class ListenService {
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
@ -60,9 +62,7 @@ class ListenService {
 | 
				
			|||||||
            switch (listenButtonText) {
 | 
					            switch (listenButtonText) {
 | 
				
			||||||
                case 'Listen':
 | 
					                case 'Listen':
 | 
				
			||||||
                    console.log('[ListenService] changeSession to "Listen"');
 | 
					                    console.log('[ListenService] changeSession to "Listen"');
 | 
				
			||||||
                    listenWindow.show();
 | 
					                    internalBridge.emit('request-window-visibility', { name: 'listen', visible: true });
 | 
				
			||||||
                    updateLayout();
 | 
					 | 
				
			||||||
                    listenWindow.webContents.send('window-show-animation');
 | 
					 | 
				
			||||||
                    await this.initializeSession();
 | 
					                    await this.initializeSession();
 | 
				
			||||||
                    listenWindow.webContents.send('session-state-changed', { isActive: true });
 | 
					                    listenWindow.webContents.send('session-state-changed', { isActive: true });
 | 
				
			||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
@ -75,7 +75,7 @@ class ListenService {
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
                case 'Done':
 | 
					                case 'Done':
 | 
				
			||||||
                    console.log('[ListenService] changeSession to "Done"');
 | 
					                    console.log('[ListenService] changeSession to "Done"');
 | 
				
			||||||
                    listenWindow.webContents.send('window-hide-animation');
 | 
					                    internalBridge.emit('request-window-visibility', { name: 'listen', visible: false });
 | 
				
			||||||
                    listenWindow.webContents.send('session-state-changed', { isActive: false });
 | 
					                    listenWindow.webContents.send('session-state-changed', { isActive: false });
 | 
				
			||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
 | 
				
			|||||||
@ -256,16 +256,8 @@ contextBridge.exposeInMainWorld('api', {
 | 
				
			|||||||
    sendAnimationFinished: () => ipcRenderer.send('animation-finished'),
 | 
					    sendAnimationFinished: () => ipcRenderer.send('animation-finished'),
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Listeners
 | 
					    // Listeners
 | 
				
			||||||
    onWindowShowAnimation: (callback) => ipcRenderer.on('window-show-animation', callback),
 | 
					 | 
				
			||||||
    removeOnWindowShowAnimation: (callback) => ipcRenderer.removeListener('window-show-animation', callback),
 | 
					 | 
				
			||||||
    onWindowHideAnimation: (callback) => ipcRenderer.on('window-hide-animation', callback),
 | 
					 | 
				
			||||||
    removeOnWindowHideAnimation: (callback) => ipcRenderer.removeListener('window-hide-animation', callback),
 | 
					 | 
				
			||||||
    onSettingsWindowHideAnimation: (callback) => ipcRenderer.on('settings-window-hide-animation', callback),
 | 
					    onSettingsWindowHideAnimation: (callback) => ipcRenderer.on('settings-window-hide-animation', callback),
 | 
				
			||||||
    removeOnSettingsWindowHideAnimation: (callback) => ipcRenderer.removeListener('settings-window-hide-animation', callback),
 | 
					    removeOnSettingsWindowHideAnimation: (callback) => ipcRenderer.removeListener('settings-window-hide-animation', callback),    
 | 
				
			||||||
    onListenWindowMoveToCenter: (callback) => ipcRenderer.on('listen-window-move-to-center', callback),
 | 
					 | 
				
			||||||
    removeOnListenWindowMoveToCenter: (callback) => ipcRenderer.removeListener('listen-window-move-to-center', callback),
 | 
					 | 
				
			||||||
    onListenWindowMoveToLeft: (callback) => ipcRenderer.on('listen-window-move-to-left', callback),
 | 
					 | 
				
			||||||
    removeOnListenWindowMoveToLeft: (callback) => ipcRenderer.removeListener('listen-window-move-to-left', callback)
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // src/ui/listen/audioCore/listenCapture.js
 | 
					  // src/ui/listen/audioCore/listenCapture.js
 | 
				
			||||||
 | 
				
			|||||||
@ -99,62 +99,6 @@
 | 
				
			|||||||
                transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.25s ease-out;
 | 
					                transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.25s ease-out;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            .window-sliding-down {
 | 
					 | 
				
			||||||
                animation: slideDownFromHeader 0.12s cubic-bezier(0.23, 1, 0.32, 1) forwards;
 | 
					 | 
				
			||||||
                will-change: transform, opacity;
 | 
					 | 
				
			||||||
                transform-style: preserve-3d;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            .window-sliding-up {
 | 
					 | 
				
			||||||
                animation: slideUpToHeader 0.10s cubic-bezier(0.55, 0.085, 0.68, 0.53) forwards;
 | 
					 | 
				
			||||||
                will-change: transform, opacity;
 | 
					 | 
				
			||||||
                transform-style: preserve-3d;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            .window-hidden {
 | 
					 | 
				
			||||||
                opacity: 0;
 | 
					 | 
				
			||||||
                transform: translate3d(0, -18px, 0) scale3d(0.96, 0.96, 1);
 | 
					 | 
				
			||||||
                pointer-events: none;
 | 
					 | 
				
			||||||
                will-change: auto;
 | 
					 | 
				
			||||||
                contain: layout style paint;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            .listen-window-moving {
 | 
					 | 
				
			||||||
                transition: transform 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
 | 
					 | 
				
			||||||
                will-change: transform;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            .listen-window-center {
 | 
					 | 
				
			||||||
                transform: translate3d(0, 0, 0);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            .listen-window-left {
 | 
					 | 
				
			||||||
                transform: translate3d(-110px, 0, 0);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            @keyframes slideDownFromHeader {
 | 
					 | 
				
			||||||
                0% {
 | 
					 | 
				
			||||||
                    opacity: 0;
 | 
					 | 
				
			||||||
                    transform: translate3d(0, -18px, 0) scale3d(0.96, 0.96, 1);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                25% {
 | 
					 | 
				
			||||||
                    opacity: 0.4;
 | 
					 | 
				
			||||||
                    transform: translate3d(0, -10px, 0) scale3d(0.98, 0.98, 1);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                50% {
 | 
					 | 
				
			||||||
                    opacity: 0.7;
 | 
					 | 
				
			||||||
                    transform: translate3d(0, -3px, 0) scale3d(1.01, 1.01, 1);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                75% {
 | 
					 | 
				
			||||||
                    opacity: 0.9;
 | 
					 | 
				
			||||||
                    transform: translate3d(0, -0.5px, 0) scale3d(1.005, 1.005, 1);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                100% {
 | 
					 | 
				
			||||||
                    opacity: 1;
 | 
					 | 
				
			||||||
                    transform: translate3d(0, 0, 0) scale3d(1, 1, 1);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            .settings-window-show {
 | 
					            .settings-window-show {
 | 
				
			||||||
                animation: settingsPopFromButton 0.12s cubic-bezier(0.23, 1, 0.32, 1) forwards;
 | 
					                animation: settingsPopFromButton 0.12s cubic-bezier(0.23, 1, 0.32, 1) forwards;
 | 
				
			||||||
                transform-origin: 85% 0%;
 | 
					                transform-origin: 85% 0%;
 | 
				
			||||||
@ -206,25 +150,6 @@
 | 
				
			|||||||
                    transform: translate3d(0, -8px, 0) scale3d(0.5, 0.5, 1);
 | 
					                    transform: translate3d(0, -8px, 0) scale3d(0.5, 0.5, 1);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					 | 
				
			||||||
            @keyframes slideUpToHeader {
 | 
					 | 
				
			||||||
                0% {
 | 
					 | 
				
			||||||
                    opacity: 1;
 | 
					 | 
				
			||||||
                    transform: translate3d(0, 0, 0) scale3d(1, 1, 1);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                30% {
 | 
					 | 
				
			||||||
                    opacity: 0.6;
 | 
					 | 
				
			||||||
                    transform: translate3d(0, -6px, 0) scale3d(0.98, 0.98, 1);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                65% {
 | 
					 | 
				
			||||||
                    opacity: 0.2;
 | 
					 | 
				
			||||||
                    transform: translate3d(0, -14px, 0) scale3d(0.95, 0.95, 1);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                100% {
 | 
					 | 
				
			||||||
                    opacity: 0;
 | 
					 | 
				
			||||||
                    transform: translate3d(0, -18px, 0) scale3d(0.93, 0.93, 1);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        </style>
 | 
					        </style>
 | 
				
			||||||
    </head>
 | 
					    </head>
 | 
				
			||||||
    <body>
 | 
					    <body>
 | 
				
			||||||
@ -239,62 +164,24 @@
 | 
				
			|||||||
                const app = document.getElementById('pickle-glass');
 | 
					                const app = document.getElementById('pickle-glass');
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
                if (window.api) {
 | 
					                if (window.api) {
 | 
				
			||||||
                    // --- REFACTORED: Event-driven animation handling ---
 | 
					 | 
				
			||||||
                    app.addEventListener('animationend', (event) => {
 | 
					                    app.addEventListener('animationend', (event) => {
 | 
				
			||||||
                        // 숨김 애니메이션이 끝나면 main 프로세스에 알려 창을 실제로 숨깁니다.
 | 
					                        if (event.animationName === 'settingsCollapseToButton') {
 | 
				
			||||||
                        if (event.animationName === 'slideUpToHeader' || event.animationName === 'settingsCollapseToButton') {
 | 
					                            console.log('Settings hide animation finished. Notifying main process.');
 | 
				
			||||||
                            console.log(`Animation finished: ${event.animationName}. Notifying main process.`);
 | 
					 | 
				
			||||||
                            window.api.content.sendAnimationFinished();
 | 
					                            window.api.content.sendAnimationFinished();
 | 
				
			||||||
        
 | 
					                            
 | 
				
			||||||
                            // 완료 후 애니메이션 클래스 정리
 | 
					                            app.classList.remove('settings-window-hide');
 | 
				
			||||||
                            app.classList.remove('window-sliding-up', 'settings-window-hide');
 | 
					                            app.classList.add('hidden');
 | 
				
			||||||
                            app.classList.add('window-hidden');
 | 
					                        } 
 | 
				
			||||||
                        } else if (event.animationName === 'slideDownFromHeader' || event.animationName === 'settingsPopFromButton') {
 | 
					                        else if (event.animationName === 'settingsPopFromButton') {
 | 
				
			||||||
                             // 보이기 애니메이션 완료 후 클래스 정리
 | 
					                            app.classList.remove('settings-window-show');
 | 
				
			||||||
                            app.classList.remove('window-sliding-down', 'settings-window-show');
 | 
					 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
                    window.api.content.onWindowShowAnimation(() => {
 | 
					 | 
				
			||||||
                        console.log('Starting window show animation');
 | 
					 | 
				
			||||||
                        app.classList.remove('window-hidden', 'window-sliding-up', 'settings-window-hide');
 | 
					 | 
				
			||||||
                        app.classList.add('window-sliding-down');
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
                    window.api.content.onWindowHideAnimation(() => {
 | 
					 | 
				
			||||||
                        console.log('Starting window hide animation');
 | 
					 | 
				
			||||||
                        app.classList.remove('window-sliding-down', 'settings-window-show');
 | 
					 | 
				
			||||||
                        app.classList.add('window-sliding-up');
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
                    window.api.content.onSettingsWindowHideAnimation(() => {
 | 
					                    window.api.content.onSettingsWindowHideAnimation(() => {
 | 
				
			||||||
                        console.log('Starting settings window hide animation');
 | 
					                        console.log('Starting settings window hide animation');
 | 
				
			||||||
                        app.classList.remove('window-sliding-down', 'settings-window-show');
 | 
					                        app.classList.remove('settings-window-show');
 | 
				
			||||||
                        app.classList.add('settings-window-hide');
 | 
					                        app.classList.add('settings-window-hide');
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
        
 | 
					 | 
				
			||||||
                    // --- UNCHANGED: Existing logic for listen window movement ---
 | 
					 | 
				
			||||||
                    window.api.content.onListenWindowMoveToCenter(() => {
 | 
					 | 
				
			||||||
                        console.log('Moving listen window to center');
 | 
					 | 
				
			||||||
                        app.classList.add('listen-window-moving');
 | 
					 | 
				
			||||||
                        app.classList.remove('listen-window-left');
 | 
					 | 
				
			||||||
                        app.classList.add('listen-window-center');
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
                        setTimeout(() => {
 | 
					 | 
				
			||||||
                            app.classList.remove('listen-window-moving');
 | 
					 | 
				
			||||||
                        }, 350);
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
                    window.api.content.onListenWindowMoveToLeft(() => {
 | 
					 | 
				
			||||||
                        console.log('Moving listen window to left');
 | 
					 | 
				
			||||||
                        app.classList.add('listen-window-moving');
 | 
					 | 
				
			||||||
                        app.classList.remove('listen-window-center');
 | 
					 | 
				
			||||||
                        app.classList.add('listen-window-left');
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
                        setTimeout(() => {
 | 
					 | 
				
			||||||
                            app.classList.remove('listen-window-moving');
 | 
					 | 
				
			||||||
                        }, 350);
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        </script>
 | 
					        </script>
 | 
				
			||||||
 | 
				
			|||||||
@ -878,7 +878,7 @@ export class AskView extends LitElement {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    handleCloseAskWindow() {
 | 
					    handleCloseAskWindow() {
 | 
				
			||||||
        this.clearResponseContent();
 | 
					        // this.clearResponseContent();
 | 
				
			||||||
        window.api.askView.closeAskWindow();
 | 
					        window.api.askView.closeAskWindow();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -170,6 +170,56 @@ class SmoothMovementManager {
 | 
				
			|||||||
        this.animateToPosition(header, clampedX, clampedY, windowSize);
 | 
					        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)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    animateWindow(win, targetX, targetY, options = {}) {
 | 
				
			||||||
 | 
					        if (!this._isWindowValid(win)) {
 | 
				
			||||||
 | 
					            if (options.onComplete) options.onComplete();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const { sizeOverride, onComplete, duration: animDuration } = options;
 | 
				
			||||||
 | 
					        const start = win.getBounds();
 | 
				
			||||||
 | 
					        const startTime = Date.now();
 | 
				
			||||||
 | 
					        const duration = animDuration || this.animationDuration;
 | 
				
			||||||
 | 
					        const { width, height } = sizeOverride || start;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const step = () => {
 | 
				
			||||||
 | 
					            // 애니메이션 중간에 창이 파괴될 경우 콜백을 실행하고 중단
 | 
				
			||||||
 | 
					            if (!this._isWindowValid(win)) {
 | 
				
			||||||
 | 
					                if (onComplete) onComplete();
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const p = Math.min((Date.now() - startTime) / duration, 1);
 | 
				
			||||||
 | 
					            const eased = 1 - Math.pow(1 - p, 3); // ease-out-cubic
 | 
				
			||||||
 | 
					            const x = start.x + (targetX - start.x) * eased;
 | 
				
			||||||
 | 
					            const y = start.y + (targetY - start.y) * eased;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            win.setBounds({ x: Math.round(x), y: Math.round(y), width, height });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (p < 1) {
 | 
				
			||||||
 | 
					                setTimeout(step, 8); // requestAnimationFrame 대신 setTimeout으로 간결하게 처리
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                // 애니메이션 종료
 | 
				
			||||||
 | 
					                this.updateLayout(); // 레이아웃 재정렬
 | 
				
			||||||
 | 
					                if (onComplete) {
 | 
				
			||||||
 | 
					                    onComplete(); // 완료 콜백 실행
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        step();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    animateToPosition(header, targetX, targetY, windowSize) {
 | 
					    animateToPosition(header, targetX, targetY, windowSize) {
 | 
				
			||||||
        if (!this._isWindowValid(header)) return;
 | 
					        if (!this._isWindowValid(header)) return;
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
const { screen } = require('electron');
 | 
					const { screen } = require('electron');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 주어진 창이 현재 어느 디스플레이에 속해 있는지 반환합니다.
 | 
					 * 
 | 
				
			||||||
 * @param {BrowserWindow} window - 확인할 창 객체
 | 
					 * @param {BrowserWindow} window 
 | 
				
			||||||
 * @returns {Display} Electron의 Display 객체
 | 
					 * @returns {Display}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function getCurrentDisplay(window) {
 | 
					function getCurrentDisplay(window) {
 | 
				
			||||||
    if (!window || window.isDestroyed()) return screen.getPrimaryDisplay();
 | 
					    if (!window || window.isDestroyed()) return screen.getPrimaryDisplay();
 | 
				
			||||||
@ -27,10 +27,6 @@ class WindowLayoutManager {
 | 
				
			|||||||
        this.PADDING = 80;
 | 
					        this.PADDING = 80;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 모든 창의 레이아웃 업데이트를 요청합니다.
 | 
					 | 
				
			||||||
     * 중복 실행을 방지하기 위해 isUpdating 플래그를 사용합니다.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    updateLayout() {
 | 
					    updateLayout() {
 | 
				
			||||||
        if (this.isUpdating) return;
 | 
					        if (this.isUpdating) return;
 | 
				
			||||||
        this.isUpdating = true;
 | 
					        this.isUpdating = true;
 | 
				
			||||||
@ -42,8 +38,112 @@ class WindowLayoutManager {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 헤더 창을 기준으로 모든 기능 창들의 위치를 계산하고 배치합니다.
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param {object} [visibilityOverride] - { listen: true, ask: true }
 | 
				
			||||||
 | 
					     * @returns {{listen: {x:number, y:number}|null, ask: {x:number, y:number}|null}}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
 | 
					    getTargetBoundsForFeatureWindows(visibilityOverride = {}) {
 | 
				
			||||||
 | 
					        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 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;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					            const yPos = (strategy.primary === 'above') ?
 | 
				
			||||||
 | 
					                (headerBounds.y - workAreaY) - Math.max(askB.height, listenB.height) - PAD :
 | 
				
			||||||
 | 
					                (headerBounds.y - workAreaY) + headerBounds.height + PAD;
 | 
				
			||||||
 | 
					            const yAbs = yPos + workAreaY;
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					            result.listen = { x: Math.round(listenXRel + workAreaX), y: Math.round(yAbs) };
 | 
				
			||||||
 | 
					            result.ask = { x: Math.round(askXRel + workAreaX), y: Math.round(yAbs) };
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            const winB = askVis ? askB : listenB;
 | 
				
			||||||
 | 
					            let xRel = headerCenterXRel - winB.width / 2;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            let yPos = (strategy.primary === 'above') ?
 | 
				
			||||||
 | 
					                (headerBounds.y - workAreaY) - winB.height - PAD :
 | 
				
			||||||
 | 
					                (headerBounds.y - workAreaY) + headerBounds.height + PAD;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            xRel = Math.max(PAD, Math.min(screenWidth - winB.width - PAD, xRel));
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            const abs = { x: Math.round(xRel + workAreaX), y: Math.round(yPos + workAreaY) };
 | 
				
			||||||
 | 
					            if (askVis) result.ask = abs;
 | 
				
			||||||
 | 
					            if (listenVis) result.listen = abs;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return result;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   /**
 | 
				
			||||||
 | 
					    * 
 | 
				
			||||||
 | 
					    * @returns {{listen: {x:number, y:number}}}
 | 
				
			||||||
 | 
					    */
 | 
				
			||||||
 | 
					   getTargetBoundsForListenNextToAsk() {
 | 
				
			||||||
 | 
					       const ask = this.windowPool.get('ask');
 | 
				
			||||||
 | 
					       const listen = this.windowPool.get('listen');
 | 
				
			||||||
 | 
					       const header = this.windowPool.get('header');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       if (!ask || !listen || !header || !ask.isVisible() || ask.isDestroyed() || listen.isDestroyed()) {
 | 
				
			||||||
 | 
					           return {};
 | 
				
			||||||
 | 
					       }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       const askB = ask.getBounds();
 | 
				
			||||||
 | 
					       const listenB = listen.getBounds();
 | 
				
			||||||
 | 
					       const PAD = 8;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       const listenX = askB.x - listenB.width - PAD;
 | 
				
			||||||
 | 
					       const listenY = askB.y;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       const display = getCurrentDisplay(header);
 | 
				
			||||||
 | 
					       const { x: workAreaX } = display.workArea;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       return {
 | 
				
			||||||
 | 
					           listen: {
 | 
				
			||||||
 | 
					               x: Math.max(workAreaX + PAD, listenX),
 | 
				
			||||||
 | 
					               y: listenY
 | 
				
			||||||
 | 
					           }
 | 
				
			||||||
 | 
					       };
 | 
				
			||||||
 | 
					   }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    positionWindows() {
 | 
					    positionWindows() {
 | 
				
			||||||
        const header = this.windowPool.get('header');
 | 
					        const header = this.windowPool.get('header');
 | 
				
			||||||
        if (!header?.getBounds) return;
 | 
					        if (!header?.getBounds) return;
 | 
				
			||||||
@ -59,21 +159,24 @@ class WindowLayoutManager {
 | 
				
			|||||||
        const relativeX = headerCenterX / screenWidth;
 | 
					        const relativeX = headerCenterX / screenWidth;
 | 
				
			||||||
        const relativeY = headerCenterY / screenHeight;
 | 
					        const relativeY = headerCenterY / screenHeight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY);
 | 
					        const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY, workAreaX, workAreaY);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY);
 | 
					        this.positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY);
 | 
				
			||||||
        this.positionSettingsWindow(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY);
 | 
					        this.positionSettingsWindow(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 헤더 창의 위치에 따라 기능 창들을 배치할 최적의 전략을 결정합니다.
 | 
					     * 
 | 
				
			||||||
     * @returns {{name: string, primary: string, secondary: string}} 레이아웃 전략
 | 
					     * @returns {{name: string, primary: string, secondary: string}}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY) {
 | 
					    determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY, workAreaX, workAreaY) {
 | 
				
			||||||
        const spaceBelow = screenHeight - (headerBounds.y + headerBounds.height);
 | 
					        const headerRelX = headerBounds.x - workAreaX;
 | 
				
			||||||
        const spaceAbove = headerBounds.y;
 | 
					        const headerRelY = headerBounds.y - workAreaY;
 | 
				
			||||||
        const spaceLeft = headerBounds.x;
 | 
					
 | 
				
			||||||
        const spaceRight = screenWidth - (headerBounds.x + headerBounds.width);
 | 
					        const spaceBelow = screenHeight - (headerRelY + headerBounds.height);
 | 
				
			||||||
 | 
					        const spaceAbove = headerRelY;
 | 
				
			||||||
 | 
					        const spaceLeft = headerRelX;
 | 
				
			||||||
 | 
					        const spaceRight = screenWidth - (headerRelX + headerBounds.width);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (spaceBelow >= 400) {
 | 
					        if (spaceBelow >= 400) {
 | 
				
			||||||
            return { name: 'below', primary: 'below', secondary: relativeX < 0.5 ? 'right' : 'left' };
 | 
					            return { name: 'below', primary: 'below', secondary: relativeX < 0.5 ? 'right' : 'left' };
 | 
				
			||||||
@ -88,9 +191,7 @@ class WindowLayoutManager {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					
 | 
				
			||||||
     * 'ask'와 'listen' 창의 위치를 조정합니다.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY) {
 | 
					    positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY) {
 | 
				
			||||||
        const ask = this.windowPool.get('ask');
 | 
					        const ask = this.windowPool.get('ask');
 | 
				
			||||||
        const listen = this.windowPool.get('listen');
 | 
					        const listen = this.windowPool.get('listen');
 | 
				
			||||||
@ -105,10 +206,8 @@ class WindowLayoutManager {
 | 
				
			|||||||
        let listenBounds = listenVisible ? listen.getBounds() : null;
 | 
					        let listenBounds = listenVisible ? listen.getBounds() : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (askVisible && listenVisible) {
 | 
					        if (askVisible && listenVisible) {
 | 
				
			||||||
            const combinedWidth = listenBounds.width + PAD + askBounds.width;
 | 
					            let askXRel = headerCenterXRel - (askBounds.width / 2);
 | 
				
			||||||
            let groupStartXRel = headerCenterXRel - combinedWidth / 2;
 | 
					            let listenXRel = askXRel - listenBounds.width - PAD;
 | 
				
			||||||
            let listenXRel = groupStartXRel;
 | 
					 | 
				
			||||||
            let askXRel = groupStartXRel + listenBounds.width + PAD;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (listenXRel < PAD) {
 | 
					            if (listenXRel < PAD) {
 | 
				
			||||||
                listenXRel = PAD;
 | 
					                listenXRel = PAD;
 | 
				
			||||||
@ -119,30 +218,28 @@ class WindowLayoutManager {
 | 
				
			|||||||
                listenXRel = askXRel - listenBounds.width - PAD;
 | 
					                listenXRel = askXRel - listenBounds.width - PAD;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let yRel = (strategy.primary === 'above')
 | 
					            const yPos = (strategy.primary === 'above')
 | 
				
			||||||
                ? headerBounds.y - workAreaY - Math.max(askBounds.height, listenBounds.height) - PAD
 | 
					                ? (headerBounds.y - workAreaY) - Math.max(askBounds.height, listenBounds.height) - PAD
 | 
				
			||||||
                : headerBounds.y - workAreaY + headerBounds.height + PAD;
 | 
					                : (headerBounds.y - workAreaY) + headerBounds.height + PAD;
 | 
				
			||||||
 | 
					            const yAbs = yPos + workAreaY;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            listen.setBounds({ x: Math.round(listenXRel + workAreaX), y: Math.round(yRel + workAreaY), width: listenBounds.width, height: listenBounds.height });
 | 
					            listen.setBounds({ x: Math.round(listenXRel + workAreaX), y: Math.round(yAbs), width: listenBounds.width, height: listenBounds.height });
 | 
				
			||||||
            ask.setBounds({ x: Math.round(askXRel + workAreaX), y: Math.round(yRel + workAreaY), width: askBounds.width, height: askBounds.height });
 | 
					            ask.setBounds({ x: Math.round(askXRel + workAreaX), y: Math.round(yAbs), width: askBounds.width, height: askBounds.height });
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            const win = askVisible ? ask : listen;
 | 
					            const win = askVisible ? ask : listen;
 | 
				
			||||||
            const winBounds = askVisible ? askBounds : listenBounds;
 | 
					            const winBounds = askVisible ? askBounds : listenBounds;
 | 
				
			||||||
            let xRel = headerCenterXRel - winBounds.width / 2;
 | 
					            let xRel = headerCenterXRel - winBounds.width / 2;
 | 
				
			||||||
            let yRel = (strategy.primary === 'above')
 | 
					            let yPos = (strategy.primary === 'above')
 | 
				
			||||||
                ? headerBounds.y - workAreaY - winBounds.height - PAD
 | 
					                ? (headerBounds.y - workAreaY) - winBounds.height - PAD
 | 
				
			||||||
                : headerBounds.y - workAreaY + headerBounds.height + PAD;
 | 
					                : (headerBounds.y - workAreaY) + headerBounds.height + PAD;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            xRel = Math.max(PAD, Math.min(screenWidth - winBounds.width - PAD, xRel));
 | 
					            xRel = Math.max(PAD, Math.min(screenWidth - winBounds.width - PAD, xRel));
 | 
				
			||||||
            yRel = Math.max(PAD, Math.min(screenHeight - winBounds.height - PAD, yRel));
 | 
					            const yAbs = yPos + workAreaY;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            win.setBounds({ x: Math.round(xRel + workAreaX), y: Math.round(yRel + workAreaY), width: winBounds.width, height: winBounds.height });
 | 
					            win.setBounds({ x: Math.round(xRel + workAreaX), y: Math.round(yAbs), width: winBounds.width, height: winBounds.height });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 'settings' 창의 위치를 조정합니다.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    positionSettingsWindow(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY) {
 | 
					    positionSettingsWindow(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY) {
 | 
				
			||||||
        const settings = this.windowPool.get('settings');
 | 
					        const settings = this.windowPool.get('settings');
 | 
				
			||||||
        if (!settings?.getBounds || !settings.isVisible()) return;
 | 
					        if (!settings?.getBounds || !settings.isVisible()) return;
 | 
				
			||||||
@ -198,10 +295,9 @@ class WindowLayoutManager {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 두 사각형 영역이 겹치는지 확인합니다.
 | 
					 | 
				
			||||||
     * @param {Rectangle} bounds1
 | 
					     * @param {Rectangle} bounds1
 | 
				
			||||||
     * @param {Rectangle} bounds2
 | 
					     * @param {Rectangle} bounds2
 | 
				
			||||||
     * @returns {boolean} 겹침 여부
 | 
					     * @returns {boolean}
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    boundsOverlap(bounds1, bounds2) {
 | 
					    boundsOverlap(bounds1, bounds2) {
 | 
				
			||||||
        const margin = 10;
 | 
					        const margin = 10;
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ const path = require('node:path');
 | 
				
			|||||||
const os = require('os');
 | 
					const os = require('os');
 | 
				
			||||||
const shortcutsService = require('../features/shortcuts/shortcutsService');
 | 
					const shortcutsService = require('../features/shortcuts/shortcutsService');
 | 
				
			||||||
const internalBridge = require('../bridge/internalBridge');
 | 
					const internalBridge = require('../bridge/internalBridge');
 | 
				
			||||||
 | 
					const { EVENTS } = internalBridge;
 | 
				
			||||||
const permissionRepository = require('../features/common/repositories/permission');
 | 
					const permissionRepository = require('../features/common/repositories/permission');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* ────────────────[ GLASS BYPASS ]─────────────── */
 | 
					/* ────────────────[ GLASS BYPASS ]─────────────── */
 | 
				
			||||||
@ -54,6 +55,145 @@ function updateLayout() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
let movementManager = null;
 | 
					let movementManager = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function setupAnimationController(windowPool, layoutManager, movementManager) {
 | 
				
			||||||
 | 
					    internalBridge.on('request-window-visibility', ({ name, visible }) => {
 | 
				
			||||||
 | 
					        handleWindowVisibilityRequest(windowPool, layoutManager, movementManager, name, visible);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @param {Map<string, BrowserWindow>} windowPool
 | 
				
			||||||
 | 
					 * @param {WindowLayoutManager} layoutManager 
 | 
				
			||||||
 | 
					 * @param {SmoothMovementManager} movementManager
 | 
				
			||||||
 | 
					 * @param {'listen' | 'ask'} 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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const isCurrentlyVisible = win.isVisible();
 | 
				
			||||||
 | 
					    if (isCurrentlyVisible === shouldBeVisible) {
 | 
				
			||||||
 | 
					        console.log(`[WindowManager] Window '${name}' is already in the desired state.`);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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_Y = 100; 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (shouldBeVisible) {
 | 
				
			||||||
 | 
					        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();
 | 
				
			||||||
 | 
					                win.setOpacity(1);
 | 
				
			||||||
 | 
					                movementManager.animateWindow(win, targets.listen.x, targets.listen.y);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                const targets = layoutManager.getTargetBoundsForListenNextToAsk();
 | 
				
			||||||
 | 
					                if (!targets.listen) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const startPos = { x: targets.listen.x - ANIM_OFFSET_X, y: targets.listen.y };
 | 
				
			||||||
 | 
					                win.setBounds(startPos);
 | 
				
			||||||
 | 
					                win.show();
 | 
				
			||||||
 | 
					                win.setOpacity(1);
 | 
				
			||||||
 | 
					                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();
 | 
				
			||||||
 | 
					                win.setOpacity(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();
 | 
				
			||||||
 | 
					                win.setOpacity(1);
 | 
				
			||||||
 | 
					                movementManager.animateWindow(otherWin, targets.listen.x, targets.listen.y);
 | 
				
			||||||
 | 
					                movementManager.animateWindow(win, targets.ask.x, targets.ask.y);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        const currentBounds = win.getBounds();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (name === 'listen') {
 | 
				
			||||||
 | 
					            if (!isOtherWinVisible) {
 | 
				
			||||||
 | 
					                const targetX = currentBounds.x - ANIM_OFFSET_X;
 | 
				
			||||||
 | 
					                movementManager.animateWindow(win, targetX, currentBounds.y, {
 | 
				
			||||||
 | 
					                    onComplete: () => win.hide()
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                const targetX = currentBounds.x - currentBounds.width;
 | 
				
			||||||
 | 
					                movementManager.animateWindow(win, targetX, currentBounds.y, {
 | 
				
			||||||
 | 
					                    onComplete: () => win.hide()
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else if (name === 'ask') {
 | 
				
			||||||
 | 
					            if (!isOtherWinVisible) {
 | 
				
			||||||
 | 
					                 const targetY = currentBounds.y - ANIM_OFFSET_Y;
 | 
				
			||||||
 | 
					                 movementManager.animateWindow(win, currentBounds.x, targetY, {
 | 
				
			||||||
 | 
					                     onComplete: () => win.hide()
 | 
				
			||||||
 | 
					                 });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                const targetAskY = currentBounds.y - ANIM_OFFSET_Y;
 | 
				
			||||||
 | 
					                movementManager.animateWindow(win, currentBounds.x, targetAskY, {
 | 
				
			||||||
 | 
					                    onComplete: () => win.hide()
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const targets = layoutManager.getTargetBoundsForFeatureWindows({ listen: true, ask: false });
 | 
				
			||||||
 | 
					                if (targets.listen) {
 | 
				
			||||||
 | 
					                    movementManager.animateWindow(otherWin, targets.listen.x, targets.listen.y);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleAnimationFinished = (sender) => {
 | 
				
			||||||
 | 
					    const win = BrowserWindow.fromWebContents(sender);
 | 
				
			||||||
 | 
					    if (win && !win.isDestroyed()) {
 | 
				
			||||||
 | 
					        console.log(`[WindowManager] Hiding window after animation.`);
 | 
				
			||||||
 | 
					        win.hide();
 | 
				
			||||||
 | 
					        const listenWin = windowPool.get('listen');
 | 
				
			||||||
 | 
					        const askWin = windowPool.get('ask');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (win === askWin && listenWin && !listenWin.isDestroyed() && listenWin.isVisible()) {
 | 
				
			||||||
 | 
					            console.log('[WindowManager] Ask window hidden, moving listen window to center.');
 | 
				
			||||||
 | 
					            listenWin.webContents.send('listen-window-move-to-center');
 | 
				
			||||||
 | 
					            updateLayout();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const setContentProtection = (status) => {
 | 
					const setContentProtection = (status) => {
 | 
				
			||||||
    isContentProtectionOn = status;
 | 
					    isContentProtectionOn = status;
 | 
				
			||||||
    console.log(`[Protection] Content protection toggled to: ${isContentProtectionOn}`);
 | 
					    console.log(`[Protection] Content protection toggled to: ${isContentProtectionOn}`);
 | 
				
			||||||
@ -531,6 +671,7 @@ function createWindows() {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setupIpcHandlers(movementManager);
 | 
					    setupIpcHandlers(movementManager);
 | 
				
			||||||
 | 
					    setupAnimationController(windowPool, layoutManager, movementManager);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (currentHeaderState === 'main') {
 | 
					    if (currentHeaderState === 'main') {
 | 
				
			||||||
        createFeatureWindows(header, ['listen', 'ask', 'settings', 'shortcut-settings']);
 | 
					        createFeatureWindows(header, ['listen', 'ask', 'settings', 'shortcut-settings']);
 | 
				
			||||||
@ -689,45 +830,6 @@ const adjustWindowHeight = (sender, targetHeight) => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const handleAnimationFinished = (sender) => {
 | 
					 | 
				
			||||||
    const win = BrowserWindow.fromWebContents(sender);
 | 
					 | 
				
			||||||
    if (win && !win.isDestroyed()) {
 | 
					 | 
				
			||||||
        console.log(`[WindowManager] Hiding window after animation.`);
 | 
					 | 
				
			||||||
        win.hide();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const closeAskWindow = () => {
 | 
					 | 
				
			||||||
    const askWindow = windowPool.get('ask');
 | 
					 | 
				
			||||||
    if (askWindow) {
 | 
					 | 
				
			||||||
        askWindow.webContents.send('window-hide-animation');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function ensureAskWindowVisible() {
 | 
					 | 
				
			||||||
    if (currentHeaderState !== 'main') {
 | 
					 | 
				
			||||||
        console.log('[WindowManager] Not in main state, skipping ensureAskWindowVisible');
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let askWindow = windowPool.get('ask');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!askWindow || askWindow.isDestroyed()) {
 | 
					 | 
				
			||||||
        console.log('[WindowManager] Ask window not found, creating new one');
 | 
					 | 
				
			||||||
        createFeatureWindows(windowPool.get('header'), 'ask');
 | 
					 | 
				
			||||||
        askWindow = windowPool.get('ask');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!askWindow.isVisible()) {
 | 
					 | 
				
			||||||
        console.log('[WindowManager] Showing hidden Ask window');
 | 
					 | 
				
			||||||
        askWindow.show();
 | 
					 | 
				
			||||||
        updateLayout();
 | 
					 | 
				
			||||||
        askWindow.webContents.send('window-show-animation');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//////// after_modelStateService ////////
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const closeWindow = (windowName) => {
 | 
					const closeWindow = (windowName) => {
 | 
				
			||||||
    const win = windowPool.get(windowName);
 | 
					    const win = windowPool.get(windowName);
 | 
				
			||||||
@ -759,6 +861,4 @@ module.exports = {
 | 
				
			|||||||
    moveHeaderTo,
 | 
					    moveHeaderTo,
 | 
				
			||||||
    adjustWindowHeight,
 | 
					    adjustWindowHeight,
 | 
				
			||||||
    handleAnimationFinished,
 | 
					    handleAnimationFinished,
 | 
				
			||||||
    closeAskWindow,
 | 
					 | 
				
			||||||
    ensureAskWindowVisible,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user