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: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 }) => {
|
||||
|
@ -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) {
|
||||
|
@ -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.');
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -878,7 +878,7 @@ export class AskView extends LitElement {
|
||||
}
|
||||
|
||||
handleCloseAskWindow() {
|
||||
this.clearResponseContent();
|
||||
// this.clearResponseContent();
|
||||
window.api.askView.closeAskWindow();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user