fix window animation

This commit is contained in:
sanio 2025-07-14 00:23:46 +09:00
parent 7d33ea9ca8
commit 8da13dcb27
11 changed files with 371 additions and 230 deletions

2
aec

@ -1 +1 @@
Subproject commit f00bb1fb948053c752b916adfee19f90644a0b2f
Subproject commit 9e11f4f95707714464194bdfc9db0222ec5c6163

View File

@ -76,7 +76,8 @@ module.exports = {
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:toggleAskButton', async () => await askService.toggleAskButton());
ipcMain.handle('ask:closeAskWindow', async () => await askService.closeAskWindow());
// Listen
ipcMain.handle('listen:sendMicAudio', async (event, { data, mimeType }) => await listenService.handleSendMicAudioContent(data, mimeType));
ipcMain.handle('listen:sendSystemAudio', async (event, { data, mimeType }) => {

View File

@ -24,7 +24,7 @@ module.exports = {
ipcMain.handle('adjust-window-height', (event, targetHeight) => windowManager.adjustWindowHeight(event.sender, targetHeight));
ipcMain.handle('toggle-all-windows-visibility', () => windowManager.toggleAllWindowsVisibility());
ipcMain.on('animation-finished', (event) => windowManager.handleAnimationFinished(event.sender));
ipcMain.handle('ask:closeAskWindow', () => windowManager.closeAskWindow());
// ipcMain.handle('ask:closeAskWindow', () => windowManager.closeAskWindow());
},
notifyFocusChange(win, isFocused) {

View File

@ -2,6 +2,8 @@ const { BrowserWindow } = require('electron');
const { createStreamingLLM } = require('../common/ai/factory');
// Lazy require helper to avoid circular dependency issues
const getWindowManager = () => require('../../window/windowManager');
const internalBridge = require('../../bridge/internalBridge');
const { EVENTS } = internalBridge;
const getWindowPool = () => {
try {
@ -10,8 +12,6 @@ const getWindowPool = () => {
return null;
}
};
const updateLayout = () => getWindowManager().updateLayout();
const ensureAskWindowVisible = () => getWindowManager().ensureAskWindowVisible();
const sessionRepository = require('../common/repositories/session');
const askRepository = require('./repositories');
@ -148,32 +148,47 @@ class AskService {
async toggleAskButton() {
const askWindow = getWindowPool()?.get('ask');
// 답변이 있거나 스트리밍 중일 때
const hasContent = this.state.isStreaming || (this.state.currentResponse && this.state.currentResponse.length > 0);
if (askWindow && askWindow.isVisible() && hasContent) {
// 창을 닫는 대신, 텍스트 입력창만 토글합니다.
this.state.showTextInput = !this.state.showTextInput;
this._broadcastState(); // 변경된 상태 전파
this._broadcastState();
} else {
// 기존의 창 보이기/숨기기 로직
if (askWindow && askWindow.isVisible()) {
askWindow.webContents.send('window-hide-animation');
internalBridge.emit('request-window-visibility', { name: 'ask', visible: false });
this.state.isVisible = false;
} else {
console.log('[AskService] Showing hidden Ask window');
internalBridge.emit('request-window-visibility', { name: 'ask', visible: true });
this.state.isVisible = true;
askWindow?.show();
updateLayout();
askWindow?.webContents.send('window-show-animation');
}
// 창이 다시 열릴 때를 대비해 상태를 초기화하고 전파합니다.
if (this.state.isVisible) {
this.state.showTextInput = true;
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}>}
*/
async sendMessage(userPrompt, conversationHistoryRaw=[]) {
ensureAskWindowVisible();
// ensureAskWindowVisible();
if (this.abortController) {
this.abortController.abort('New request received.');

View File

@ -4,6 +4,8 @@ const SummaryService = require('./summary/summaryService');
const authService = require('../common/services/authService');
const sessionRepository = require('../common/repositories/session');
const sttRepository = require('./stt/repositories');
const internalBridge = require('../../bridge/internalBridge');
const { EVENTS } = internalBridge;
class ListenService {
constructor() {
@ -60,9 +62,7 @@ class ListenService {
switch (listenButtonText) {
case 'Listen':
console.log('[ListenService] changeSession to "Listen"');
listenWindow.show();
updateLayout();
listenWindow.webContents.send('window-show-animation');
internalBridge.emit('request-window-visibility', { name: 'listen', visible: true });
await this.initializeSession();
listenWindow.webContents.send('session-state-changed', { isActive: true });
break;
@ -75,7 +75,7 @@ class ListenService {
case '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 });
break;

View File

@ -256,16 +256,8 @@ contextBridge.exposeInMainWorld('api', {
sendAnimationFinished: () => ipcRenderer.send('animation-finished'),
// 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),
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)
removeOnSettingsWindowHideAnimation: (callback) => ipcRenderer.removeListener('settings-window-hide-animation', callback),
},
// src/ui/listen/audioCore/listenCapture.js

View File

@ -99,62 +99,6 @@
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 {
animation: settingsPopFromButton 0.12s cubic-bezier(0.23, 1, 0.32, 1) forwards;
transform-origin: 85% 0%;
@ -206,25 +150,6 @@
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>
</head>
<body>
@ -239,62 +164,24 @@
const app = document.getElementById('pickle-glass');
if (window.api) {
// --- REFACTORED: Event-driven animation handling ---
app.addEventListener('animationend', (event) => {
// 숨김 애니메이션이 끝나면 main 프로세스에 알려 창을 실제로 숨깁니다.
if (event.animationName === 'slideUpToHeader' || event.animationName === 'settingsCollapseToButton') {
console.log(`Animation finished: ${event.animationName}. Notifying main process.`);
if (event.animationName === 'settingsCollapseToButton') {
console.log('Settings hide animation finished. Notifying main process.');
window.api.content.sendAnimationFinished();
// 완료 후 애니메이션 클래스 정리
app.classList.remove('window-sliding-up', 'settings-window-hide');
app.classList.add('window-hidden');
} else if (event.animationName === 'slideDownFromHeader' || event.animationName === 'settingsPopFromButton') {
// 보이기 애니메이션 완료 후 클래스 정리
app.classList.remove('window-sliding-down', 'settings-window-show');
app.classList.remove('settings-window-hide');
app.classList.add('hidden');
}
else if (event.animationName === 'settingsPopFromButton') {
app.classList.remove('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(() => {
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');
});
// --- 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>

View File

@ -878,7 +878,7 @@ export class AskView extends LitElement {
}
handleCloseAskWindow() {
this.clearResponseContent();
// this.clearResponseContent();
window.api.askView.closeAskWindow();
}

View File

@ -170,6 +170,56 @@ class SmoothMovementManager {
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) {
if (!this._isWindowValid(header)) return;

View File

@ -1,9 +1,9 @@
const { screen } = require('electron');
/**
* 주어진 창이 현재 어느 디스플레이에 속해 있는지 반환합니다.
* @param {BrowserWindow} window - 확인할 객체
* @returns {Display} Electron의 Display 객체
*
* @param {BrowserWindow} window
* @returns {Display}
*/
function getCurrentDisplay(window) {
if (!window || window.isDestroyed()) return screen.getPrimaryDisplay();
@ -27,10 +27,6 @@ class WindowLayoutManager {
this.PADDING = 80;
}
/**
* 모든 창의 레이아웃 업데이트를 요청합니다.
* 중복 실행을 방지하기 위해 isUpdating 플래그를 사용합니다.
*/
updateLayout() {
if (this.isUpdating) return;
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() {
const header = this.windowPool.get('header');
if (!header?.getBounds) return;
@ -59,21 +159,24 @@ class WindowLayoutManager {
const relativeX = headerCenterX / screenWidth;
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.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) {
const spaceBelow = screenHeight - (headerBounds.y + headerBounds.height);
const spaceAbove = headerBounds.y;
const spaceLeft = headerBounds.x;
const spaceRight = screenWidth - (headerBounds.x + headerBounds.width);
determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY, workAreaX, workAreaY) {
const headerRelX = headerBounds.x - workAreaX;
const headerRelY = headerBounds.y - workAreaY;
const spaceBelow = screenHeight - (headerRelY + headerBounds.height);
const spaceAbove = headerRelY;
const spaceLeft = headerRelX;
const spaceRight = screenWidth - (headerRelX + headerBounds.width);
if (spaceBelow >= 400) {
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) {
const ask = this.windowPool.get('ask');
const listen = this.windowPool.get('listen');
@ -105,10 +206,8 @@ class WindowLayoutManager {
let listenBounds = listenVisible ? listen.getBounds() : null;
if (askVisible && listenVisible) {
const combinedWidth = listenBounds.width + PAD + askBounds.width;
let groupStartXRel = headerCenterXRel - combinedWidth / 2;
let listenXRel = groupStartXRel;
let askXRel = groupStartXRel + listenBounds.width + PAD;
let askXRel = headerCenterXRel - (askBounds.width / 2);
let listenXRel = askXRel - listenBounds.width - PAD;
if (listenXRel < PAD) {
listenXRel = PAD;
@ -119,30 +218,28 @@ class WindowLayoutManager {
listenXRel = askXRel - listenBounds.width - PAD;
}
let yRel = (strategy.primary === 'above')
? headerBounds.y - workAreaY - Math.max(askBounds.height, listenBounds.height) - PAD
: headerBounds.y - workAreaY + headerBounds.height + PAD;
const yPos = (strategy.primary === 'above')
? (headerBounds.y - workAreaY) - Math.max(askBounds.height, listenBounds.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 });
ask.setBounds({ x: Math.round(askXRel + workAreaX), y: Math.round(yRel + workAreaY), width: askBounds.width, height: askBounds.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(yAbs), width: askBounds.width, height: askBounds.height });
} else {
const win = askVisible ? ask : listen;
const winBounds = askVisible ? askBounds : listenBounds;
let xRel = headerCenterXRel - winBounds.width / 2;
let yRel = (strategy.primary === 'above')
? headerBounds.y - workAreaY - winBounds.height - PAD
: headerBounds.y - workAreaY + headerBounds.height + PAD;
let yPos = (strategy.primary === 'above')
? (headerBounds.y - workAreaY) - winBounds.height - PAD
: (headerBounds.y - workAreaY) + headerBounds.height + PAD;
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) {
const settings = this.windowPool.get('settings');
if (!settings?.getBounds || !settings.isVisible()) return;
@ -198,10 +295,9 @@ class WindowLayoutManager {
}
/**
* 사각형 영역이 겹치는지 확인합니다.
* @param {Rectangle} bounds1
* @param {Rectangle} bounds2
* @returns {boolean} 겹침 여부
* @returns {boolean}
*/
boundsOverlap(bounds1, bounds2) {
const margin = 10;

View File

@ -5,6 +5,7 @@ const path = require('node:path');
const os = require('os');
const shortcutsService = require('../features/shortcuts/shortcutsService');
const internalBridge = require('../bridge/internalBridge');
const { EVENTS } = internalBridge;
const permissionRepository = require('../features/common/repositories/permission');
/* ────────────────[ GLASS BYPASS ]─────────────── */
@ -54,6 +55,145 @@ function updateLayout() {
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) => {
isContentProtectionOn = status;
console.log(`[Protection] Content protection toggled to: ${isContentProtectionOn}`);
@ -531,6 +671,7 @@ function createWindows() {
});
setupIpcHandlers(movementManager);
setupAnimationController(windowPool, layoutManager, movementManager);
if (currentHeaderState === 'main') {
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 win = windowPool.get(windowName);
@ -759,6 +861,4 @@ module.exports = {
moveHeaderTo,
adjustWindowHeight,
handleAnimationFinished,
closeAskWindow,
ensureAskWindowVisible,
};