From 12a07b86072a5a0d35b3ec1db33e3d0f3c6eacdd Mon Sep 17 00:00:00 2001 From: sanio Date: Mon, 7 Jul 2025 08:04:05 +0900 Subject: [PATCH] fix header name, modulize windowmanager, fix ui size bug --- package.json | 3 +- src/app/ApiKeyHeader.js | 24 + src/app/HeaderController.js | 60 +- src/app/{AppHeader.js => MainHeader.js} | 64 +- ...PermissionSetup.js => PermissionHeader.js} | 54 +- src/app/PickleGlassApp.js | 4 +- src/app/content.html | 6 + src/app/header.html | 8 +- src/electron/smoothMovementManager.js | 312 +++++ src/electron/windowLayoutManager.js | 217 ++++ src/electron/windowManager.js | 1094 +++-------------- src/features/ask/AskView.js | 41 +- src/features/customize/CustomizeView.js | 38 +- src/features/listen/AssistantView.js | 73 +- 14 files changed, 992 insertions(+), 1006 deletions(-) rename src/app/{AppHeader.js => MainHeader.js} (89%) rename src/app/{PermissionSetup.js => PermissionHeader.js} (88%) create mode 100644 src/electron/smoothMovementManager.js create mode 100644 src/electron/windowLayoutManager.js diff --git a/package.json b/package.json index 8117581..75a89c0 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ }, "optionalDependencies": { "@img/sharp-darwin-x64": "^0.34.2", - "@img/sharp-libvips-darwin-x64": "^1.1.0" + "@img/sharp-libvips-darwin-x64": "^1.1.0", + "electron-liquid-glass": "^1.0.1" } } diff --git a/src/app/ApiKeyHeader.js b/src/app/ApiKeyHeader.js index b884c13..0df4836 100644 --- a/src/app/ApiKeyHeader.js +++ b/src/app/ApiKeyHeader.js @@ -248,6 +248,30 @@ export class ApiKeyHeader extends LitElement { width: 100%; text-align: left; } + :host-context(body.has-glass) .container, + :host-context(body.has-glass) .api-input, + :host-context(body.has-glass) .provider-select, + :host-context(body.has-glass) .action-button, + :host-context(body.has-glass) .close-button { + background: transparent !important; + border: none !important; + box-shadow: none !important; + filter: none !important; + backdrop-filter: none !important; + } + + /* 가상 레이어·그라데이션 테두리 제거 */ + :host-context(body.has-glass) .container::after, + :host-context(body.has-glass) .action-button::after { + display: none !important; + } + + /* hover/active 때 다시 생기는 배경도 차단 */ + :host-context(body.has-glass) .action-button:hover, + :host-context(body.has-glass) .provider-select:hover, + :host-context(body.has-glass) .close-button:hover { + background: transparent !important; + } `; constructor() { diff --git a/src/app/HeaderController.js b/src/app/HeaderController.js index f88e40a..26313f1 100644 --- a/src/app/HeaderController.js +++ b/src/app/HeaderController.js @@ -1,18 +1,18 @@ -import './AppHeader.js'; +import './MainHeader.js'; import './ApiKeyHeader.js'; -import './PermissionSetup.js'; +import './PermissionHeader.js'; class HeaderTransitionManager { constructor() { this.headerContainer = document.getElementById('header-container'); - this.currentHeaderType = null; // 'apikey' | 'app' | 'permission' + this.currentHeaderType = null; // 'apikey' | 'main' | 'permission' this.apiKeyHeader = null; - this.appHeader = null; - this.permissionSetup = null; + this.mainHeader = null; + this.permissionHeader = null; /** * only one header window is allowed - * @param {'apikey'|'app'|'permission'} type + * @param {'apikey'|'main'|'permission'} type */ this.ensureHeader = (type) => { if (this.currentHeaderType === type) return; @@ -20,21 +20,21 @@ class HeaderTransitionManager { this.headerContainer.innerHTML = ''; this.apiKeyHeader = null; - this.appHeader = null; - this.permissionSetup = null; + this.mainHeader = null; + this.permissionHeader = null; // Create new header element if (type === 'apikey') { this.apiKeyHeader = document.createElement('apikey-header'); this.headerContainer.appendChild(this.apiKeyHeader); } else if (type === 'permission') { - this.permissionSetup = document.createElement('permission-setup'); - this.permissionSetup.continueCallback = () => this.transitionToAppHeader(); - this.headerContainer.appendChild(this.permissionSetup); + this.permissionHeader = document.createElement('permission-setup'); + this.permissionHeader.continueCallback = () => this.transitionToMainHeader(); + this.headerContainer.appendChild(this.permissionHeader); } else { - this.appHeader = document.createElement('app-header'); - this.headerContainer.appendChild(this.appHeader); - this.appHeader.startSlideInAnimation?.(); + this.mainHeader = document.createElement('main-header'); + this.headerContainer.appendChild(this.mainHeader); + this.mainHeader.startSlideInAnimation?.(); } this.currentHeaderType = type; @@ -87,16 +87,16 @@ class HeaderTransitionManager { const { isLoggedIn, hasApiKey } = userState; if (isLoggedIn) { - // Firebase user: Check permissions, then show App or Permission Setup + // Firebase user: Check permissions, then show Main or Permission header const permissionResult = await this.checkPermissions(); if (permissionResult.success) { - this.transitionToAppHeader(); + this.transitionToMainHeader(); } else { - this.transitionToPermissionSetup(); + this.transitionToPermissionHeader(); } } else if (hasApiKey) { - // API Key only user: Skip permission check, go directly to App - this.transitionToAppHeader(); + // API Key only user: Skip permission check, go directly to Main + this.transitionToMainHeader(); } else { // No auth at all await this._resizeForApiKey(); @@ -104,7 +104,7 @@ class HeaderTransitionManager { } } - async transitionToPermissionSetup() { + async transitionToPermissionHeader() { // Prevent duplicate transitions if (this.currentHeaderType === 'permission') { console.log('[HeaderController] Already showing permission setup, skipping transition'); @@ -123,7 +123,7 @@ class HeaderTransitionManager { const permissionResult = await this.checkPermissions(); if (permissionResult.success) { // Skip permission setup if already granted - this.transitionToAppHeader(); + this.transitionToMainHeader(); return; } @@ -134,24 +134,24 @@ class HeaderTransitionManager { } } - await this._resizeForPermissionSetup(); + await this._resizeForPermissionHeader(); this.ensureHeader('permission'); } - async transitionToAppHeader(animate = true) { - if (this.currentHeaderType === 'app') { - return this._resizeForApp(); + async transitionToMainHeader(animate = true) { + if (this.currentHeaderType === 'main') { + return this._resizeForMain(); } - await this._resizeForApp(); - this.ensureHeader('app'); + await this._resizeForMain(); + this.ensureHeader('main'); } - _resizeForApp() { + _resizeForMain() { if (!window.require) return; return window .require('electron') - .ipcRenderer.invoke('resize-header-window', { width: 353, height: 60 }) + .ipcRenderer.invoke('resize-header-window', { width: 353, height: 47 }) .catch(() => {}); } @@ -163,7 +163,7 @@ class HeaderTransitionManager { .catch(() => {}); } - async _resizeForPermissionSetup() { + async _resizeForPermissionHeader() { if (!window.require) return; return window .require('electron') diff --git a/src/app/AppHeader.js b/src/app/MainHeader.js similarity index 89% rename from src/app/AppHeader.js rename to src/app/MainHeader.js index 1ae4715..3111066 100644 --- a/src/app/AppHeader.js +++ b/src/app/MainHeader.js @@ -1,6 +1,6 @@ import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js'; -export class AppHeader extends LitElement { +export class MainHeader extends LitElement { static properties = { isSessionActive: { type: Boolean, state: true }, }; @@ -292,6 +292,58 @@ export class AppHeader extends LitElement { width: 16px; height: 16px; } + :host-context(body.has-glass) .header, + :host-context(body.has-glass) .listen-button, + :host-context(body.has-glass) .header-actions, + :host-context(body.has-glass) .settings-button { + /* 배경·블러·그림자 전부 제거 */ + background: transparent !important; + filter: none !important; + box-shadow: none !important; + backdrop-filter: none !important; + } + :host-context(body.has-glass) .icon-box { + background: transparent !important; + border: none !important; + } + + /* 장식용 before/after 레이어와 버튼 오버레이 비활성화 */ + :host-context(body.has-glass) .header::before, + :host-context(body.has-glass) .header::after, + :host-context(body.has-glass) .listen-button::before, + :host-context(body.has-glass) .listen-button::after { + display: none !important; + } + + /* hover 때 의도치 않게 생기는 배경도 차단 */ + :host-context(body.has-glass) .header-actions:hover, + :host-context(body.has-glass) .settings-button:hover, + :host-context(body.has-glass) .listen-button:hover::before { + background: transparent !important; + } + :host-context(body.has-glass) * { + animation: none !important; + transition: none !important; + transform: none !important; + filter: none !important; + backdrop-filter: none !important; + box-shadow: none !important; + } + + /* 2) pill 형태·아이콘 박스 둥근 모서리 평면화 (선택) */ + :host-context(body.has-glass) .header, + :host-context(body.has-glass) .listen-button, + :host-context(body.has-glass) .header-actions, + :host-context(body.has-glass) .settings-button, + :host-context(body.has-glass) .icon-box { + border-radius: 0 !important; + } + :host-context(body.has-glass) { + animation: none !important; + transition: none !important; + transform: none !important; + will-change: auto !important; + } `; constructor() { @@ -362,7 +414,7 @@ export class AppHeader extends LitElement { toggleVisibility() { if (this.isAnimating) { - console.log('[AppHeader] Animation already in progress, ignoring toggle'); + console.log('[MainHeader] Animation already in progress, ignoring toggle'); return; } @@ -432,7 +484,7 @@ export class AppHeader extends LitElement { } else if (this.classList.contains('sliding-in')) { this.classList.remove('sliding-in'); this.hasSlidIn = true; - console.log('[AppHeader] Slide-in animation completed'); + console.log('[MainHeader] Slide-in animation completed'); } } @@ -484,7 +536,7 @@ export class AppHeader extends LitElement { if (this.wasJustDragged) return; if (window.require) { const { ipcRenderer } = window.require('electron'); - console.log(`[AppHeader] showWindow('${name}') called at ${Date.now()}`); + console.log(`[MainHeader] showWindow('${name}') called at ${Date.now()}`); ipcRenderer.send('cancel-hide-window', name); @@ -508,7 +560,7 @@ export class AppHeader extends LitElement { hideWindow(name) { if (this.wasJustDragged) return; if (window.require) { - console.log(`[AppHeader] hideWindow('${name}') called at ${Date.now()}`); + console.log(`[MainHeader] hideWindow('${name}') called at ${Date.now()}`); window.require('electron').ipcRenderer.send('hide-window', name); } } @@ -590,4 +642,4 @@ export class AppHeader extends LitElement { } } -customElements.define('app-header', AppHeader); +customElements.define('main-header', MainHeader); diff --git a/src/app/PermissionSetup.js b/src/app/PermissionHeader.js similarity index 88% rename from src/app/PermissionSetup.js rename to src/app/PermissionHeader.js index f8a7914..85b55bf 100644 --- a/src/app/PermissionSetup.js +++ b/src/app/PermissionHeader.js @@ -1,6 +1,6 @@ import { LitElement, html, css } from '../assets/lit-core-2.7.4.min.js'; -export class PermissionSetup extends LitElement { +export class PermissionHeader extends LitElement { static styles = css` :host { display: block; @@ -237,6 +237,30 @@ export class PermissionSetup extends LitElement { background: rgba(255, 255, 255, 0.2); cursor: not-allowed; } + :host-context(body.has-glass) .container, + :host-context(body.has-glass) .action-button, + :host-context(body.has-glass) .continue-button, + :host-context(body.has-glass) .close-button { + background: transparent !important; + border: none !important; + box-shadow: none !important; + filter: none !important; + backdrop-filter: none !important; + } + + /* Remove gradient borders / pseudo layers */ + :host-context(body.has-glass) .container::after, + :host-context(body.has-glass) .action-button::after, + :host-context(body.has-glass) .continue-button::after { + display: none !important; + } + + /* Prevent background reappearing on hover/active */ + :host-context(body.has-glass) .action-button:hover, + :host-context(body.has-glass) .continue-button:hover, + :host-context(body.has-glass) .close-button:hover { + background: transparent !important; + } `; static properties = { @@ -337,7 +361,7 @@ export class PermissionSetup extends LitElement { try { const permissions = await ipcRenderer.invoke('check-system-permissions'); - console.log('[PermissionSetup] Permission check result:', permissions); + console.log('[PermissionHeader] Permission check result:', permissions); const prevMic = this.microphoneGranted; const prevScreen = this.screenGranted; @@ -347,7 +371,7 @@ export class PermissionSetup extends LitElement { // if permissions changed == UI update if (prevMic !== this.microphoneGranted || prevScreen !== this.screenGranted) { - console.log('[PermissionSetup] Permission status changed, updating UI'); + console.log('[PermissionHeader] Permission status changed, updating UI'); this.requestUpdate(); } @@ -355,11 +379,11 @@ export class PermissionSetup extends LitElement { if (this.microphoneGranted === 'granted' && this.screenGranted === 'granted' && this.continueCallback) { - console.log('[PermissionSetup] All permissions granted, proceeding automatically'); + console.log('[PermissionHeader] All permissions granted, proceeding automatically'); setTimeout(() => this.handleContinue(), 500); } } catch (error) { - console.error('[PermissionSetup] Error checking permissions:', error); + console.error('[PermissionHeader] Error checking permissions:', error); } finally { this.isChecking = false; } @@ -368,12 +392,12 @@ export class PermissionSetup extends LitElement { async handleMicrophoneClick() { if (!window.require || this.microphoneGranted === 'granted' || this.wasJustDragged) return; - console.log('[PermissionSetup] Requesting microphone permission...'); + console.log('[PermissionHeader] Requesting microphone permission...'); const { ipcRenderer } = window.require('electron'); try { const result = await ipcRenderer.invoke('check-system-permissions'); - console.log('[PermissionSetup] Microphone permission result:', result); + console.log('[PermissionHeader] Microphone permission result:', result); if (result.microphone === 'granted') { this.microphoneGranted = 'granted'; @@ -394,19 +418,19 @@ export class PermissionSetup extends LitElement { // Check permissions again after a delay // setTimeout(() => this.checkPermissions(), 1000); } catch (error) { - console.error('[PermissionSetup] Error requesting microphone permission:', error); + console.error('[PermissionHeader] Error requesting microphone permission:', error); } } async handleScreenClick() { if (!window.require || this.screenGranted === 'granted' || this.wasJustDragged) return; - console.log('[PermissionSetup] Checking screen recording permission...'); + console.log('[PermissionHeader] Checking screen recording permission...'); const { ipcRenderer } = window.require('electron'); try { const permissions = await ipcRenderer.invoke('check-system-permissions'); - console.log('[PermissionSetup] Screen permission check result:', permissions); + console.log('[PermissionHeader] Screen permission check result:', permissions); if (permissions.screen === 'granted') { this.screenGranted = 'granted'; @@ -414,7 +438,7 @@ export class PermissionSetup extends LitElement { return; } if (permissions.screen === 'not-determined' || permissions.screen === 'denied' || permissions.screen === 'unknown' || permissions.screen === 'restricted') { - console.log('[PermissionSetup] Opening screen recording preferences...'); + console.log('[PermissionHeader] Opening screen recording preferences...'); await ipcRenderer.invoke('open-system-preferences', 'screen-recording'); } @@ -422,7 +446,7 @@ export class PermissionSetup extends LitElement { // (This may not execute if app restarts after permission grant) // setTimeout(() => this.checkPermissions(), 2000); } catch (error) { - console.error('[PermissionSetup] Error opening screen recording preferences:', error); + console.error('[PermissionHeader] Error opening screen recording preferences:', error); } } @@ -436,9 +460,9 @@ export class PermissionSetup extends LitElement { const { ipcRenderer } = window.require('electron'); try { await ipcRenderer.invoke('mark-permissions-completed'); - console.log('[PermissionSetup] Marked permissions as completed'); + console.log('[PermissionHeader] Marked permissions as completed'); } catch (error) { - console.error('[PermissionSetup] Error marking permissions as completed:', error); + console.error('[PermissionHeader] Error marking permissions as completed:', error); } } @@ -530,4 +554,4 @@ export class PermissionSetup extends LitElement { } } -customElements.define('permission-setup', PermissionSetup); \ No newline at end of file +customElements.define('permission-setup', PermissionHeader); \ No newline at end of file diff --git a/src/app/PickleGlassApp.js b/src/app/PickleGlassApp.js index eb639eb..d10c55a 100644 --- a/src/app/PickleGlassApp.js +++ b/src/app/PickleGlassApp.js @@ -1,7 +1,6 @@ import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js'; import { CustomizeView } from '../features/customize/CustomizeView.js'; import { AssistantView } from '../features/listen/AssistantView.js'; -import { OnboardingView } from '../features/onboarding/OnboardingView.js'; import { AskView } from '../features/ask/AskView.js'; import '../features/listen/renderer.js'; @@ -11,6 +10,7 @@ export class PickleGlassApp extends LitElement { :host { display: block; width: 100%; + height: 100%; color: var(--text-color); background: transparent; border-radius: 7px; @@ -19,11 +19,13 @@ export class PickleGlassApp extends LitElement { assistant-view { display: block; width: 100%; + height: 100%; } ask-view, customize-view, history-view, help-view, onboarding-view, setup-view { display: block; width: 100%; + height: 100%; } `; diff --git a/src/app/content.html b/src/app/content.html index 9ce8d1c..61768e1 100644 --- a/src/app/content.html +++ b/src/app/content.html @@ -301,5 +301,11 @@ } }); + diff --git a/src/app/header.html b/src/app/header.html index 2f35e05..46ea2d7 100644 --- a/src/app/header.html +++ b/src/app/header.html @@ -15,10 +15,14 @@
-
+ diff --git a/src/electron/smoothMovementManager.js b/src/electron/smoothMovementManager.js new file mode 100644 index 0000000..2550d0a --- /dev/null +++ b/src/electron/smoothMovementManager.js @@ -0,0 +1,312 @@ +const { screen } = require('electron'); + +class SmoothMovementManager { + constructor(windowPool, getDisplayById, getCurrentDisplay, updateLayout) { + this.windowPool = windowPool; + this.getDisplayById = getDisplayById; + this.getCurrentDisplay = getCurrentDisplay; + this.updateLayout = updateLayout; + this.stepSize = 80; + this.animationDuration = 300; + this.headerPosition = { x: 0, y: 0 }; + this.isAnimating = false; + this.hiddenPosition = null; + this.lastVisiblePosition = null; + this.currentDisplayId = null; + this.animationFrameId = null; + } + + /** + * @param {BrowserWindow} win + * @returns {boolean} + */ + _isWindowValid(win) { + if (!win || win.isDestroyed()) { + if (this.isAnimating) { + console.warn('[MovementManager] Window destroyed mid-animation. Halting.'); + this.isAnimating = false; + if (this.animationFrameId) { + clearTimeout(this.animationFrameId); + this.animationFrameId = null; + } + } + return false; + } + return true; + } + + moveToDisplay(displayId) { + const header = this.windowPool.get('header'); + if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return; + + const targetDisplay = this.getDisplayById(displayId); + if (!targetDisplay) return; + + const currentBounds = header.getBounds(); + const currentDisplay = this.getCurrentDisplay(header); + + if (currentDisplay.id === targetDisplay.id) return; + + const relativeX = (currentBounds.x - currentDisplay.workArea.x) / currentDisplay.workAreaSize.width; + const relativeY = (currentBounds.y - currentDisplay.workArea.y) / currentDisplay.workAreaSize.height; + const targetX = targetDisplay.workArea.x + targetDisplay.workAreaSize.width * relativeX; + const targetY = targetDisplay.workArea.y + targetDisplay.workAreaSize.height * relativeY; + + const finalX = Math.max(targetDisplay.workArea.x, Math.min(targetDisplay.workArea.x + targetDisplay.workAreaSize.width - currentBounds.width, targetX)); + const finalY = Math.max(targetDisplay.workArea.y, Math.min(targetDisplay.workArea.y + targetDisplay.workAreaSize.height - currentBounds.height, targetY)); + + this.headerPosition = { x: currentBounds.x, y: currentBounds.y }; + this.animateToPosition(header, finalX, finalY); + this.currentDisplayId = targetDisplay.id; + } + + hideToEdge(edge, callback) { + const header = this.windowPool.get('header'); + if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return; + + const currentBounds = header.getBounds(); + const display = this.getCurrentDisplay(header); + + if ( + !currentBounds || typeof currentBounds.x !== 'number' || typeof currentBounds.y !== 'number' || + !display || !display.workArea || !display.workAreaSize || + typeof display.workArea.x !== 'number' || typeof display.workArea.y !== 'number' || + typeof display.workAreaSize.width !== 'number' || typeof display.workAreaSize.height !== 'number' + ) { + console.error('[MovementManager] Invalid bounds or display info for hideToEdge. Aborting.'); + return; + } + + this.lastVisiblePosition = { x: currentBounds.x, y: currentBounds.y }; + this.headerPosition = { x: currentBounds.x, y: currentBounds.y }; + + const { width: screenWidth, height: screenHeight } = display.workAreaSize; + const { x: workAreaX, y: workAreaY } = display.workArea; + + let targetX = this.headerPosition.x; + let targetY = this.headerPosition.y; + + switch (edge) { + case 'top': targetY = workAreaY - currentBounds.height - 20; break; + case 'bottom': targetY = workAreaY + screenHeight + 20; break; + case 'left': targetX = workAreaX - currentBounds.width - 20; break; + case 'right': targetX = workAreaX + screenWidth + 20; break; + } + + this.hiddenPosition = { x: targetX, y: targetY, edge }; + this.isAnimating = true; + const startX = this.headerPosition.x; + const startY = this.headerPosition.y; + const duration = 400; + const startTime = Date.now(); + + const animate = () => { + if (!this._isWindowValid(header)) return; + + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / duration, 1); + const eased = progress * progress * progress; + const currentX = startX + (targetX - startX) * eased; + const currentY = startY + (targetY - startY) * eased; + + if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) { + this.isAnimating = false; + return; + } + + if (!this._isWindowValid(header)) return; + header.setPosition(Math.round(currentX), Math.round(currentY)); + + if (progress < 1) { + this.animationFrameId = setTimeout(animate, 8); + } else { + this.animationFrameId = null; + this.headerPosition = { x: targetX, y: targetY }; + if (Number.isFinite(targetX) && Number.isFinite(targetY)) { + if (!this._isWindowValid(header)) return; + header.setPosition(Math.round(targetX), Math.round(targetY)); + } + this.isAnimating = false; + if (typeof callback === 'function') callback(); + } + }; + animate(); + } + + showFromEdge(callback) { + const header = this.windowPool.get('header'); + if ( + !this._isWindowValid(header) || this.isAnimating || + !this.hiddenPosition || !this.lastVisiblePosition || + typeof this.hiddenPosition.x !== 'number' || typeof this.hiddenPosition.y !== 'number' || + typeof this.lastVisiblePosition.x !== 'number' || typeof this.lastVisiblePosition.y !== 'number' + ) { + console.error('[MovementManager] Invalid state for showFromEdge. Aborting.'); + this.isAnimating = false; + this.hiddenPosition = null; + this.lastVisiblePosition = null; + return; + } + + if (!this._isWindowValid(header)) return; + header.setPosition(this.hiddenPosition.x, this.hiddenPosition.y); + + this.headerPosition = { x: this.hiddenPosition.x, y: this.hiddenPosition.y }; + const targetX = this.lastVisiblePosition.x; + const targetY = this.lastVisiblePosition.y; + this.isAnimating = true; + const startX = this.headerPosition.x; + const startY = this.headerPosition.y; + const duration = 500; + const startTime = Date.now(); + + const animate = () => { + if (!this._isWindowValid(header)) return; + + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / duration, 1); + const c1 = 1.70158; + const c3 = c1 + 1; + const eased = 1 + c3 * Math.pow(progress - 1, 3) + c1 * Math.pow(progress - 1, 2); + const currentX = startX + (targetX - startX) * eased; + const currentY = startY + (targetY - startY) * eased; + if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) { + this.isAnimating = false; + return; + } + + if (!this._isWindowValid(header)) return; + header.setPosition(Math.round(currentX), Math.round(currentY)); + + if (progress < 1) { + this.animationFrameId = setTimeout(animate, 8); + } else { + this.animationFrameId = null; + this.headerPosition = { x: targetX, y: targetY }; + if (Number.isFinite(targetX) && Number.isFinite(targetY)) { + if (!this._isWindowValid(header)) return; + header.setPosition(Math.round(targetX), Math.round(targetY)); + } + this.isAnimating = false; + this.hiddenPosition = null; + this.lastVisiblePosition = null; + if (callback) callback(); + } + }; + animate(); + } + + moveStep(direction) { + const header = this.windowPool.get('header'); + if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return; + + const currentBounds = header.getBounds(); + this.headerPosition = { x: currentBounds.x, y: currentBounds.y }; + let targetX = this.headerPosition.x; + let targetY = this.headerPosition.y; + + switch (direction) { + case 'left': targetX -= this.stepSize; break; + case 'right': targetX += this.stepSize; break; + case 'up': targetY -= this.stepSize; break; + case 'down': targetY += this.stepSize; break; + default: return; + } + + const displays = screen.getAllDisplays(); + let validPosition = displays.some(d => ( + targetX >= d.workArea.x && targetX + currentBounds.width <= d.workArea.x + d.workArea.width && + targetY >= d.workArea.y && targetY + currentBounds.height <= d.workArea.y + d.workArea.height + )); + + if (!validPosition) { + const nearestDisplay = screen.getDisplayNearestPoint({ x: targetX, y: targetY }); + const { x, y, width, height } = nearestDisplay.workArea; + targetX = Math.max(x, Math.min(x + width - currentBounds.width, targetX)); + targetY = Math.max(y, Math.min(y + height - currentBounds.height, targetY)); + } + + if (targetX === this.headerPosition.x && targetY === this.headerPosition.y) return; + this.animateToPosition(header, targetX, targetY); + } + + animateToPosition(header, targetX, targetY) { + if (!this._isWindowValid(header)) return; + + this.isAnimating = true; + const startX = this.headerPosition.x; + const startY = this.headerPosition.y; + const startTime = Date.now(); + + if (!Number.isFinite(targetX) || !Number.isFinite(targetY) || !Number.isFinite(startX) || !Number.isFinite(startY)) { + this.isAnimating = false; + return; + } + + const animate = () => { + if (!this._isWindowValid(header)) return; + + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / this.animationDuration, 1); + const eased = 1 - Math.pow(1 - progress, 3); + const currentX = startX + (targetX - startX) * eased; + const currentY = startY + (targetY - startY) * eased; + + if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) { + this.isAnimating = false; + return; + } + + if (!this._isWindowValid(header)) return; + header.setPosition(Math.round(currentX), Math.round(currentY)); + + if (progress < 1) { + this.animationFrameId = setTimeout(animate, 8); + } else { + this.animationFrameId = null; + this.headerPosition = { x: targetX, y: targetY }; + if (Number.isFinite(targetX) && Number.isFinite(targetY)) { + if (!this._isWindowValid(header)) return; + header.setPosition(Math.round(targetX), Math.round(targetY)); + } + this.isAnimating = false; + this.updateLayout(); + } + }; + animate(); + } + + moveToEdge(direction) { + const header = this.windowPool.get('header'); + if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return; + + const display = this.getCurrentDisplay(header); + const { width, height } = display.workAreaSize; + const { x: workAreaX, y: workAreaY } = display.workArea; + const headerBounds = header.getBounds(); + const currentBounds = header.getBounds(); + let targetX = currentBounds.x; + let targetY = currentBounds.y; + + switch (direction) { + case 'left': targetX = workAreaX; break; + case 'right': targetX = workAreaX + width - headerBounds.width; break; + case 'up': targetY = workAreaY; break; + case 'down': targetY = workAreaY + height - headerBounds.height; break; + } + + this.headerPosition = { x: currentBounds.x, y: currentBounds.y }; + this.animateToPosition(header, targetX, targetY); + } + + destroy() { + if (this.animationFrameId) { + clearTimeout(this.animationFrameId); + this.animationFrameId = null; + } + this.isAnimating = false; + console.log('[Movement] Manager destroyed'); + } +} + +module.exports = SmoothMovementManager; \ No newline at end of file diff --git a/src/electron/windowLayoutManager.js b/src/electron/windowLayoutManager.js new file mode 100644 index 0000000..7893822 --- /dev/null +++ b/src/electron/windowLayoutManager.js @@ -0,0 +1,217 @@ +const { screen } = require('electron'); + +/** + * 주어진 창이 현재 어느 디스플레이에 속해 있는지 반환합니다. + * @param {BrowserWindow} window - 확인할 창 객체 + * @returns {Display} Electron의 Display 객체 + */ +function getCurrentDisplay(window) { + if (!window || window.isDestroyed()) return screen.getPrimaryDisplay(); + + const windowBounds = window.getBounds(); + const windowCenter = { + x: windowBounds.x + windowBounds.width / 2, + y: windowBounds.y + windowBounds.height / 2, + }; + + return screen.getDisplayNearestPoint(windowCenter); +} + +class WindowLayoutManager { + /** + * @param {Map} windowPool - 관리할 창들의 맵 + */ + constructor(windowPool) { + this.windowPool = windowPool; + this.isUpdating = false; + this.PADDING = 80; + } + + /** + * 모든 창의 레이아웃 업데이트를 요청합니다. + * 중복 실행을 방지하기 위해 isUpdating 플래그를 사용합니다. + */ + updateLayout() { + if (this.isUpdating) return; + this.isUpdating = true; + + setImmediate(() => { + this.positionWindows(); + this.isUpdating = false; + }); + } + + /** + * 헤더 창을 기준으로 모든 기능 창들의 위치를 계산하고 배치합니다. + */ + positionWindows() { + const header = this.windowPool.get('header'); + if (!header?.getBounds) return; + + const headerBounds = header.getBounds(); + const display = getCurrentDisplay(header); + const { width: screenWidth, height: screenHeight } = display.workAreaSize; + const { x: workAreaX, y: workAreaY } = display.workArea; + + const headerCenterX = headerBounds.x - workAreaX + headerBounds.width / 2; + const headerCenterY = headerBounds.y - workAreaY + headerBounds.height / 2; + + const relativeX = headerCenterX / screenWidth; + const relativeY = headerCenterY / screenHeight; + + const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY); + + this.positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY); + this.positionSettingsWindow(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY); + } + + /** + * 헤더 창의 위치에 따라 기능 창들을 배치할 최적의 전략을 결정합니다. + * @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); + + if (spaceBelow >= 400) { + return { name: 'below', primary: 'below', secondary: relativeX < 0.5 ? 'right' : 'left' }; + } else if (spaceAbove >= 400) { + return { name: 'above', primary: 'above', secondary: relativeX < 0.5 ? 'right' : 'left' }; + } else if (relativeX < 0.3 && spaceRight >= 800) { + return { name: 'right-side', primary: 'right', secondary: spaceBelow > spaceAbove ? 'below' : 'above' }; + } else if (relativeX > 0.7 && spaceLeft >= 800) { + return { name: 'left-side', primary: 'left', secondary: spaceBelow > spaceAbove ? 'below' : 'above' }; + } else { + return { name: 'adaptive', primary: spaceBelow > spaceAbove ? 'below' : 'above', secondary: spaceRight > spaceLeft ? 'right' : 'left' }; + } + } + + /** + * 'ask'와 'listen' 창의 위치를 조정합니다. + */ + positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY) { + const ask = this.windowPool.get('ask'); + const listen = this.windowPool.get('listen'); + const askVisible = ask && ask.isVisible() && !ask.isDestroyed(); + const listenVisible = listen && listen.isVisible() && !listen.isDestroyed(); + + if (!askVisible && !listenVisible) return; + + const PAD = 8; + const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2; + let askBounds = askVisible ? ask.getBounds() : null; + 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; + + if (listenXRel < PAD) { + listenXRel = PAD; + askXRel = listenXRel + listenBounds.width + PAD; + } + if (askXRel + askBounds.width > screenWidth - PAD) { + askXRel = screenWidth - PAD - askBounds.width; + listenXRel = askXRel - listenBounds.width - PAD; + } + + let yRel = (strategy.primary === 'above') + ? headerBounds.y - workAreaY - Math.max(askBounds.height, listenBounds.height) - PAD + : headerBounds.y - workAreaY + headerBounds.height + PAD; + + 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 }); + } 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; + + xRel = Math.max(PAD, Math.min(screenWidth - winBounds.width - PAD, xRel)); + yRel = Math.max(PAD, Math.min(screenHeight - winBounds.height - PAD, yRel)); + + win.setBounds({ x: Math.round(xRel + workAreaX), y: Math.round(yRel + workAreaY), 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; + + if (settings.__lockedByButton) { + const headerDisplay = getCurrentDisplay(this.windowPool.get('header')); + const settingsDisplay = getCurrentDisplay(settings); + if (headerDisplay.id !== settingsDisplay.id) { + settings.__lockedByButton = false; + } else { + return; + } + } + + const settingsBounds = settings.getBounds(); + const PAD = 5; + const buttonPadding = 17; + let x = headerBounds.x + headerBounds.width - settingsBounds.width - buttonPadding; + let y = headerBounds.y + headerBounds.height + PAD; + + const otherVisibleWindows = []; + ['listen', 'ask'].forEach(name => { + const win = this.windowPool.get(name); + if (win && win.isVisible() && !win.isDestroyed()) { + otherVisibleWindows.push({ name, bounds: win.getBounds() }); + } + }); + + const settingsNewBounds = { x, y, width: settingsBounds.width, height: settingsBounds.height }; + let hasOverlap = otherVisibleWindows.some(otherWin => this.boundsOverlap(settingsNewBounds, otherWin.bounds)); + + if (hasOverlap) { + x = headerBounds.x + headerBounds.width + PAD; + y = headerBounds.y; + if (x + settingsBounds.width > screenWidth - 10) { + x = headerBounds.x - settingsBounds.width - PAD; + } + if (x < 10) { + x = headerBounds.x + headerBounds.width - settingsBounds.width - buttonPadding; + y = headerBounds.y - settingsBounds.height - PAD; + if (y < 10) { + x = headerBounds.x + headerBounds.width - settingsBounds.width; + y = headerBounds.y + headerBounds.height + PAD; + } + } + } + + x = Math.max(workAreaX + 10, Math.min(workAreaX + screenWidth - settingsBounds.width - 10, x)); + y = Math.max(workAreaY + 10, Math.min(workAreaY + screenHeight - settingsBounds.height - 10, y)); + + settings.setBounds({ x: Math.round(x), y: Math.round(y) }); + settings.moveTop(); + } + + /** + * 두 사각형 영역이 겹치는지 확인합니다. + * @param {Rectangle} bounds1 + * @param {Rectangle} bounds2 + * @returns {boolean} 겹침 여부 + */ + boundsOverlap(bounds1, bounds2) { + const margin = 10; + return !( + bounds1.x + bounds1.width + margin < bounds2.x || + bounds2.x + bounds2.width + margin < bounds1.x || + bounds1.y + bounds1.height + margin < bounds2.y || + bounds2.y + bounds2.height + margin < bounds1.y + ); + } +} + +module.exports = WindowLayoutManager; \ No newline at end of file diff --git a/src/electron/windowManager.js b/src/electron/windowManager.js index 586e3f8..86ab579 100644 --- a/src/electron/windowManager.js +++ b/src/electron/windowManager.js @@ -1,4 +1,7 @@ const { BrowserWindow, globalShortcut, ipcMain, screen, app, shell, desktopCapturer } = require('electron'); +const WindowLayoutManager = require('./windowLayoutManager'); +const SmoothMovementManager = require('./smoothMovementManager'); +const liquidGlass = require('electron-liquid-glass'); const path = require('node:path'); const fs = require('node:fs'); const os = require('os'); @@ -10,13 +13,22 @@ const systemSettingsRepository = require('../common/repositories/systemSettings' const userRepository = require('../common/repositories/user'); const fetch = require('node-fetch'); +const isLiquidGlassSupported = () => { + if (process.platform !== 'darwin') { + return false; + } + const majorVersion = parseInt(os.release().split('.')[0], 10); + return majorVersion >= 26; // macOS 26+ (Darwin 25+) +}; +const shouldUseLiquidGlass = isLiquidGlassSupported(); + let isContentProtectionOn = true; let currentDisplayId = null; let mouseEventsIgnored = false; let lastVisibleWindows = new Set(['header']); -const HEADER_HEIGHT = 60; -const DEFAULT_WINDOW_WIDTH = 345; +const HEADER_HEIGHT = 47; +const DEFAULT_WINDOW_WIDTH = 353; let currentHeaderState = 'apikey'; const windowPool = new Map(); @@ -27,38 +39,22 @@ let settingsHideTimer = null; let selectedCaptureSourceId = null; -const windowDefinitions = { - header: { - file: 'header.html', - options: { - /*…*/ - }, - allowedStates: ['apikey', 'app'], - }, - ask: { - file: 'ask.html', - options: { - /*…*/ - }, - allowedStates: ['app'], - }, - listen: { - file: 'assistant.html', - options: { - /*…*/ - }, - allowedStates: ['app'], - }, - settings: { - file: 'settings.html', - options: { - /*…*/ - }, - allowedStates: ['app'], - }, -}; +let layoutManager = null; +function updateLayout() { + if (layoutManager) { + layoutManager.updateLayout(); + } +} + +let movementManager = null; + +let storedProvider = 'openai'; const featureWindows = ['listen','ask','settings']; +function isAllowed(name) { + if (name === 'header') return true; + return featureWindows.includes(name) && currentHeaderState === 'main'; +} function createFeatureWindows(header) { if (windowPool.has('listen')) return; @@ -68,6 +64,7 @@ function createFeatureWindows(header) { show: false, frame: false, transparent: true, + vibrancy: false, hasShadow: false, skipTaskbar: true, hiddenInMissionControl: true, @@ -77,19 +74,62 @@ function createFeatureWindows(header) { // listen const listen = new BrowserWindow({ - ...commonChildOptions, width:400,height:300,minWidth:400,maxWidth:400, - minHeight:200,maxHeight:700, + ...commonChildOptions, width:400,minWidth:400,maxWidth:400, + maxHeight:700, }); listen.setContentProtection(isContentProtectionOn); listen.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); - listen.loadFile(path.join(__dirname,'../app/content.html'),{query:{view:'listen'}}); + listen.setWindowButtonVisibility(false); + const listenLoadOptions = { query: { view: 'listen' } }; + if (!shouldUseLiquidGlass) { + listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions); + } + else { + listenLoadOptions.query.glass = 'true'; + listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions); + listen.webContents.once('did-finish-load', () => { + const viewId = liquidGlass.addView(listen.getNativeWindowHandle(), { + cornerRadius: 12, + tintColor: '#FF00001A', // Red tint + opaque: false, + }); + if (viewId !== -1) { + liquidGlass.unstable_setVariant(viewId, 2); + // liquidGlass.unstable_setScrim(viewId, 1); + // liquidGlass.unstable_setSubdued(viewId, 1); + } + }); + } + + windowPool.set('listen', listen); // ask - const ask = new BrowserWindow({ ...commonChildOptions, width:600, height:350 }); + const ask = new BrowserWindow({ ...commonChildOptions, width:600 }); ask.setContentProtection(isContentProtectionOn); ask.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); - ask.loadFile(path.join(__dirname,'../app/content.html'),{query:{view:'ask'}}); + ask.setWindowButtonVisibility(false); + const askLoadOptions = { query: { view: 'ask' } }; + if (!shouldUseLiquidGlass) { + ask.loadFile(path.join(__dirname, '../app/content.html'), askLoadOptions); + } + else { + askLoadOptions.query.glass = 'true'; + ask.loadFile(path.join(__dirname, '../app/content.html'), askLoadOptions); + ask.webContents.once('did-finish-load', () => { + const viewId = liquidGlass.addView(ask.getNativeWindowHandle(), { + cornerRadius: 12, + tintColor: '#FF00001A', // Red tint + opaque: false, + }); + if (viewId !== -1) { + liquidGlass.unstable_setVariant(viewId, 2); + // liquidGlass.unstable_setScrim(viewId, 1); + // liquidGlass.unstable_setSubdued(viewId, 1); + } + }); + } + ask.on('blur',()=>ask.webContents.send('window-blur')); // Open DevTools in development @@ -99,12 +139,33 @@ function createFeatureWindows(header) { windowPool.set('ask', ask); // settings - const settings = new BrowserWindow({ ...commonChildOptions, width:240, height:450, parent:undefined }); + const settings = new BrowserWindow({ ...commonChildOptions, width:240, maxHeight:350, parent:undefined }); settings.setContentProtection(isContentProtectionOn); settings.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); - settings.loadFile(path.join(__dirname,'../app/content.html'),{query:{view:'customize'}}) - .catch(console.error); - windowPool.set('settings', settings); + settings.setWindowButtonVisibility(false); + const settingsLoadOptions = { query: { view: 'customize' } }; + if (!shouldUseLiquidGlass) { + settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions) + .catch(console.error); + } + else { + settingsLoadOptions.query.glass = 'true'; + settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions) + .catch(console.error); + settings.webContents.once('did-finish-load', () => { + const viewId = liquidGlass.addView(settings.getNativeWindowHandle(), { + cornerRadius: 12, + tintColor: '#FF00001A', // Red tint + opaque: false, + }); + if (viewId !== -1) { + liquidGlass.unstable_setVariant(viewId, 2); + // liquidGlass.unstable_setScrim(viewId, 1); + // liquidGlass.unstable_setSubdued(viewId, 1); + } + }); + } + windowPool.set('settings', settings); } function destroyFeatureWindows() { @@ -119,10 +180,6 @@ function destroyFeatureWindows() { }); } -function isAllowed(name) { - const def = windowDefinitions[name]; - return def && def.allowedStates.includes(currentHeaderState); -} function getCurrentDisplay(window) { if (!window || window.isDestroyed()) return screen.getPrimaryDisplay(); @@ -141,713 +198,9 @@ function getDisplayById(displayId) { return displays.find(d => d.id === displayId) || screen.getPrimaryDisplay(); } -class WindowLayoutManager { - constructor() { - this.isUpdating = false; - this.PADDING = 80; - } - updateLayout() { - if (this.isUpdating) return; - this.isUpdating = true; - setImmediate(() => { - this.positionWindows(); - this.isUpdating = false; - }); - } - - positionWindows() { - const header = windowPool.get('header'); - if (!header?.getBounds) return; - - const headerBounds = header.getBounds(); - const display = getCurrentDisplay(header); - const { width: screenWidth, height: screenHeight } = display.workAreaSize; - const { x: workAreaX, y: workAreaY } = display.workArea; - - const headerCenterX = headerBounds.x - workAreaX + headerBounds.width / 2; - const headerCenterY = headerBounds.y - workAreaY + headerBounds.height / 2; - - const relativeX = headerCenterX / screenWidth; - const relativeY = headerCenterY / screenHeight; - - const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY); - - this.positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY); - this.positionSettingsWindow(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY); - } - - 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); - - const spaces = { - below: spaceBelow, - above: spaceAbove, - left: spaceLeft, - right: spaceRight, - }; - - if (spaceBelow >= 400) { - return { - name: 'below', - primary: 'below', - secondary: relativeX < 0.5 ? 'right' : 'left', - }; - } else if (spaceAbove >= 400) { - return { - name: 'above', - primary: 'above', - secondary: relativeX < 0.5 ? 'right' : 'left', - }; - } else if (relativeX < 0.3 && spaceRight >= 800) { - return { - name: 'right-side', - primary: 'right', - secondary: spaceBelow > spaceAbove ? 'below' : 'above', - }; - } else if (relativeX > 0.7 && spaceLeft >= 800) { - return { - name: 'left-side', - primary: 'left', - secondary: spaceBelow > spaceAbove ? 'below' : 'above', - }; - } else { - return { - name: 'adaptive', - primary: spaceBelow > spaceAbove ? 'below' : 'above', - secondary: spaceRight > spaceLeft ? 'right' : 'left', - }; - } - } - - positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY) { - const ask = windowPool.get('ask'); - const listen = windowPool.get('listen'); - const askVisible = ask && ask.isVisible() && !ask.isDestroyed(); - const listenVisible = listen && listen.isVisible() && !listen.isDestroyed(); - - if (!askVisible && !listenVisible) return; - - const PAD = 8; - - /* ① 헤더 중심 X를 "디스플레이 기준 상대좌표"로 변환 */ - const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2; - - let askBounds = askVisible ? ask.getBounds() : null; - let listenBounds = listenVisible ? listen.getBounds() : null; - - /* ------------------------------------------------- */ - /* 두 창 모두 보이는 경우 */ - /* ------------------------------------------------- */ - if (askVisible && listenVisible) { - const combinedWidth = listenBounds.width + PAD + askBounds.width; - - /* ② 모든 X 좌표를 상대좌표로 계산 */ - let groupStartXRel = headerCenterXRel - combinedWidth / 2; - let listenXRel = groupStartXRel; - let askXRel = groupStartXRel + listenBounds.width + PAD; - - /* 좌우 화면 여백 클램프 – 역시 상대좌표로 */ - if (listenXRel < PAD) { - listenXRel = PAD; - askXRel = listenXRel + listenBounds.width + PAD; - } - if (askXRel + askBounds.width > screenWidth - PAD) { - askXRel = screenWidth - PAD - askBounds.width; - listenXRel = askXRel - listenBounds.width - PAD; - } - - /* Y 좌표는 이미 상대값으로 계산돼 있음 */ - let yRel; - switch (strategy.primary) { - case 'below': - yRel = headerBounds.y - workAreaY + headerBounds.height + PAD; - break; - case 'above': - yRel = headerBounds.y - workAreaY - Math.max(askBounds.height, listenBounds.height) - PAD; - break; - default: - yRel = headerBounds.y - workAreaY + headerBounds.height + PAD; - break; - } - - /* ③ setBounds 직전에 workAreaX/Y를 더해 절대좌표로 변환 */ - 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, - }); - - /* ------------------------------------------------- */ - /* 하나만 보이는 경우 */ - /* ------------------------------------------------- */ - } else { - const win = askVisible ? ask : listen; - const winBounds = askVisible ? askBounds : listenBounds; - - /* X, Y 둘 다 상대좌표로 계산 */ - let xRel = headerCenterXRel - winBounds.width / 2; - let yRel; - switch (strategy.primary) { - case 'below': - yRel = headerBounds.y - workAreaY + headerBounds.height + PAD; - break; - case 'above': - yRel = headerBounds.y - workAreaY - winBounds.height - PAD; - break; - default: - yRel = headerBounds.y - workAreaY + headerBounds.height + PAD; - break; - } - - /* 화면 경계 클램프 */ - xRel = Math.max(PAD, Math.min(screenWidth - winBounds.width - PAD, xRel)); - yRel = Math.max(PAD, Math.min(screenHeight - winBounds.height - PAD, yRel)); - - /* 절대좌표로 변환 후 배치 */ - win.setBounds({ - x: Math.round(xRel + workAreaX), - y: Math.round(yRel + workAreaY), - width: winBounds.width, - height: winBounds.height, - }); - } - } - - positionSettingsWindow(headerBounds, strategy, screenWidth, screenHeight) { - const settings = windowPool.get('settings'); - if (!settings?.getBounds || !settings.isVisible()) return; - - // if (settings.__lockedByButton) return; - if (settings.__lockedByButton) { - const headerDisplay = getCurrentDisplay(windowPool.get('header')); - const settingsDisplay = getCurrentDisplay(settings); - if (headerDisplay.id !== settingsDisplay.id) { - settings.__lockedByButton = false; - } else { - return; // 같은 화면이면 그대로 둔다 - } - } - - const settingsBounds = settings.getBounds(); - const PAD = 5; - - const buttonPadding = 17; - let x = headerBounds.x + headerBounds.width - settingsBounds.width - buttonPadding; - let y = headerBounds.y + headerBounds.height + PAD; - - const otherVisibleWindows = []; - ['listen', 'ask'].forEach(name => { - const win = windowPool.get(name); - if (win && win.isVisible() && !win.isDestroyed()) { - otherVisibleWindows.push({ - name, - bounds: win.getBounds(), - }); - } - }); - - const settingsNewBounds = { x, y, width: settingsBounds.width, height: settingsBounds.height }; - let hasOverlap = false; - - for (const otherWin of otherVisibleWindows) { - if (this.boundsOverlap(settingsNewBounds, otherWin.bounds)) { - hasOverlap = true; - break; - } - } - - if (hasOverlap) { - x = headerBounds.x + headerBounds.width + PAD; - y = headerBounds.y; - settingsNewBounds.x = x; - settingsNewBounds.y = y; - - if (x + settingsBounds.width > screenWidth - 10) { - x = headerBounds.x - settingsBounds.width - PAD; - settingsNewBounds.x = x; - } - - if (x < 10) { - x = headerBounds.x + headerBounds.width - settingsBounds.width - buttonPadding; - y = headerBounds.y - settingsBounds.height - PAD; - settingsNewBounds.x = x; - settingsNewBounds.y = y; - - if (y < 10) { - x = headerBounds.x + headerBounds.width - settingsBounds.width; - y = headerBounds.y + headerBounds.height + PAD; - } - } - } - - x = Math.max(10, Math.min(screenWidth - settingsBounds.width - 10, x)); - y = Math.max(10, Math.min(screenHeight - settingsBounds.height - 10, y)); - - settings.setBounds({ x, y }); - settings.moveTop(); - - // console.log(`[Layout] Settings positioned at (${x}, ${y}) ${hasOverlap ? '(adjusted for overlap)' : '(default position)'}`); - } - - boundsOverlap(bounds1, bounds2) { - const margin = 10; - return !( - bounds1.x + bounds1.width + margin < bounds2.x || - bounds2.x + bounds2.width + margin < bounds1.x || - bounds1.y + bounds1.height + margin < bounds2.y || - bounds2.y + bounds2.height + margin < bounds1.y - ); - } - - isWindowVisible(windowName) { - const window = windowPool.get(windowName); - return window && !window.isDestroyed() && window.isVisible(); - } - - destroy() {} -} - -class SmoothMovementManager { - constructor() { - this.stepSize = 80; - this.animationDuration = 300; - this.headerPosition = { x: 0, y: 0 }; - this.isAnimating = false; - this.hiddenPosition = null; - this.lastVisiblePosition = null; - this.currentDisplayId = null; - } - - moveToDisplay(displayId) { - const header = windowPool.get('header'); - if (!header || !header.isVisible() || this.isAnimating) return; - - const targetDisplay = getDisplayById(displayId); - if (!targetDisplay) return; - - const currentBounds = header.getBounds(); - const currentDisplay = getCurrentDisplay(header); - - if (currentDisplay.id === targetDisplay.id) { - console.log('[Movement] Already on target display'); - return; - } - - const relativeX = (currentBounds.x - currentDisplay.workArea.x) / currentDisplay.workAreaSize.width; - const relativeY = (currentBounds.y - currentDisplay.workArea.y) / currentDisplay.workAreaSize.height; - - const targetX = targetDisplay.workArea.x + targetDisplay.workAreaSize.width * relativeX; - const targetY = targetDisplay.workArea.y + targetDisplay.workAreaSize.height * relativeY; - - const finalX = Math.max( - targetDisplay.workArea.x, - Math.min(targetDisplay.workArea.x + targetDisplay.workAreaSize.width - currentBounds.width, targetX) - ); - const finalY = Math.max( - targetDisplay.workArea.y, - Math.min(targetDisplay.workArea.y + targetDisplay.workAreaSize.height - currentBounds.height, targetY) - ); - - this.headerPosition = { x: currentBounds.x, y: currentBounds.y }; - this.animateToPosition(header, finalX, finalY); - - this.currentDisplayId = targetDisplay.id; - } - - hideToEdge(edge, callback) { - const header = windowPool.get('header'); - if (!header || !header.isVisible() || this.isAnimating) return; - - console.log(`[Movement] Hiding to ${edge} edge`); - - const currentBounds = header.getBounds(); - this.lastVisiblePosition = { x: currentBounds.x, y: currentBounds.y }; - this.headerPosition = { x: currentBounds.x, y: currentBounds.y }; - - const display = getCurrentDisplay(header); - const { width: screenWidth, height: screenHeight } = display.workAreaSize; - const { x: workAreaX, y: workAreaY } = display.workArea; - const headerBounds = header.getBounds(); - - let targetX = this.headerPosition.x; - let targetY = this.headerPosition.y; - - switch (edge) { - case 'top': - targetY = workAreaY - headerBounds.height - 20; - break; - case 'bottom': - targetY = workAreaY + screenHeight + 20; - break; - case 'left': - targetX = workAreaX - headerBounds.width - 20; - break; - case 'right': - targetX = workAreaX + screenWidth + 20; - break; - } - - this.hiddenPosition = { x: targetX, y: targetY, edge }; - - this.isAnimating = true; - const startX = this.headerPosition.x; - const startY = this.headerPosition.y; - const duration = 400; - const startTime = Date.now(); - - const animate = () => { - if (!header || typeof header.setPosition !== 'function' || header.isDestroyed()) { - this.isAnimating = false; - return; - } - - const elapsed = Date.now() - startTime; - const progress = Math.min(elapsed / duration, 1); - const eased = progress * progress * progress; - - const currentX = startX + (targetX - startX) * eased; - const currentY = startY + (targetY - startY) * eased; - - // Validate computed positions before using - if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) { - console.error('[Movement] Invalid animation values for hide:', { - currentX, currentY, progress, eased, startX, startY, targetX, targetY - }); - this.isAnimating = false; - return; - } - - // Safely call setPosition - try { - header.setPosition(Math.round(currentX), Math.round(currentY)); - } catch (err) { - console.error('[Movement] Failed to set position:', err); - this.isAnimating = false; - return; - } - - if (progress < 1) { - setTimeout(animate, 8); - } else { - this.headerPosition = { x: targetX, y: targetY }; - - if (Number.isFinite(targetX) && Number.isFinite(targetY)) { - try { - header.setPosition(Math.round(targetX), Math.round(targetY)); - } catch (err) { - console.error('[Movement] Failed to set final position:', err); - } - } - - this.isAnimating = false; - - if (typeof callback === 'function') { - try { - callback(); - } catch (err) { - console.error('[Movement] Callback error:', err); - } - } - - console.log(`[Movement] Hide to ${edge} completed`); - } - }; - - animate(); - } - - showFromEdge(callback) { - const header = windowPool.get('header'); - if (!header || this.isAnimating || !this.hiddenPosition || !this.lastVisiblePosition) return; - - console.log(`[Movement] Showing from ${this.hiddenPosition.edge} edge`); - - header.setPosition(this.hiddenPosition.x, this.hiddenPosition.y); - this.headerPosition = { x: this.hiddenPosition.x, y: this.hiddenPosition.y }; - - const targetX = this.lastVisiblePosition.x; - const targetY = this.lastVisiblePosition.y; - - this.isAnimating = true; - const startX = this.headerPosition.x; - const startY = this.headerPosition.y; - const duration = 500; - const startTime = Date.now(); - - const animate = () => { - if (!header || header.isDestroyed()) { - this.isAnimating = false; - return; - } - - const elapsed = Date.now() - startTime; - const progress = Math.min(elapsed / duration, 1); - - const c1 = 1.70158; - const c3 = c1 + 1; - const eased = 1 + c3 * Math.pow(progress - 1, 3) + c1 * Math.pow(progress - 1, 2); - - const currentX = startX + (targetX - startX) * eased; - const currentY = startY + (targetY - startY) * eased; - - if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) { - console.error('[Movement] Invalid animation values for show:', { currentX, currentY, progress, eased }); - this.isAnimating = false; - return; - } - - header.setPosition(Math.round(currentX), Math.round(currentY)); - - if (progress < 1) { - setTimeout(animate, 8); - } else { - this.headerPosition = { x: targetX, y: targetY }; - this.headerPosition = { x: targetX, y: targetY }; - if (Number.isFinite(targetX) && Number.isFinite(targetY)) { - header.setPosition(Math.round(targetX), Math.round(targetY)); - } - this.isAnimating = false; - - this.hiddenPosition = null; - this.lastVisiblePosition = null; - - if (callback) callback(); - - console.log(`[Movement] Show from edge completed`); - } - }; - - animate(); - } - - moveStep(direction) { - const header = windowPool.get('header'); - if (!header || !header.isVisible() || this.isAnimating) return; - - console.log(`[Movement] Step ${direction}`); - - const currentBounds = header.getBounds(); - this.headerPosition = { x: currentBounds.x, y: currentBounds.y }; - - let targetX = this.headerPosition.x; - let targetY = this.headerPosition.y; - - switch (direction) { - case 'left': - targetX -= this.stepSize; - break; - case 'right': - targetX += this.stepSize; - break; - case 'up': - targetY -= this.stepSize; - break; - case 'down': - targetY += this.stepSize; - break; - default: - return; - } - - const displays = screen.getAllDisplays(); - let validPosition = false; - - for (const display of displays) { - const { x, y, width, height } = display.workArea; - const headerBounds = header.getBounds(); - - if (targetX >= x && targetX + headerBounds.width <= x + width && targetY >= y && targetY + headerBounds.height <= y + height) { - validPosition = true; - break; - } - } - - if (!validPosition) { - const nearestDisplay = screen.getDisplayNearestPoint({ x: targetX, y: targetY }); - const { x, y, width, height } = nearestDisplay.workArea; - const headerBounds = header.getBounds(); - - targetX = Math.max(x, Math.min(x + width - headerBounds.width, targetX)); - targetY = Math.max(y, Math.min(y + height - headerBounds.height, targetY)); - } - - if (targetX === this.headerPosition.x && targetY === this.headerPosition.y) { - console.log(`[Movement] Already at boundary for ${direction}`); - return; - } - - this.animateToPosition(header, targetX, targetY); - } - - animateToPosition(header, targetX, targetY) { - this.isAnimating = true; - - const startX = this.headerPosition.x; - const startY = this.headerPosition.y; - const startTime = Date.now(); - - if (!Number.isFinite(targetX) || !Number.isFinite(targetY) || !Number.isFinite(startX) || !Number.isFinite(startY)) { - console.error('[Movement] Invalid position values:', { startX, startY, targetX, targetY }); - this.isAnimating = false; - return; - } - - const animate = () => { - if (!header || header.isDestroyed()) { - this.isAnimating = false; - return; - } - - const elapsed = Date.now() - startTime; - const progress = Math.min(elapsed / this.animationDuration, 1); - - const eased = 1 - Math.pow(1 - progress, 3); - - const currentX = startX + (targetX - startX) * eased; - const currentY = startY + (targetY - startY) * eased; - - if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) { - console.error('[Movement] Invalid animation values:', { currentX, currentY, progress, eased }); - this.isAnimating = false; - return; - } - - header.setPosition(Math.round(currentX), Math.round(currentY)); - - if (progress < 1) { - setTimeout(animate, 8); - } else { - this.headerPosition = { x: targetX, y: targetY }; - if (Number.isFinite(targetX) && Number.isFinite(targetY)) { - header.setPosition(Math.round(targetX), Math.round(targetY)); - } else { - console.warn('[Movement] Final position invalid, skip setPosition:', { targetX, targetY }); - } - this.isAnimating = false; - - updateLayout(); - - console.log(`[Movement] Step completed to (${targetX}, ${targetY})`); - } - }; - - animate(); - } - - moveToEdge(direction) { - const header = windowPool.get('header'); - if (!header || !header.isVisible() || this.isAnimating) return; - - console.log(`[Movement] Move to edge: ${direction}`); - - const display = getCurrentDisplay(header); - const { width, height } = display.workAreaSize; - const { x: workAreaX, y: workAreaY } = display.workArea; - const headerBounds = header.getBounds(); - - const currentBounds = header.getBounds(); - let targetX = currentBounds.x; - let targetY = currentBounds.y; - - switch (direction) { - case 'left': - targetX = workAreaX; - break; - case 'right': - targetX = workAreaX + width - headerBounds.width; - break; - case 'up': - targetY = workAreaY; - break; - case 'down': - targetY = workAreaY + height - headerBounds.height; - break; - } - - this.headerPosition = { x: currentBounds.x, y: currentBounds.y }; - - this.isAnimating = true; - const startX = this.headerPosition.x; - const startY = this.headerPosition.y; - const duration = 400; - const startTime = Date.now(); // 이 줄을 animate 함수 정의 전으로 이동 - - if (!Number.isFinite(targetX) || !Number.isFinite(targetY) || !Number.isFinite(startX) || !Number.isFinite(startY)) { - console.error('[Movement] Invalid edge position values:', { startX, startY, targetX, targetY }); - this.isAnimating = false; - return; - } - - const animate = () => { - if (!header || header.isDestroyed()) { - this.isAnimating = false; - return; - } - - const elapsed = Date.now() - startTime; - const progress = Math.min(elapsed / duration, 1); - - const eased = 1 - Math.pow(1 - progress, 4); - - const currentX = startX + (targetX - startX) * eased; - const currentY = startY + (targetY - startY) * eased; - - if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) { - console.error('[Movement] Invalid edge animation values:', { currentX, currentY, progress, eased }); - this.isAnimating = false; - return; - } - - header.setPosition(Math.round(currentX), Math.round(currentY)); - - if (progress < 1) { - setTimeout(animate, 8); - } else { - if (Number.isFinite(targetX) && Number.isFinite(targetY)) { - header.setPosition(Math.round(targetX), Math.round(targetY)); - } - this.headerPosition = { x: targetX, y: targetY }; - this.isAnimating = false; - - updateLayout(); - - console.log(`[Movement] Edge movement completed: ${direction}`); - } - }; - - animate(); - } - - handleKeyPress(direction) {} - - handleKeyRelease(direction) {} - - forceStopMovement() { - this.isAnimating = false; - } - - destroy() { - this.isAnimating = false; - console.log('[Movement] Destroyed'); - } -} - -const layoutManager = new WindowLayoutManager(); -let movementManager = null; - -function toggleAllWindowsVisibility() { +function toggleAllWindowsVisibility(movementManager) { const header = windowPool.get('header'); if (!header) return; @@ -919,21 +272,6 @@ function toggleAllWindowsVisibility() { } } -function ensureDataDirectories() { - const homeDir = os.homedir(); - const pickleGlassDir = path.join(homeDir, '.pickle-glass'); - const dataDir = path.join(pickleGlassDir, 'data'); - const imageDir = path.join(dataDir, 'image'); - const audioDir = path.join(dataDir, 'audio'); - - [pickleGlassDir, dataDir, imageDir, audioDir].forEach(dir => { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - }); - - return { imageDir, audioDir }; -} function createWindows() { const primaryDisplay = screen.getPrimaryDisplay(); @@ -941,8 +279,8 @@ function createWindows() { const initialX = Math.round((screenWidth - DEFAULT_WINDOW_WIDTH) / 2); const initialY = workAreaY + 21; - movementManager = new SmoothMovementManager(); - + movementManager = new SmoothMovementManager(windowPool, getDisplayById, getCurrentDisplay, updateLayout); + const header = new BrowserWindow({ width: DEFAULT_WINDOW_WIDTH, height: HEADER_HEIGHT, @@ -950,6 +288,7 @@ function createWindows() { y: initialY, frame: false, transparent: true, + vibrancy: false, alwaysOnTop: true, skipTaskbar: true, hiddenInMissionControl: true, @@ -963,23 +302,43 @@ function createWindows() { webSecurity: false, }, }); - - windowPool.set('header', header); - - if (currentHeaderState === 'app') { - createFeatureWindows(header); + header.setWindowButtonVisibility(false); + const headerLoadOptions = {}; + if (!shouldUseLiquidGlass) { + header.loadFile(path.join(__dirname, '../app/header.html'), headerLoadOptions); + } + else { + headerLoadOptions.query = { glass: 'true' }; + header.loadFile(path.join(__dirname, '../app/header.html'), headerLoadOptions); + header.webContents.once('did-finish-load', () => { + const viewId = liquidGlass.addView(header.getNativeWindowHandle(), { + cornerRadius: 12, + tintColor: '#FF00001A', // Red tint + opaque: false, + }); + if (viewId !== -1) { + liquidGlass.unstable_setVariant(viewId, 2); + // liquidGlass.unstable_setScrim(viewId, 1); + // liquidGlass.unstable_setSubdued(viewId, 1); + } + }); } - - windowPool.set('header', header); + layoutManager = new WindowLayoutManager(windowPool); - if (currentHeaderState === 'app') { + header.webContents.once('dom-ready', () => { + loadAndRegisterShortcuts(movementManager); + }); + + setupIpcHandlers(movementManager); + + if (currentHeaderState === 'main') { createFeatureWindows(header); } header.setContentProtection(isContentProtectionOn); header.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); - header.loadFile(path.join(__dirname, '../app/header.html')); + // header.loadFile(path.join(__dirname, '../app/header.html')); // Open DevTools in development if (!app.isPackaged) { @@ -1005,18 +364,14 @@ function createWindows() { header.on('resize', updateLayout); - header.webContents.once('dom-ready', () => { - loadAndRegisterShortcuts(); - }); + // header.webContents.once('dom-ready', () => { + // loadAndRegisterShortcuts(); + // }); ipcMain.handle('toggle-all-windows-visibility', toggleAllWindowsVisibility); ipcMain.handle('toggle-feature', async (event, featureName) => { - if (!windowPool.get(featureName) && currentHeaderState === 'app') { - createFeatureWindows(windowPool.get('header')); - } - - if (!windowPool.get(featureName) && currentHeaderState === 'app') { + if (!windowPool.get(featureName) && currentHeaderState === 'main') { createFeatureWindows(windowPool.get('header')); } @@ -1208,12 +563,12 @@ function createWindows() { } }); - setupIpcHandlers(); + // setupIpcHandlers(); return windowPool; } -function loadAndRegisterShortcuts() { +function loadAndRegisterShortcuts(movementManager) { const defaultKeybinds = getDefaultKeybinds(); const header = windowPool.get('header'); const sendToRenderer = (channel, ...args) => { @@ -1226,10 +581,9 @@ function loadAndRegisterShortcuts() { }); }; - const openaiSessionRef = { current: null }; if (!header) { - return updateGlobalShortcuts(defaultKeybinds, undefined, sendToRenderer, openaiSessionRef); + return updateGlobalShortcuts(defaultKeybinds, undefined, sendToRenderer, movementManager); } header.webContents @@ -1237,19 +591,13 @@ function loadAndRegisterShortcuts() { .then(saved => (saved ? JSON.parse(saved) : {})) .then(savedKeybinds => { const keybinds = { ...defaultKeybinds, ...savedKeybinds }; - updateGlobalShortcuts(keybinds, header, sendToRenderer, openaiSessionRef); + updateGlobalShortcuts(keybinds, header, sendToRenderer, movementManager); }) - .catch(() => updateGlobalShortcuts(defaultKeybinds, header, sendToRenderer, openaiSessionRef)); + .catch(() => updateGlobalShortcuts(defaultKeybinds, header, sendToRenderer, movementManager)); } -function updateLayout() { - layoutManager.updateLayout(); -} - -function setupIpcHandlers(openaiSessionRef) { - const layoutManager = new WindowLayoutManager(); - // const movementManager = new SmoothMovementManager(); +function setupIpcHandlers(movementManager) { screen.on('display-added', (event, newDisplay) => { console.log('[Display] New display added:', newDisplay.id); }); @@ -1394,9 +742,9 @@ function setupIpcHandlers(openaiSessionRef) { console.log(`[WindowManager] Header state changed to: ${state}`); currentHeaderState = state; - if (state === 'app') { + if (state === 'main') { createFeatureWindows(windowPool.get('header')); - } else { // 'apikey' + } else { // 'apikey' | 'permission' destroyFeatureWindows(); } @@ -1428,7 +776,7 @@ function setupIpcHandlers(openaiSessionRef) { }); }; - updateGlobalShortcuts(keybinds, header, sendToRenderer, { current: null }); + updateGlobalShortcuts(keybinds, header, sendToRenderer, movementManager); }) .catch(console.error); } @@ -1742,7 +1090,6 @@ function setupIpcHandlers(openaiSessionRef) { }); } -let storedProvider = 'openai'; async function setApiKey(apiKey, provider = 'openai') { console.log('[WindowManager] Persisting API key and provider to DB'); @@ -1834,96 +1181,6 @@ function setupApiKeyIPC() { console.log('[WindowManager] API key related IPC handlers registered (SQLite-backed)'); } -function createWindow(sendToRenderer, openaiSessionRef) { - const mainWindow = new BrowserWindow({ - width: DEFAULT_WINDOW_WIDTH, - height: HEADER_HEIGHT, - x: initialX, - y: initialY, - frame: false, - transparent: false, - hasShadow: true, - alwaysOnTop: true, - skipTaskbar: true, - hiddenInMissionControl: true, - webPreferences: { - nodeIntegration: true, - contextIsolation: false, - backgroundThrottling: false, - enableBlinkFeatures: 'GetDisplayMedia', - webSecurity: true, - allowRunningInsecureContent: false, - }, - backgroundColor: '#FF0000', - }); - - const { session, desktopCapturer } = require('electron'); - session.defaultSession.setDisplayMediaRequestHandler( - (request, callback) => { - desktopCapturer.getSources({ types: ['screen'] }).then(sources => { - callback({ video: sources[0], audio: 'loopback' }); - }); - }, - { useSystemPicker: true } - ); - - mainWindow.setResizable(false); - mainWindow.setContentProtection(true); - mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); - - const primaryDisplay = screen.getPrimaryDisplay(); - const { width: screenWidth } = primaryDisplay.workAreaSize; - const x = Math.floor((screenWidth - DEFAULT_WINDOW_WIDTH) / 2); - const y = 0; - mainWindow.setPosition(x, y); - - if (process.platform === 'win32') { - mainWindow.setAlwaysOnTop(true, 'screen-saver', 1); - } - - mainWindow.loadFile(path.join(__dirname, '../index.html')); - - mainWindow.webContents.once('dom-ready', () => { - setTimeout(() => { - const defaultKeybinds = getDefaultKeybinds(); - let keybinds = defaultKeybinds; - - mainWindow.webContents - .executeJavaScript( - ` - (() => { - try { - const savedKeybinds = localStorage.getItem('customKeybinds'); - const savedContentProtection = localStorage.getItem('contentProtection'); - - return { - keybinds: savedKeybinds ? JSON.parse(savedKeybinds) : null, - contentProtection: savedContentProtection !== null ? savedContentProtection === 'true' : true - }; - } catch (e) { - return { keybinds: null, contentProtection: true }; - } - })() - ` - ) - .then(savedSettings => { - if (savedSettings.keybinds) { - keybinds = { ...defaultKeybinds, ...savedSettings.keybinds }; - } - mainWindow.setContentProtection(savedSettings.contentProtection); - updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, openaiSessionRef); - }) - .catch(() => { - mainWindow.setContentProtection(true); - updateGlobalShortcuts(defaultKeybinds, mainWindow, sendToRenderer, openaiSessionRef); - }); - }, 150); - }); - - setupWindowIpcHandlers(mainWindow, sendToRenderer, openaiSessionRef); - - return mainWindow; -} function getDefaultKeybinds() { const isMac = process.platform === 'darwin'; @@ -1943,23 +1200,20 @@ function getDefaultKeybinds() { }; } -function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, openaiSessionRef) { +function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementManager) { console.log('Updating global shortcuts with:', keybinds); // Unregister all existing shortcuts globalShortcut.unregisterAll(); - if (movementManager) { - movementManager.destroy(); - } - movementManager = new SmoothMovementManager(); + let toggleVisibilityDebounceTimer = null; const isMac = process.platform === 'darwin'; const modifier = isMac ? 'Cmd' : 'Ctrl'; if (keybinds.toggleVisibility) { try { - globalShortcut.register(keybinds.toggleVisibility, toggleAllWindowsVisibility); + globalShortcut.register(keybinds.toggleVisibility, () => toggleAllWindowsVisibility(movementManager)); console.log(`Registered toggleVisibility: ${keybinds.toggleVisibility}`); } catch (error) { console.error(`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`, error); @@ -1972,9 +1226,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, openaiSessi const key = `${modifier}+Shift+${index + 1}`; try { globalShortcut.register(key, () => { - if (movementManager) { - movementManager.moveToDisplay(display.id); - } + movementManager.moveToDisplay(display.id); }); console.log(`Registered display switch shortcut: ${key} -> Display ${index + 1}`); } catch (error) { @@ -2152,48 +1404,6 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, openaiSessi } } -function setupWindowIpcHandlers(mainWindow, sendToRenderer, openaiSessionRef) { - ipcMain.handle('resize-window', async (event, args) => { - try { - const { isMainViewVisible, view } = args; - let targetHeight = HEADER_HEIGHT; - let targetWidth = DEFAULT_WINDOW_WIDTH; - - if (isMainViewVisible) { - const viewHeights = { - listen: 400, - customize: 600, - help: 550, - history: 550, - setup: 200, - }; - targetHeight = viewHeights[view] || 400; - } - - const [currentWidth, currentHeight] = mainWindow.getSize(); - if (currentWidth !== targetWidth || currentHeight !== targetHeight) { - console.log('Window resize requested but disabled for manual resize prevention'); - } - } catch (error) { - console.error('Error resizing window:', error); - } - }); - - ipcMain.handle('toggle-window-visibility', async event => { - if (mainWindow.isVisible()) { - mainWindow.hide(); - } else { - mainWindow.show(); - } - }); - - ipcMain.handle('quit-application', async () => { - app.quit(); - }); - - // Keep other essential IPC handlers - // ... other handlers like open-external, etc. can be added from the old file if needed -} async function captureScreenshot(options = {}) { if (process.platform === 'darwin') { diff --git a/src/features/ask/AskView.js b/src/features/ask/AskView.js index 78e3ba3..9f125a3 100644 --- a/src/features/ask/AskView.js +++ b/src/features/ask/AskView.js @@ -590,6 +590,43 @@ export class AskView extends LitElement { color: rgba(255, 255, 255, 0.5); font-size: 14px; } + :host-context(body.has-glass) .ask-container, + :host-context(body.has-glass) .response-header, + :host-context(body.has-glass) .response-icon, + :host-context(body.has-glass) .copy-button, + :host-context(body.has-glass) .close-button, + :host-context(body.has-glass) .line-copy-button, + :host-context(body.has-glass) .text-input-container, + :host-context(body.has-glass) .response-container pre, + :host-context(body.has-glass) .response-container p code, + :host-context(body.has-glass) .response-container pre code { + background: transparent !important; + border: none !important; + outline: none !important; + box-shadow: none !important; + filter: none !important; + backdrop-filter: none !important; + } + + /* ask-container 의 블러·그림자 레이어 제거 */ + :host-context(body.has-glass) .ask-container::before { + display: none !important; + } + + /* hover/active 때 다시 생기는 배경도 차단 */ + :host-context(body.has-glass) .copy-button:hover, + :host-context(body.has-glass) .close-button:hover, + :host-context(body.has-glass) .line-copy-button, + :host-context(body.has-glass) .line-copy-button:hover, + :host-context(body.has-glass) .response-line:hover { + background: transparent !important; + } + + /* 스크롤바 트랙·썸 마저 투명화 (원할 경우) */ + :host-context(body.has-glass) .response-container::-webkit-scrollbar-track, + :host-context(body.has-glass) .response-container::-webkit-scrollbar-thumb { + background: transparent !important; + } `; constructor() { @@ -1378,9 +1415,9 @@ export class AskView extends LitElement { const responseHeight = responseEl.scrollHeight; const inputHeight = (inputEl && !inputEl.classList.contains('hidden')) ? inputEl.offsetHeight : 0; - const idealHeight = headerHeight + responseHeight + inputHeight + 20; // padding + const idealHeight = headerHeight + responseHeight + inputHeight; - const targetHeight = Math.min(700, Math.max(200, idealHeight)); + const targetHeight = Math.min(700, idealHeight); const { ipcRenderer } = window.require('electron'); ipcRenderer.invoke('adjust-window-height', targetHeight); diff --git a/src/features/customize/CustomizeView.js b/src/features/customize/CustomizeView.js index b3c6dba..ccba875 100644 --- a/src/features/customize/CustomizeView.js +++ b/src/features/customize/CustomizeView.js @@ -11,7 +11,7 @@ export class CustomizeView extends LitElement { :host { display: block; width: 180px; - min-height: 180px; + height: 100%; color: white; } @@ -234,6 +234,38 @@ export class CustomizeView extends LitElement { font-size: 11px; margin-bottom: 4px; } + :host-context(body.has-glass) .settings-container, + :host-context(body.has-glass) .settings-button, + :host-context(body.has-glass) .cmd-key, + :host-context(body.has-glass) .shortcut-key, + :host-context(body.has-glass) .api-key-section input { + background: transparent !important; + border: none !important; + outline: none !important; + box-shadow: none !important; + filter: none !important; + backdrop-filter: none !important; + } + + /* 블러·그림자·gradient 레이어 제거 */ + :host-context(body.has-glass) .settings-container::before { + display: none !important; + } + + /* hover/active 시 다시 생기는 배경도 차단 */ + :host-context(body.has-glass) .settings-button:hover, + :host-context(body.has-glass) .shortcut-item:hover, + :host-context(body.has-glass) .settings-button.danger:hover { + background: transparent !important; + border-color: transparent !important; + transform: none !important; + } + + /* 스크롤바 트랙·썸 투명화(선택 사항) */ + :host-context(body.has-glass) .settings-container::-webkit-scrollbar-track, + :host-context(body.has-glass) .settings-container::-webkit-scrollbar-thumb { + background: transparent !important; + } `; @@ -390,9 +422,7 @@ export class CustomizeView extends LitElement { updateScrollHeight() { const windowHeight = window.innerHeight; - const headerHeight = 60; - const padding = 40; - const maxHeight = windowHeight - headerHeight - padding; + const maxHeight = windowHeight; this.style.maxHeight = `${maxHeight}px`; } diff --git a/src/features/listen/AssistantView.js b/src/features/listen/AssistantView.js index e1584ca..9c76e34 100644 --- a/src/features/listen/AssistantView.js +++ b/src/features/listen/AssistantView.js @@ -161,7 +161,7 @@ export class AssistantView extends LitElement { /* outline: 0.5px rgba(255, 255, 255, 0.5) solid; */ /* outline-offset: -1px; */ width: 100%; - min-height: 200px; + height: 100%; } .assistant-container::after { @@ -542,6 +542,73 @@ export class AssistantView extends LitElement { font-size: 10px; color: rgba(255, 255, 255, 0.7); } + :host-context(body.has-glass) .assistant-container, + :host-context(body.has-glass) .top-bar, + :host-context(body.has-glass) .toggle-button, + :host-context(body.has-glass) .copy-button, + :host-context(body.has-glass) .transcription-container, + :host-context(body.has-glass) .insights-container, + :host-context(body.has-glass) .stt-message, + :host-context(body.has-glass) .outline-item, + :host-context(body.has-glass) .request-item, + :host-context(body.has-glass) .markdown-content, + :host-context(body.has-glass) .insights-container pre, + :host-context(body.has-glass) .insights-container p code, + :host-context(body.has-glass) .insights-container pre code { + background: transparent !important; + border: none !important; + outline: none !important; + box-shadow: none !important; + filter: none !important; + backdrop-filter: none !important; + } + + /* 가상 레이어·gradient 테두리 제거 */ + :host-context(body.has-glass) .assistant-container::before, + :host-context(body.has-glass) .assistant-container::after { + display: none !important; + } + + /* hover 상태에서 생기는 배경도 차단 */ + :host-context(body.has-glass) .toggle-button:hover, + :host-context(body.has-glass) .copy-button:hover, + :host-context(body.has-glass) .outline-item:hover, + :host-context(body.has-glass) .request-item.clickable:hover, + :host-context(body.has-glass) .markdown-content:hover { + background: transparent !important; + transform: none !important; + } + + /* 스크롤바 트랙·썸도 투명화(선택) */ + :host-context(body.has-glass) .transcription-container::-webkit-scrollbar-track, + :host-context(body.has-glass) .transcription-container::-webkit-scrollbar-thumb, + :host-context(body.has-glass) .insights-container::-webkit-scrollbar-track, + :host-context(body.has-glass) .insights-container::-webkit-scrollbar-thumb { + background: transparent !important; + } + :host-context(body.has-glass) * { + animation: none !important; + transition: none !important; + transform: none !important; + filter: none !important; + backdrop-filter: none !important; + box-shadow: none !important; + } + + /* 추가: 둥근 모서리와 스크롤바도 평면화하려면 */ + :host-context(body.has-glass) .assistant-container, + :host-context(body.has-glass) .stt-message, + :host-context(body.has-glass) .toggle-button, + :host-context(body.has-glass) .copy-button { + border-radius: 0 !important; + } + + :host-context(body.has-glass) ::-webkit-scrollbar, + :host-context(body.has-glass) ::-webkit-scrollbar-track, + :host-context(body.has-glass) ::-webkit-scrollbar-thumb { + background: transparent !important; + width: 0 !important; /* 스크롤바 자체 숨기기 */ + } `; static properties = { @@ -777,9 +844,9 @@ export class AssistantView extends LitElement { const contentHeight = activeContent.scrollHeight; - const idealHeight = topBarHeight + contentHeight + 20; + const idealHeight = topBarHeight + contentHeight; - const targetHeight = Math.min(700, Math.max(200, idealHeight)); + const targetHeight = Math.min(700, idealHeight); console.log( `[Height Adjusted] Mode: ${this.viewMode}, TopBar: ${topBarHeight}px, Content: ${contentHeight}px, Ideal: ${idealHeight}px, Target: ${targetHeight}px`