diff --git a/src/app/MainHeader.js b/src/app/MainHeader.js index 33c2a33..24b2d3d 100644 --- a/src/app/MainHeader.js +++ b/src/app/MainHeader.js @@ -2,7 +2,9 @@ import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js'; export class MainHeader extends LitElement { static properties = { - isSessionActive: { type: Boolean, state: true }, + // isSessionActive: { type: Boolean, state: true }, + isTogglingSession: { type: Boolean, state: true }, + actionText: { type: String, state: true }, shortcuts: { type: Object, state: true }, }; @@ -95,6 +97,11 @@ export class MainHeader extends LitElement { position: relative; } + .listen-button:disabled { + cursor: default; + opacity: 0.8; + } + .listen-button.active::before { background: rgba(215, 0, 0, 0.5); } @@ -103,6 +110,24 @@ export class MainHeader extends LitElement { background: rgba(255, 20, 20, 0.6); } + .listen-button.done { + background-color: rgba(255, 255, 255, 0.6); + transition: background-color 0.15s ease; + } + + .listen-button.done .action-text-content { + color: black; + } + + .listen-button.done .listen-icon svg rect, + .listen-button.done .listen-icon svg path { + fill: black; + } + + .listen-button.done:hover { + background-color: #f0f0f0; + } + .listen-button:hover::before { background: rgba(255, 255, 255, 0.18); } @@ -132,6 +157,38 @@ export class MainHeader extends LitElement { pointer-events: none; } + .listen-button.done::after { + display: none; + } + + .loading-dots { + display: flex; + align-items: center; + gap: 5px; + } + + .loading-dots span { + width: 6px; + height: 6px; + background-color: white; + border-radius: 50%; + animation: pulse 1.4s infinite ease-in-out both; + } + .loading-dots span:nth-of-type(1) { + animation-delay: -0.32s; + } + .loading-dots span:nth-of-type(2) { + animation-delay: -0.16s; + } + @keyframes pulse { + 0%, 80%, 100% { + opacity: 0.2; + } + 40% { + opacity: 1.0; + } + } + .header-actions { -webkit-app-region: no-drag; height: 26px; @@ -242,7 +299,9 @@ export class MainHeader extends LitElement { this.isAnimating = false; this.hasSlidIn = false; this.settingsHideTimer = null; - this.isSessionActive = false; + // this.isSessionActive = false; + this.isTogglingSession = false; + this.actionText = 'Listen'; this.animationEndTimer = null; this.handleAnimationEnd = this.handleAnimationEnd.bind(this); } @@ -305,10 +364,19 @@ export class MainHeader extends LitElement { if (window.require) { const { ipcRenderer } = window.require('electron'); - this._sessionStateListener = (event, { isActive }) => { - this.isSessionActive = isActive; + + this._sessionStateTextListener = (event, text) => { + this.actionText = text; + this.isTogglingSession = false; }; - ipcRenderer.on('session-state-changed', this._sessionStateListener); + ipcRenderer.on('session-state-text', this._sessionStateTextListener); + + + // this._sessionStateListener = (event, { isActive }) => { + // this.isSessionActive = isActive; + // this.isTogglingSession = false; + // }; + // ipcRenderer.on('session-state-changed', this._sessionStateListener); this._shortcutListener = (event, keybinds) => { console.log('[MainHeader] Received updated shortcuts:', keybinds); this.shortcuts = keybinds; @@ -328,9 +396,12 @@ export class MainHeader extends LitElement { if (window.require) { const { ipcRenderer } = window.require('electron'); - if (this._sessionStateListener) { - ipcRenderer.removeListener('session-state-changed', this._sessionStateListener); + if (this._sessionStateTextListener) { + ipcRenderer.removeListener('session-state-text', this._sessionStateTextListener); } + // if (this._sessionStateListener) { + // ipcRenderer.removeListener('session-state-changed', this._sessionStateListener); + // } if (this._shortcutListener) { ipcRenderer.removeListener('shortcuts-updated', this._shortcutListener); } @@ -341,6 +412,7 @@ export class MainHeader extends LitElement { if (window.require) { window.require('electron').ipcRenderer.invoke(channel, ...args); } + // return Promise.resolve(); } showSettingsWindow(element) { @@ -369,6 +441,23 @@ export class MainHeader extends LitElement { } } + async _handleListenClick() { + if (this.isTogglingSession) { + return; + } + + this.isTogglingSession = true; + + try { + const channel = 'toggle-feature'; + const args = ['listen']; + await this.invoke(channel, ...args); + } catch (error) { + console.error('IPC invoke for session toggle failed:', error); + this.isTogglingSession = false; + } + } + renderShortcut(accelerator) { if (!accelerator) return html``; @@ -394,31 +483,45 @@ export class MainHeader extends LitElement { } render() { + const buttonClasses = { + active: this.actionText === 'Stop', + done: this.actionText === 'Done', + }; + const showStopIcon = this.actionText === 'Stop' || this.actionText === 'Done'; + 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'); }