From 186f0a98391f5fe979e804b4a73d2e2faf1a4b70 Mon Sep 17 00:00:00 2001 From: sanio Date: Wed, 9 Jul 2025 22:43:28 +0900 Subject: [PATCH 01/11] centralized keyframe --- src/app/ApiKeyHeader.js | 114 ++----------- src/app/MainHeader.js | 221 ++------------------------ src/app/PermissionHeader.js | 115 ++------------ src/app/content.html | 56 ++++--- src/app/header.html | 5 + src/common/styles/glass-bypass.css | 12 ++ src/electron/windowManager.js | 104 ++---------- src/features/ask/AskView.js | 113 ------------- src/features/listen/AssistantView.js | 189 ---------------------- src/features/settings/SettingsView.js | 30 +--- 10 files changed, 98 insertions(+), 861 deletions(-) create mode 100644 src/common/styles/glass-bypass.css diff --git a/src/app/ApiKeyHeader.js b/src/app/ApiKeyHeader.js index 92962c8..76abf21 100644 --- a/src/app/ApiKeyHeader.js +++ b/src/app/ApiKeyHeader.js @@ -15,15 +15,14 @@ export class ApiKeyHeader extends LitElement { static styles = css` :host { - display: block; - transform: translate3d(0, 0, 0); - backface-visibility: hidden; - transition: opacity 0.25s ease-out; + display: block; + transition: opacity 0.3s ease-in, transform 0.3s ease-in; + will-change: opacity, transform; } :host(.sliding-out) { - animation: slideOutUp 0.3s ease-in forwards; - will-change: opacity, transform; + opacity: 0; + transform: translateY(-20px); } :host(.hidden) { @@ -31,17 +30,6 @@ export class ApiKeyHeader extends LitElement { pointer-events: none; } - @keyframes slideOutUp { - from { - opacity: 1; - transform: translateY(0); - } - to { - opacity: 0; - transform: translateY(-20px); - } - } - * { font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; cursor: default; @@ -50,6 +38,7 @@ export class ApiKeyHeader extends LitElement { } .container { + -webkit-app-region: drag; width: 350px; min-height: 260px; padding: 18px 20px; @@ -79,6 +68,7 @@ export class ApiKeyHeader extends LitElement { } .close-button { + -webkit-app-region: no-drag; position: absolute; top: 10px; right: 10px; @@ -135,6 +125,7 @@ export class ApiKeyHeader extends LitElement { } .api-input { + -webkit-app-region: no-drag; width: 100%; height: 34px; background: rgba(255, 255, 255, 0.1); @@ -162,6 +153,7 @@ export class ApiKeyHeader extends LitElement { .provider-column { flex: 1; display: flex; flex-direction: column; align-items: center; } .provider-label { color: rgba(255, 255, 255, 0.7); font-size: 11px; font-weight: 500; margin-bottom: 6px; } .api-input, .provider-select { + -webkit-app-region: no-drag; width: 100%; height: 34px; text-align: center; @@ -188,6 +180,7 @@ export class ApiKeyHeader extends LitElement { .action-button { + -webkit-app-region: no-drag; width: 100%; height: 34px; background: rgba(255, 255, 255, 0.2); @@ -233,37 +226,10 @@ export class ApiKeyHeader extends LitElement { font-weight: 500; /* Medium */ margin: 10px 0; } - - - /* ────────────────[ GLASS BYPASS ]─────────────── */ - :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; - } - - :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() { super() - this.dragState = null - this.wasJustDragged = false this.isLoading = false this.errorMessage = "" //////// after_modelStateService //////// @@ -275,8 +241,6 @@ export class ApiKeyHeader extends LitElement { this.loadProviderConfig(); //////// after_modelStateService //////// - this.handleMouseMove = this.handleMouseMove.bind(this) - this.handleMouseUp = this.handleMouseUp.bind(this) this.handleKeyPress = this.handleKeyPress.bind(this) this.handleSubmit = this.handleSubmit.bind(this) this.handleInput = this.handleInput.bind(this) @@ -321,61 +285,6 @@ export class ApiKeyHeader extends LitElement { if (sttProviders.length > 0) this.sttProvider = sttProviders[0].id; this.requestUpdate(); -} - - async handleMouseDown(e) { - if (e.target.tagName === "INPUT" || e.target.tagName === "BUTTON" || e.target.tagName === "SELECT") { - return - } - - e.preventDefault() - - const { ipcRenderer } = window.require("electron") - const initialPosition = await ipcRenderer.invoke("get-header-position") - - this.dragState = { - initialMouseX: e.screenX, - initialMouseY: e.screenY, - initialWindowX: initialPosition.x, - initialWindowY: initialPosition.y, - moved: false, - } - - window.addEventListener("mousemove", this.handleMouseMove) - window.addEventListener("mouseup", this.handleMouseUp, { once: true }) - } - - handleMouseMove(e) { - if (!this.dragState) return - - const deltaX = Math.abs(e.screenX - this.dragState.initialMouseX) - const deltaY = Math.abs(e.screenY - this.dragState.initialMouseY) - - if (deltaX > 3 || deltaY > 3) { - this.dragState.moved = true - } - - const newWindowX = this.dragState.initialWindowX + (e.screenX - this.dragState.initialMouseX) - const newWindowY = this.dragState.initialWindowY + (e.screenY - this.dragState.initialMouseY) - - const { ipcRenderer } = window.require("electron") - ipcRenderer.invoke("move-header-to", newWindowX, newWindowY) - } - - handleMouseUp(e) { - if (!this.dragState) return - - const wasDragged = this.dragState.moved - - window.removeEventListener("mousemove", this.handleMouseMove) - this.dragState = null - - if (wasDragged) { - this.wasJustDragged = true - setTimeout(() => { - this.wasJustDragged = false - }, 200) - } } handleInput(e) { @@ -473,7 +382,6 @@ export class ApiKeyHeader extends LitElement { handleUsePicklesKey(e) { e.preventDefault() - if (this.wasJustDragged) return console.log("Requesting Firebase authentication from main process...") if (window.require) { @@ -515,7 +423,7 @@ export class ApiKeyHeader extends LitElement { const isButtonDisabled = this.isLoading || !this.llmApiKey.trim() || !this.sttApiKey.trim(); return html` -
+

Enter Your API Keys

diff --git a/src/app/MainHeader.js b/src/app/MainHeader.js index 7c92f7c..e8b85b1 100644 --- a/src/app/MainHeader.js +++ b/src/app/MainHeader.js @@ -9,10 +9,7 @@ export class MainHeader extends LitElement { static styles = css` :host { display: flex; - transform: translate3d(0, 0, 0); - backface-visibility: hidden; transition: transform 0.2s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.2s ease-out; - will-change: transform, opacity; } :host(.hiding) { @@ -33,65 +30,6 @@ export class MainHeader extends LitElement { pointer-events: none; } - @keyframes slideUp { - 0% { - opacity: 1; - transform: translateY(0) scale(1); - filter: blur(0px); - } - 30% { - opacity: 0.7; - transform: translateY(-20%) scale(0.98); - filter: blur(0.5px); - } - 70% { - opacity: 0.3; - transform: translateY(-80%) scale(0.92); - filter: blur(1.5px); - } - 100% { - opacity: 0; - transform: translateY(-150%) scale(0.85); - filter: blur(2px); - } - } - - @keyframes slideDown { - 0% { - opacity: 0; - transform: translateY(-150%) scale(0.85); - filter: blur(2px); - } - 30% { - opacity: 0.5; - transform: translateY(-50%) scale(0.92); - filter: blur(1px); - } - 65% { - opacity: 0.9; - transform: translateY(-5%) scale(0.99); - filter: blur(0.2px); - } - 85% { - opacity: 0.98; - transform: translateY(2%) scale(1.005); - filter: blur(0px); - } - 100% { - opacity: 1; - transform: translateY(0) scale(1); - filter: blur(0px); - } - } - - @keyframes fadeIn { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } - } * { font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; @@ -100,6 +38,7 @@ export class MainHeader extends LitElement { } .header { + -webkit-app-region: drag; width: max-content; height: 47px; padding: 2px 10px 2px 13px; @@ -141,6 +80,7 @@ export class MainHeader extends LitElement { } .listen-button { + -webkit-app-region: no-drag; height: 26px; padding: 0 13px; background: transparent; @@ -193,6 +133,7 @@ export class MainHeader extends LitElement { } .header-actions { + -webkit-app-region: no-drag; height: 26px; box-sizing: border-box; justify-content: flex-start; @@ -264,6 +205,7 @@ export class MainHeader extends LitElement { } .settings-button { + -webkit-app-region: no-drag; padding: 5px; border-radius: 50%; background: transparent; @@ -291,125 +233,20 @@ export class MainHeader extends LitElement { width: 16px; height: 16px; } - - /* ────────────────[ GLASS BYPASS ]─────────────── */ - :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; - } - - :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; - } - - :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; - } - - :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() { super(); this.shortcuts = {}; - this.dragState = null; - this.wasJustDragged = false; this.isVisible = true; this.isAnimating = false; this.hasSlidIn = false; this.settingsHideTimer = null; this.isSessionActive = false; this.animationEndTimer = null; - this.handleMouseMove = this.handleMouseMove.bind(this); - this.handleMouseUp = this.handleMouseUp.bind(this); this.handleAnimationEnd = this.handleAnimationEnd.bind(this); } - async handleMouseDown(e) { - e.preventDefault(); - - const { ipcRenderer } = window.require('electron'); - const initialPosition = await ipcRenderer.invoke('get-header-position'); - - this.dragState = { - initialMouseX: e.screenX, - initialMouseY: e.screenY, - initialWindowX: initialPosition.x, - initialWindowY: initialPosition.y, - moved: false, - }; - - window.addEventListener('mousemove', this.handleMouseMove, { capture: true }); - window.addEventListener('mouseup', this.handleMouseUp, { once: true, capture: true }); - } - - handleMouseMove(e) { - if (!this.dragState) return; - - const deltaX = Math.abs(e.screenX - this.dragState.initialMouseX); - const deltaY = Math.abs(e.screenY - this.dragState.initialMouseY); - - if (deltaX > 3 || deltaY > 3) { - this.dragState.moved = true; - } - - const newWindowX = this.dragState.initialWindowX + (e.screenX - this.dragState.initialMouseX); - const newWindowY = this.dragState.initialWindowY + (e.screenY - this.dragState.initialMouseY); - - const { ipcRenderer } = window.require('electron'); - ipcRenderer.invoke('move-header-to', newWindowX, newWindowY); - } - - handleMouseUp(e) { - if (!this.dragState) return; - - const wasDragged = this.dragState.moved; - - window.removeEventListener('mousemove', this.handleMouseMove, { capture: true }); - this.dragState = null; - - if (wasDragged) { - this.wasJustDragged = true; - setTimeout(() => { - this.wasJustDragged = false; - }, 0); - } - } - toggleVisibility() { if (this.isAnimating) { console.log('[MainHeader] Animation already in progress, ignoring toggle'); @@ -431,58 +268,29 @@ export class MainHeader extends LitElement { } hide() { - this.classList.remove('showing', 'hidden'); + this.classList.remove('showing'); this.classList.add('hiding'); - this.isVisible = false; - - this.animationEndTimer = setTimeout(() => { - if (this.classList.contains('hiding')) { - this.handleAnimationEnd({ target: this }); - } - }, 350); } - + show() { this.classList.remove('hiding', 'hidden'); this.classList.add('showing'); - this.isVisible = true; - - this.animationEndTimer = setTimeout(() => { - if (this.classList.contains('showing')) { - this.handleAnimationEnd({ target: this }); - } - }, 400); } - + handleAnimationEnd(e) { if (e.target !== this) return; - - if (this.animationEndTimer) { - clearTimeout(this.animationEndTimer); - this.animationEndTimer = null; - } - + this.isAnimating = false; - + if (this.classList.contains('hiding')) { - this.classList.remove('hiding'); this.classList.add('hidden'); - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.send('header-animation-complete', 'hidden'); + window.require('electron').ipcRenderer.send('header-animation-finished', 'hidden'); } } else if (this.classList.contains('showing')) { - this.classList.remove('showing'); - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.send('header-animation-complete', 'visible'); + window.require('electron').ipcRenderer.send('header-animation-finished', 'visible'); } - } else if (this.classList.contains('sliding-in')) { - this.classList.remove('sliding-in'); - this.hasSlidIn = true; - console.log('[MainHeader] Slide-in animation completed'); } } @@ -530,16 +338,12 @@ export class MainHeader extends LitElement { } invoke(channel, ...args) { - if (this.wasJustDragged) { - return; - } if (window.require) { window.require('electron').ipcRenderer.invoke(channel, ...args); } } showWindow(name, element) { - if (this.wasJustDragged) return; if (window.require) { const { ipcRenderer } = window.require('electron'); console.log(`[MainHeader] showWindow('${name}') called at ${Date.now()}`); @@ -564,7 +368,6 @@ export class MainHeader extends LitElement { } hideWindow(name) { - if (this.wasJustDragged) return; if (window.require) { console.log(`[MainHeader] hideWindow('${name}') called at ${Date.now()}`); window.require('electron').ipcRenderer.send('hide-window', name); @@ -600,7 +403,7 @@ export class MainHeader extends LitElement { render() { return html` -
+
this.invoke('toggle-feature', 'ask')}> diff --git a/src/electron/windowManager.js b/src/electron/windowManager.js index d05e781..a0384fb 100644 --- a/src/electron/windowManager.js +++ b/src/electron/windowManager.js @@ -472,7 +472,7 @@ function createWindows() { createFeatureWindows(windowPool.get('header')); } - + const header = windowPool.get('header'); if (featureName === 'listen') { console.log(`[WindowManager] Toggling feature: ${featureName}`); const listenWindow = windowPool.get(featureName); @@ -480,18 +480,22 @@ function createWindows() { if (listenService && listenService.isSessionActive()) { console.log('[WindowManager] Listen session is active, closing it via toggle.'); await listenService.closeSession(); - return; - } - - if (listenWindow.isVisible()) { - listenWindow.webContents.send('window-hide-animation'); + listenWindow.webContents.send('session-state-changed', { isActive: false }); + header.webContents.send('session-state-text', 'Done'); + // return; } else { - listenWindow.show(); - updateLayout(); - // listenWindow.webContents.send('start-listening-session'); - listenWindow.webContents.send('window-show-animation'); - await listenService.initializeSession(); - // listenWindow.webContents.send('start-listening-session'); + if (listenWindow.isVisible()) { + listenWindow.webContents.send('window-hide-animation'); + listenWindow.webContents.send('session-state-changed', { isActive: false }); + header.webContents.send('session-state-text', 'Listen'); + } else { + listenWindow.show(); + updateLayout(); + listenWindow.webContents.send('window-show-animation'); + await listenService.initializeSession(); + listenWindow.webContents.send('session-state-changed', { isActive: true }); + header.webContents.send('session-state-text', 'Stop'); + } } } diff --git a/src/features/listen/listenService.js b/src/features/listen/listenService.js index a019f32..a4f4946 100644 --- a/src/features/listen/listenService.js +++ b/src/features/listen/listenService.js @@ -144,9 +144,7 @@ class ListenService { console.log('✅ Listen service initialized successfully.'); - this.sendToRenderer('session-state-changed', { isActive: true }); this.sendToRenderer('update-status', 'Connected. Ready to listen.'); - // this.sendToRenderer('change-listen-capture-state', { status: "start" }); return true; } catch (error) { @@ -181,6 +179,7 @@ class ListenService { async closeSession() { try { + this.sendToRenderer('change-listen-capture-state', { status: "stop" }); // Close STT sessions await this.sttService.closeSessions(); @@ -194,9 +193,7 @@ class ListenService { this.currentSessionId = null; this.summaryService.resetConversationHistory(); - this.sendToRenderer('session-state-changed', { isActive: false }); this.sendToRenderer('session-did-close'); - this.sendToRenderer('change-listen-capture-state', { status: "stop" }); console.log('Listen service session closed.'); return { success: true }; @@ -285,9 +282,9 @@ class ListenService { } }); - ipcMain.handle('close-session', async () => { - return await this.closeSession(); - }); + // ipcMain.handle('close-session', async () => { + // return await this.closeSession(); + // }); ipcMain.handle('update-google-search-setting', async (event, enabled) => { try { diff --git a/src/features/listen/renderer/listenCapture.js b/src/features/listen/renderer/listenCapture.js index 6c0637e..1607d0b 100644 --- a/src/features/listen/renderer/listenCapture.js +++ b/src/features/listen/renderer/listenCapture.js @@ -7,15 +7,9 @@ let aecPtr = 0; // Rust Aec* 1개만 재사용 /** WASM 모듈 가져오고 1회 초기화 */ async function getAec () { - if (aecModPromise) { - console.log('[AEC] getAec: 캐시=있음(재사용)'); - return aecModPromise; // 캐시 - } - - console.log('[AEC] getAec: 캐시=없음 → 모듈 로드 시작'); + if (aecModPromise) return aecModPromise; // 캐시 aecModPromise = createAecModule().then((M) => { - console.log('[AEC] WASM 모듈 로드 완료'); aecMod = M; // C 심볼 → JS 래퍼 바인딩 (딱 1번) M.newPtr = M.cwrap('AecNew', 'number', @@ -24,12 +18,7 @@ async function getAec () { ['number','number','number','number','number']); M.destroy = M.cwrap('AecDestroy', null, ['number']); return M; - }) - .catch(err => { - console.error('[AEC] WASM 모듈 로드 실패:', err); - throw err; // 상위에서도 잡을 수 있게 - }); - + }); return aecModPromise; } @@ -143,10 +132,6 @@ function disposeAec () { } function runAecSync (micF32, sysF32) { - const modStat = aecMod?.HEAPU8 ? '있음' : '없음'; // aecMod가 초기화되었고 HEAP 접근 가능? - const ptrStat = aecPtr ? '있음' : '없음'; // newPtr 호출 여부 - const heapStat = aecMod?.HEAPU8 ? '있음' : '없음'; // HEAPU8 생성 여부 - console.log(`[AEC] mod:${modStat} ptr:${ptrStat} heap:${heapStat}`); if (!aecMod || !aecPtr || !aecMod.HEAPU8) return micF32; // 아직 모듈 안 뜸 → 패스 const len = micF32.length; @@ -160,7 +145,6 @@ function runAecSync (micF32, sysF32) { const outF32 = float32FromInt16View(new Int16Array(heapBuf, out, len)); aecMod._free(mic.ptr); aecMod._free(echo.ptr); aecMod._free(out); - console.log(`[AEC] 적용 완료`); return outF32; } @@ -282,7 +266,7 @@ async function setupMicProcessing(micStream) { micProcessor.onaudioprocess = (e) => { const inputData = e.inputBuffer.getChannelData(0); audioBuffer.push(...inputData); - // console.log('🎤 micProcessor.onaudioprocess'); + console.log('🎤 micProcessor.onaudioprocess'); // samplesPerChunk(=2400) 만큼 모이면 전송 while (audioBuffer.length >= samplesPerChunk) { @@ -296,7 +280,7 @@ async function setupMicProcessing(micStream) { // **음성 구간일 때만 런** processedChunk = runAecSync(new Float32Array(chunk), sysF32); - // console.log('🔊 Applied WASM-AEC (speex)'); + console.log('🔊 Applied WASM-AEC (speex)'); } else { console.log('🔊 No system audio for AEC reference'); }