From d936af46a3b7229e2c7b69771d230251665e74b6 Mon Sep 17 00:00:00 2001 From: samtiz Date: Sun, 13 Jul 2025 15:12:05 +0900 Subject: [PATCH] rough refactor done --- aec | 2 +- src/bridge/featureBridge.js | 13 +- src/bridge/windowBridge.js | 12 +- src/features/ask/askService.js | 38 +- src/features/common/ai/providers/openai.js | 2 +- .../common/services/modelStateService.js | 2 + .../common/services/permissionService.js | 119 ++++ src/features/listen/stt/sttService.js | 14 +- src/features/listen/summary/summaryService.js | 4 +- src/features/settings/settingsService.js | 1 + src/features/shortcuts/shortcutsService.js | 4 +- src/preload.js | 6 - src/ui/listen/audioCore/listenCapture.js | 133 ----- src/ui/settings/SettingsView.js | 8 +- src/window/windowManager.js | 510 +++++------------- 15 files changed, 316 insertions(+), 552 deletions(-) create mode 100644 src/features/common/services/permissionService.js diff --git a/aec b/aec index 9e11f4f..f00bb1f 160000 --- a/aec +++ b/aec @@ -1 +1 @@ -Subproject commit 9e11f4f95707714464194bdfc9db0222ec5c6163 +Subproject commit f00bb1fb948053c752b916adfee19f90644a0b2f diff --git a/src/bridge/featureBridge.js b/src/bridge/featureBridge.js index 403da70..7df4a83 100644 --- a/src/bridge/featureBridge.js +++ b/src/bridge/featureBridge.js @@ -9,6 +9,7 @@ const shortcutsService = require('../features/shortcuts/shortcutsService'); const askService = require('../features/ask/askService'); const listenService = require('../features/listen/listenService'); +const permissionService = require('../features/common/services/permissionService'); module.exports = { // Renderer로부터의 요청을 수신 @@ -33,6 +34,14 @@ module.exports = { ipcMain.handle('save-shortcuts', async (event, newKeybinds) => await shortcutsService.handleSaveShortcuts(newKeybinds)); + // Permissions + ipcMain.handle('check-system-permissions', async () => await permissionService.checkSystemPermissions()); + ipcMain.handle('request-microphone-permission', async () => await permissionService.requestMicrophonePermission()); + ipcMain.handle('open-system-preferences', async (event, section) => await permissionService.openSystemPreferences(section)); + ipcMain.handle('mark-permissions-completed', async () => await permissionService.markPermissionsAsCompleted()); + ipcMain.handle('check-permissions-completed', async () => await permissionService.checkPermissionsCompleted()); + + // User/Auth ipcMain.handle('get-current-user', () => authService.getCurrentUser()); ipcMain.handle('start-firebase-auth', async () => await authService.startFirebaseAuthFlow()); @@ -67,7 +76,6 @@ module.exports = { ipcMain.handle('ask:sendQuestionFromAsk', async (event, userPrompt) => await askService.sendMessage(userPrompt)); ipcMain.handle('ask:sendQuestionFromSummary', async (event, userPrompt) => await askService.sendMessage(userPrompt)); ipcMain.handle('ask:toggleAskButton', async () => await askService.toggleAskButton()); - ipcMain.handle('stop-screen-capture', async () => askService.handleStopScreenCapture()); // Listen ipcMain.handle('listen:sendMicAudio', async (event, { data, mimeType }) => await listenService.handleSendMicAudioContent(data, mimeType)); @@ -98,12 +106,11 @@ module.exports = { ipcMain.handle('model:validate-key', async (e, { provider, key }) => await modelStateService.handleValidateKey(provider, key)); ipcMain.handle('model:get-all-keys', () => modelStateService.getAllApiKeys()); ipcMain.handle('model:set-api-key', async (e, { provider, key }) => await modelStateService.setApiKey(provider, key)); - ipcMain.handle('model:remove-api-key', async (e, { provider }) => await modelStateService.handleRemoveApiKey(provider)); + ipcMain.handle('model:remove-api-key', async (e, provider) => await modelStateService.handleRemoveApiKey(provider)); ipcMain.handle('model:get-selected-models', () => modelStateService.getSelectedModels()); ipcMain.handle('model:set-selected-model', async (e, { type, modelId }) => await modelStateService.handleSetSelectedModel(type, modelId)); ipcMain.handle('model:get-available-models', (e, { type }) => modelStateService.getAvailableModels(type)); ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured()); - ipcMain.handle('model:get-current-model-info', (e, { type }) => modelStateService.getCurrentModelInfo(type)); ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig()); diff --git a/src/bridge/windowBridge.js b/src/bridge/windowBridge.js index b6b7479..c298d93 100644 --- a/src/bridge/windowBridge.js +++ b/src/bridge/windowBridge.js @@ -1,5 +1,5 @@ // src/bridge/windowBridge.js -const { ipcMain } = require('electron'); +const { ipcMain, BrowserWindow } = require('electron'); const windowManager = require('../window/windowManager'); module.exports = { @@ -15,6 +15,16 @@ module.exports = { ipcMain.handle('move-window-step', (event, direction) => windowManager.moveWindowStep(direction)); ipcMain.on('close-shortcut-editor', () => windowManager.closeWindow('shortcut-settings')); + // Newly moved handlers from windowManager + ipcMain.on('header-state-changed', (event, state) => windowManager.handleHeaderStateChanged(state)); + ipcMain.on('header-animation-finished', (event, state) => windowManager.handleHeaderAnimationFinished(state)); + ipcMain.handle('get-header-position', () => windowManager.getHeaderPosition()); + ipcMain.handle('move-header', (event, newX, newY) => windowManager.moveHeader(newX, newY)); + ipcMain.handle('move-header-to', (event, newX, newY) => windowManager.moveHeaderTo(newX, newY)); + ipcMain.handle('adjust-window-height', (event, targetHeight) => windowManager.adjustWindowHeight(event.sender, targetHeight)); + ipcMain.handle('toggle-all-windows-visibility', () => windowManager.toggleAllWindowsVisibility()); + ipcMain.on('animation-finished', (event) => windowManager.handleAnimationFinished(event.sender)); + ipcMain.handle('ask:closeAskWindow', () => windowManager.closeAskWindow()); }, notifyFocusChange(win, isFocused) { diff --git a/src/features/ask/askService.js b/src/features/ask/askService.js index dfa815e..13653b4 100644 --- a/src/features/ask/askService.js +++ b/src/features/ask/askService.js @@ -1,6 +1,18 @@ const { BrowserWindow } = require('electron'); const { createStreamingLLM } = require('../common/ai/factory'); -const { getCurrentModelInfo, windowPool, updateLayout } = require('../../window/windowManager'); +// Lazy require helper to avoid circular dependency issues +const getWindowManager = () => require('../../window/windowManager'); + +const getWindowPool = () => { + try { + return getWindowManager().windowPool; + } catch { + return null; + } +}; +const updateLayout = () => getWindowManager().updateLayout(); +const ensureAskWindowVisible = () => getWindowManager().ensureAskWindowVisible(); + const sessionRepository = require('../common/repositories/session'); const askRepository = require('./repositories'); const { getSystemPrompt } = require('../common/prompts/promptBuilder'); @@ -10,6 +22,7 @@ const os = require('os'); const util = require('util'); const execFile = util.promisify(require('child_process').execFile); const { desktopCapturer } = require('electron'); +const modelStateService = require('../common/services/modelStateService'); // Try to load sharp, but don't fail if it's not available let sharp; @@ -126,33 +139,33 @@ class AskService { } _broadcastState() { - const askWindow = windowPool.get('ask'); + const askWindow = getWindowPool()?.get('ask'); if (askWindow && !askWindow.isDestroyed()) { askWindow.webContents.send('ask:stateUpdate', this.state); } } async toggleAskButton() { - const askWindow = windowPool.get('ask'); + const askWindow = getWindowPool()?.get('ask'); // 답변이 있거나 스트리밍 중일 때 const hasContent = this.state.isStreaming || (this.state.currentResponse && this.state.currentResponse.length > 0); - if (askWindow.isVisible() && hasContent) { + if (askWindow && askWindow.isVisible() && hasContent) { // 창을 닫는 대신, 텍스트 입력창만 토글합니다. this.state.showTextInput = !this.state.showTextInput; this._broadcastState(); // 변경된 상태 전파 } else { // 기존의 창 보이기/숨기기 로직 - if (askWindow.isVisible()) { + if (askWindow && askWindow.isVisible()) { askWindow.webContents.send('window-hide-animation'); this.state.isVisible = false; } else { console.log('[AskService] Showing hidden Ask window'); this.state.isVisible = true; - askWindow.show(); + askWindow?.show(); updateLayout(); - askWindow.webContents.send('window-show-animation'); + askWindow?.webContents.send('window-show-animation'); } // 창이 다시 열릴 때를 대비해 상태를 초기화하고 전파합니다. if (this.state.isVisible) { @@ -182,6 +195,8 @@ class AskService { * @returns {Promise<{success: boolean, response?: string, error?: string}>} */ async sendMessage(userPrompt, conversationHistoryRaw=[]) { + ensureAskWindowVisible(); + if (this.abortController) { this.abortController.abort('New request received.'); } @@ -212,7 +227,7 @@ class AskService { await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() }); console.log(`[AskService] DB: Saved user prompt to session ${sessionId}`); - const modelInfo = await getCurrentModelInfo(null, { type: 'llm' }); + const modelInfo = modelStateService.getCurrentModelInfo('llm'); if (!modelInfo || !modelInfo.apiKey) { throw new Error('AI model or API key not configured.'); } @@ -252,7 +267,7 @@ class AskService { }); const response = await streamingLLM.streamChat(messages); - const askWin = windowPool.get('ask'); + const askWin = getWindowPool()?.get('ask'); if (!askWin || askWin.isDestroyed()) { console.error("[AskService] Ask window is not available to send stream to."); @@ -351,11 +366,6 @@ class AskService { } } - handleStopScreenCapture() { - lastScreenshot = null; - console.log('[AskService] Stopped screen capture and cleared cache.'); - return { success: true }; - } } const askService = new AskService(); diff --git a/src/features/common/ai/providers/openai.js b/src/features/common/ai/providers/openai.js index fe37c61..eb27d14 100644 --- a/src/features/common/ai/providers/openai.js +++ b/src/features/common/ai/providers/openai.js @@ -82,7 +82,7 @@ async function createSTT({ apiKey, language = 'en', callbacks = {}, usePortkey = silence_duration_ms: 100, }, input_audio_noise_reduction: { - type: 'far_field' + type: 'near_field' } } }; diff --git a/src/features/common/services/modelStateService.js b/src/features/common/services/modelStateService.js index f2fa1c3..e60a2e0 100644 --- a/src/features/common/services/modelStateService.js +++ b/src/features/common/services/modelStateService.js @@ -359,6 +359,7 @@ class ModelStateService { } removeApiKey(provider) { + console.log(`[ModelStateService] Removing API key for provider: ${provider}`); if (provider in this.state.apiKeys) { this.state.apiKeys[provider] = null; const llmProvider = this.getProviderForModel('llm', this.state.selectedModels.llm); @@ -542,6 +543,7 @@ class ModelStateService { } async handleRemoveApiKey(provider) { + console.log(`[ModelStateService] handleRemoveApiKey: ${provider}`); const success = this.removeApiKey(provider); if (success) { const selectedModels = this.getSelectedModels(); diff --git a/src/features/common/services/permissionService.js b/src/features/common/services/permissionService.js new file mode 100644 index 0000000..d94ac04 --- /dev/null +++ b/src/features/common/services/permissionService.js @@ -0,0 +1,119 @@ +const { systemPreferences, shell, desktopCapturer } = require('electron'); +const permissionRepository = require('../repositories/permission'); + +class PermissionService { + async checkSystemPermissions() { + const permissions = { + microphone: 'unknown', + screen: 'unknown', + needsSetup: true + }; + + try { + if (process.platform === 'darwin') { + const micStatus = systemPreferences.getMediaAccessStatus('microphone'); + console.log('[Permissions] Microphone status:', micStatus); + permissions.microphone = micStatus; + + const screenStatus = systemPreferences.getMediaAccessStatus('screen'); + console.log('[Permissions] Screen status:', screenStatus); + permissions.screen = screenStatus; + + permissions.needsSetup = micStatus !== 'granted' || screenStatus !== 'granted'; + } else { + permissions.microphone = 'granted'; + permissions.screen = 'granted'; + permissions.needsSetup = false; + } + + console.log('[Permissions] System permissions status:', permissions); + return permissions; + } catch (error) { + console.error('[Permissions] Error checking permissions:', error); + return { + microphone: 'unknown', + screen: 'unknown', + needsSetup: true, + error: error.message + }; + } + } + + async requestMicrophonePermission() { + if (process.platform !== 'darwin') { + return { success: true }; + } + + try { + const status = systemPreferences.getMediaAccessStatus('microphone'); + console.log('[Permissions] Microphone status:', status); + if (status === 'granted') { + return { success: true, status: 'granted' }; + } + + const granted = await systemPreferences.askForMediaAccess('microphone'); + return { + success: granted, + status: granted ? 'granted' : 'denied' + }; + } catch (error) { + console.error('[Permissions] Error requesting microphone permission:', error); + return { + success: false, + error: error.message + }; + } + } + + async openSystemPreferences(section) { + if (process.platform !== 'darwin') { + return { success: false, error: 'Not supported on this platform' }; + } + + try { + if (section === 'screen-recording') { + try { + console.log('[Permissions] Triggering screen capture request to register app...'); + await desktopCapturer.getSources({ + types: ['screen'], + thumbnailSize: { width: 1, height: 1 } + }); + console.log('[Permissions] App registered for screen recording'); + } catch (captureError) { + console.log('[Permissions] Screen capture request triggered (expected to fail):', captureError.message); + } + + // await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'); + } + return { success: true }; + } catch (error) { + console.error('[Permissions] Error opening system preferences:', error); + return { success: false, error: error.message }; + } + } + + async markPermissionsAsCompleted() { + try { + await permissionRepository.markPermissionsAsCompleted(); + console.log('[Permissions] Marked permissions as completed'); + return { success: true }; + } catch (error) { + console.error('[Permissions] Error marking permissions as completed:', error); + return { success: false, error: error.message }; + } + } + + async checkPermissionsCompleted() { + try { + const completed = await permissionRepository.checkPermissionsCompleted(); + console.log('[Permissions] Permissions completed status:', completed); + return completed; + } catch (error) { + console.error('[Permissions] Error checking permissions completed status:', error); + return false; + } + } +} + +const permissionService = new PermissionService(); +module.exports = permissionService; \ No newline at end of file diff --git a/src/features/listen/stt/sttService.js b/src/features/listen/stt/sttService.js index 1e79bff..4109dd7 100644 --- a/src/features/listen/stt/sttService.js +++ b/src/features/listen/stt/sttService.js @@ -1,6 +1,7 @@ const { BrowserWindow } = require('electron'); const { spawn } = require('child_process'); const { createSTT } = require('../../common/ai/factory'); +const modelStateService = require('../../common/services/modelStateService'); // const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager'); const COMPLETION_DEBOUNCE_MS = 2000; @@ -131,8 +132,7 @@ class SttService { async initializeSttSessions(language = 'en') { const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || language || 'en'; - const { getCurrentModelInfo } = require('../../../window/windowManager'); - const modelInfo = await getCurrentModelInfo(null, { type: 'stt' }); + const modelInfo = modelStateService.getCurrentModelInfo('stt'); if (!modelInfo || !modelInfo.apiKey) { throw new Error('AI model or API key is not configured.'); } @@ -144,6 +144,7 @@ class SttService { console.log('[SttService] Ignoring message - session already closed'); return; } + console.log('[SttService] handleMyMessage', message); if (this.modelInfo.provider === 'whisper') { // Whisper STT emits 'transcription' events with different structure @@ -411,8 +412,7 @@ class SttService { let modelInfo = this.modelInfo; if (!modelInfo) { console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...'); - const { getCurrentModelInfo } = require('../../../window/windowManager'); - modelInfo = await getCurrentModelInfo(null, { type: 'stt' }); + modelInfo = modelStateService.getCurrentModelInfo('stt'); } if (!modelInfo) { throw new Error('STT model info could not be retrieved.'); @@ -433,8 +433,7 @@ class SttService { let modelInfo = this.modelInfo; if (!modelInfo) { console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...'); - const { getCurrentModelInfo } = require('../../../window/windowManager'); - modelInfo = await getCurrentModelInfo(null, { type: 'stt' }); + modelInfo = modelStateService.getCurrentModelInfo('stt'); } if (!modelInfo) { throw new Error('STT model info could not be retrieved.'); @@ -515,8 +514,7 @@ class SttService { let modelInfo = this.modelInfo; if (!modelInfo) { console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...'); - const { getCurrentModelInfo } = require('../../../window/windowManager'); - modelInfo = await getCurrentModelInfo(null, { type: 'stt' }); + modelInfo = modelStateService.getCurrentModelInfo('stt'); } if (!modelInfo) { throw new Error('STT model info could not be retrieved.'); diff --git a/src/features/listen/summary/summaryService.js b/src/features/listen/summary/summaryService.js index d0aae05..29f92a3 100644 --- a/src/features/listen/summary/summaryService.js +++ b/src/features/listen/summary/summaryService.js @@ -3,6 +3,7 @@ const { getSystemPrompt } = require('../../common/prompts/promptBuilder.js'); const { createLLM } = require('../../common/ai/factory'); const sessionRepository = require('../../common/repositories/session'); const summaryRepository = require('./repositories'); +const modelStateService = require('../../common/services/modelStateService'); // const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager.js'); class SummaryService { @@ -97,8 +98,7 @@ Please build upon this context while analyzing the new conversation segments. await sessionRepository.touch(this.currentSessionId); } - const { getCurrentModelInfo } = require('../../../window/windowManager'); - const modelInfo = await getCurrentModelInfo(null, { type: 'llm' }); + const modelInfo = modelStateService.getCurrentModelInfo('llm'); if (!modelInfo || !modelInfo.apiKey) { throw new Error('AI model or API key is not configured.'); } diff --git a/src/features/settings/settingsService.js b/src/features/settings/settingsService.js index 79011b7..58e0a98 100644 --- a/src/features/settings/settingsService.js +++ b/src/features/settings/settingsService.js @@ -374,6 +374,7 @@ async function removeApiKey() { } }); + console.log('[SettingsService] API key removed for all providers'); return { success: true }; } catch (error) { console.error('[SettingsService] Error removing API key:', error); diff --git a/src/features/shortcuts/shortcutsService.js b/src/features/shortcuts/shortcutsService.js index 17bff2e..744ff66 100644 --- a/src/features/shortcuts/shortcutsService.js +++ b/src/features/shortcuts/shortcutsService.js @@ -1,6 +1,7 @@ const { globalShortcut, screen } = require('electron'); const shortcutsRepository = require('./repositories'); const internalBridge = require('../../bridge/internalBridge'); +const askService = require('../ask/askService'); class ShortcutsService { @@ -210,8 +211,7 @@ class ShortcutsService { callback = () => this.toggleAllWindowsVisibility(this.windowPool); break; case 'nextStep': - // Late require to prevent circular dependency - callback = () => require('../../window/windowManager').toggleFeature('ask', {ask: { targetVisibility: 'show' }}); + callback = () => askService.toggleAskButton(); break; case 'scrollUp': callback = () => { diff --git a/src/preload.js b/src/preload.js index f175060..762a382 100644 --- a/src/preload.js +++ b/src/preload.js @@ -276,12 +276,6 @@ contextBridge.exposeInMainWorld('api', { startMacosSystemAudio: () => ipcRenderer.invoke('listen:startMacosSystemAudio'), stopMacosSystemAudio: () => ipcRenderer.invoke('listen:stopMacosSystemAudio'), - // Screen Capture - captureScreenshot: (options) => ipcRenderer.invoke('capture-screenshot', options), - getCurrentScreenshot: () => ipcRenderer.invoke('get-current-screenshot'), - startScreenCapture: () => ipcRenderer.invoke('start-screen-capture'), - stopScreenCapture: () => ipcRenderer.invoke('stop-screen-capture'), - // Session Management isSessionActive: () => ipcRenderer.invoke('is-session-active'), diff --git a/src/ui/listen/audioCore/listenCapture.js b/src/ui/listen/audioCore/listenCapture.js index a114a57..2f52f25 100644 --- a/src/ui/listen/audioCore/listenCapture.js +++ b/src/ui/listen/audioCore/listenCapture.js @@ -38,13 +38,10 @@ const isMacOS = window.api.platform.isMacOS; let mediaStream = null; let micMediaStream = null; -let screenshotInterval = null; let audioContext = null; let audioProcessor = null; let systemAudioContext = null; let systemAudioProcessor = null; -let currentImageQuality = 'medium'; -let lastScreenshotBase64 = null; let systemAudioBuffer = []; const MAX_SYSTEM_BUFFER_SIZE = 10; @@ -140,10 +137,6 @@ function runAecSync(micF32, sysF32) { return micF32; } - // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ - // 새로운 프레임 단위 처리 로직 - // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ - const frameSize = 160; // AEC 모듈 초기화 시 설정한 프레임 크기 const numFrames = Math.floor(micF32.length / frameSize); @@ -418,94 +411,10 @@ function setupSystemAudioProcessing(systemStream) { return { context: systemAudioContext, processor: systemProcessor }; } -// --------------------------- -// Screenshot functions (exact from renderer.js) -// --------------------------- -async function captureScreenshot(imageQuality = 'medium', isManual = false) { - console.log(`Capturing ${isManual ? 'manual' : 'automated'} screenshot...`); - - // Check rate limiting for automated screenshots only - if (!isManual && tokenTracker.shouldThrottle()) { - console.log('Automated screenshot skipped due to rate limiting'); - return; - } - - try { - // Request screenshot from main process - const result = await window.api.listenCapture.captureScreenshot({ - quality: imageQuality, - }); - - if (result.success && result.base64) { - // Store the latest screenshot - lastScreenshotBase64 = result.base64; - - // Note: sendResult is not defined in the original, this was likely an error - // Commenting out this section as it references undefined variable - /* - if (sendResult.success) { - // Track image tokens after successful send - const imageTokens = tokenTracker.calculateImageTokens(result.width || 1920, result.height || 1080); - tokenTracker.addTokens(imageTokens, 'image'); - console.log(`📊 Image sent successfully - ${imageTokens} tokens used (${result.width}x${result.height})`); - } else { - console.error('Failed to send image:', sendResult.error); - } - */ - } else { - console.error('Failed to capture screenshot:', result.error); - } - } catch (error) { - console.error('Error capturing screenshot:', error); - } -} - -async function captureManualScreenshot(imageQuality = null) { - console.log('Manual screenshot triggered'); - const quality = imageQuality || currentImageQuality; - await captureScreenshot(quality, true); -} - -async function getCurrentScreenshot() { - try { - // First try to get a fresh screenshot from main process - const result = await window.api.listenCapture.getCurrentScreenshot(); - - if (result.success && result.base64) { - console.log('Got fresh screenshot from main process'); - return result.base64; - } - - // If no screenshot available, capture one now - console.log('No screenshot available, capturing new one'); - const captureResult = await window.api.listenCapture.captureScreenshot({ - quality: currentImageQuality, - }); - - if (captureResult.success && captureResult.base64) { - lastScreenshotBase64 = captureResult.base64; - return captureResult.base64; - } - - // Fallback to last stored screenshot - if (lastScreenshotBase64) { - console.log('Using cached screenshot'); - return lastScreenshotBase64; - } - - throw new Error('Failed to get screenshot'); - } catch (error) { - console.error('Error getting current screenshot:', error); - return null; - } -} - // --------------------------- // Main capture functions (exact from renderer.js) // --------------------------- async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'medium') { - // Store the image quality for manual screenshots - currentImageQuality = imageQuality; // Reset token tracker when starting new capture session tokenTracker.reset(); @@ -534,13 +443,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu } } - // Initialize screen capture in main process - const screenResult = await window.api.listenCapture.startScreenCapture(); - if (!screenResult.success) { - throw new Error('Failed to start screen capture: ' + screenResult.error); - } - - try { micMediaStream = await navigator.mediaDevices.getUserMedia({ audio: { @@ -602,12 +504,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu // Windows - capture mic and system audio separately using native loopback console.log('Starting Windows capture with native loopback audio...'); - // Start screen capture in main process for screenshots - const screenResult = await window.api.listenCapture.startScreenCapture(); - if (!screenResult.success) { - throw new Error('Failed to start screen capture: ' + screenResult.error); - } - // Ensure STT sessions are initialized before starting audio capture const sessionActive = await window.api.listenCapture.isSessionActive(); if (!sessionActive) { @@ -656,20 +552,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu // Continue without system audio } } - - // Start capturing screenshots - check if manual mode - if (screenshotIntervalSeconds === 'manual' || screenshotIntervalSeconds === 'Manual') { - console.log('Manual mode enabled - screenshots will be captured on demand only'); - // Don't start automatic capture in manual mode - } else { - // 스크린샷 기능 활성화 (chatModel에서 사용) - const intervalMilliseconds = parseInt(screenshotIntervalSeconds) * 1000; - screenshotInterval = setInterval(() => captureScreenshot(imageQuality), intervalMilliseconds); - - // Capture first screenshot immediately - setTimeout(() => captureScreenshot(imageQuality), 100); - console.log(`📸 Screenshot capture enabled with ${screenshotIntervalSeconds}s interval`); - } } catch (err) { console.error('Error starting capture:', err); // Note: pickleGlass.e() is not available in this context, commenting out @@ -678,11 +560,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu } function stopCapture() { - if (screenshotInterval) { - clearInterval(screenshotInterval); - screenshotInterval = null; - } - // Clean up microphone resources if (audioProcessor) { audioProcessor.disconnect(); @@ -713,11 +590,6 @@ function stopCapture() { micMediaStream = null; } - // Stop screen capture in main process - window.api.listenCapture.stopScreenCapture().catch(err => { - console.error('Error stopping screen capture:', err); - }); - // Stop macOS audio capture if running if (isMacOS) { window.api.listenCapture.stopMacosSystemAudio().catch(err => { @@ -735,19 +607,14 @@ module.exports = { disposeAec, // 필요시 Rust 객체 파괴 startCapture, stopCapture, - captureManualScreenshot, - getCurrentScreenshot, isLinux, isMacOS, }; // Expose functions to global scope for external access (exact from renderer.js) if (typeof window !== 'undefined') { - window.captureManualScreenshot = captureManualScreenshot; window.listenCapture = module.exports; window.pickleGlass = window.pickleGlass || {}; window.pickleGlass.startCapture = startCapture; window.pickleGlass.stopCapture = stopCapture; - window.pickleGlass.captureManualScreenshot = captureManualScreenshot; - window.pickleGlass.getCurrentScreenshot = getCurrentScreenshot; } \ No newline at end of file diff --git a/src/ui/settings/SettingsView.js b/src/ui/settings/SettingsView.js index b480bd2..aba87a2 100644 --- a/src/ui/settings/SettingsView.js +++ b/src/ui/settings/SettingsView.js @@ -693,6 +693,7 @@ export class SettingsView extends LitElement { } async handleClearKey(provider) { + console.log(`[SettingsView] handleClearKey: ${provider}`); this.saving = true; await window.api.settingsView.removeApiKey(provider); this.apiKeys = { ...this.apiKeys, [provider]: '' }; @@ -1097,13 +1098,6 @@ export class SettingsView extends LitElement { } } - async handleClearApiKey() { - console.log('Clear API Key clicked'); - await window.api.settingsView.removeApiKey(); - this.apiKey = null; - this.requestUpdate(); - } - handleQuit() { console.log('Quit clicked'); window.api.settingsView.quitApplication(); diff --git a/src/window/windowManager.js b/src/window/windowManager.js index ae18e76..eacf4da 100644 --- a/src/window/windowManager.js +++ b/src/window/windowManager.js @@ -1,4 +1,4 @@ -const { BrowserWindow, globalShortcut, ipcMain, screen, app, shell, desktopCapturer } = require('electron'); +const { BrowserWindow, globalShortcut, screen, app, shell } = require('electron'); const WindowLayoutManager = require('./windowLayoutManager'); const SmoothMovementManager = require('./smoothMovementManager'); const path = require('node:path'); @@ -570,10 +570,7 @@ function createWindows() { } function setupIpcHandlers(movementManager) { - setupApiKeyIPC(); - // quit-application handler moved to windowBridge.js to avoid duplication - screen.on('display-added', (event, newDisplay) => { console.log('[Display] New display added:', newDisplay.id); }); @@ -591,387 +588,146 @@ function setupIpcHandlers(movementManager) { // console.log('[Display] Display metrics changed:', display.id, changedMetrics); updateLayout(); }); - - // Content protection handlers moved to windowBridge.js to avoid duplication - - ipcMain.on('header-state-changed', (event, state) => { - console.log(`[WindowManager] Header state changed to: ${state}`); - currentHeaderState = state; - - if (state === 'main') { - createFeatureWindows(windowPool.get('header')); - } else { // 'apikey' | 'permission' - destroyFeatureWindows(); - } - internalBridge.emit('reregister-shortcuts'); - }); - - // resize-header-window handler moved to windowBridge.js to avoid duplication - - ipcMain.on('header-animation-finished', (event, state) => { - const header = windowPool.get('header'); - if (!header || header.isDestroyed()) return; - - if (state === 'hidden') { - header.hide(); - console.log('[WindowManager] Header hidden after animation.'); - } else if (state === 'visible') { - console.log('[WindowManager] Header shown after animation.'); - updateLayout(); - } - }); - - ipcMain.handle('get-header-position', () => { - const header = windowPool.get('header'); - if (header) { - const [x, y] = header.getPosition(); - return { x, y }; - } - return { x: 0, y: 0 }; - }); - - ipcMain.handle('move-header', (event, newX, newY) => { - const header = windowPool.get('header'); - if (header) { - const currentY = newY !== undefined ? newY : header.getBounds().y; - header.setPosition(newX, currentY, false); - - updateLayout(); - } - }); - - ipcMain.handle('move-header-to', (event, newX, newY) => { - const header = windowPool.get('header'); - if (header) { - const targetDisplay = screen.getDisplayNearestPoint({ x: newX, y: newY }); - const { x: workAreaX, y: workAreaY, width, height } = targetDisplay.workArea; - const headerBounds = header.getBounds(); - - // Only clamp if the new position would actually go out of bounds - // This prevents progressive restriction of movement - let clampedX = newX; - let clampedY = newY; - - // Check if we need to clamp X position - if (newX < workAreaX) { - clampedX = workAreaX; - } else if (newX + headerBounds.width > workAreaX + width) { - clampedX = workAreaX + width - headerBounds.width; - } - - // Check if we need to clamp Y position - if (newY < workAreaY) { - clampedY = workAreaY; - } else if (newY + headerBounds.height > workAreaY + height) { - clampedY = workAreaY + height - headerBounds.height; - } - - header.setPosition(clampedX, clampedY, false); - - updateLayout(); - } - }); - - - // move-window-step handler moved to windowBridge.js to avoid duplication - - ipcMain.handle('adjust-window-height', (event, targetHeight) => { - const senderWindow = BrowserWindow.fromWebContents(event.sender); - if (senderWindow) { - const wasResizable = senderWindow.isResizable(); - if (!wasResizable) { - senderWindow.setResizable(true); - } - - const currentBounds = senderWindow.getBounds(); - const minHeight = senderWindow.getMinimumSize()[1]; - const maxHeight = senderWindow.getMaximumSize()[1]; - - let adjustedHeight; - if (maxHeight === 0) { - adjustedHeight = Math.max(minHeight, targetHeight); - } else { - adjustedHeight = Math.max(minHeight, Math.min(maxHeight, targetHeight)); - } - - senderWindow.setSize(currentBounds.width, adjustedHeight, false); - - if (!wasResizable) { - senderWindow.setResizable(false); - } - - updateLayout(); - } - }); - - ipcMain.handle('check-system-permissions', async () => { - const { systemPreferences } = require('electron'); - const permissions = { - microphone: 'unknown', - screen: 'unknown', - needsSetup: true - }; - - try { - if (process.platform === 'darwin') { - // Check microphone permission on macOS - const micStatus = systemPreferences.getMediaAccessStatus('microphone'); - console.log('[Permissions] Microphone status:', micStatus); - permissions.microphone = micStatus; - - // Check screen recording permission using the system API - const screenStatus = systemPreferences.getMediaAccessStatus('screen'); - console.log('[Permissions] Screen status:', screenStatus); - permissions.screen = screenStatus; - - permissions.needsSetup = micStatus !== 'granted' || screenStatus !== 'granted'; - } else { - permissions.microphone = 'granted'; - permissions.screen = 'granted'; - permissions.needsSetup = false; - } - - console.log('[Permissions] System permissions status:', permissions); - return permissions; - } catch (error) { - console.error('[Permissions] Error checking permissions:', error); - return { - microphone: 'unknown', - screen: 'unknown', - needsSetup: true, - error: error.message - }; - } - }); - - ipcMain.handle('request-microphone-permission', async () => { - if (process.platform !== 'darwin') { - return { success: true }; - } - - const { systemPreferences } = require('electron'); - try { - const status = systemPreferences.getMediaAccessStatus('microphone'); - console.log('[Permissions] Microphone status:', status); - if (status === 'granted') { - return { success: true, status: 'granted' }; - } - - // Req mic permission - const granted = await systemPreferences.askForMediaAccess('microphone'); - return { - success: granted, - status: granted ? 'granted' : 'denied' - }; - } catch (error) { - console.error('[Permissions] Error requesting microphone permission:', error); - return { - success: false, - error: error.message - }; - } - }); - - ipcMain.handle('open-system-preferences', async (event, section) => { - if (process.platform !== 'darwin') { - return { success: false, error: 'Not supported on this platform' }; - } - - try { - if (section === 'screen-recording') { - // First trigger screen capture request to register the app in system preferences - try { - console.log('[Permissions] Triggering screen capture request to register app...'); - await desktopCapturer.getSources({ - types: ['screen'], - thumbnailSize: { width: 1, height: 1 } - }); - console.log('[Permissions] App registered for screen recording'); - } catch (captureError) { - console.log('[Permissions] Screen capture request triggered (expected to fail):', captureError.message); - } - - // Then open system preferences - // await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'); - } - // if (section === 'microphone') { - // await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone'); - // } - return { success: true }; - } catch (error) { - console.error('[Permissions] Error opening system preferences:', error); - return { success: false, error: error.message }; - } - }); - - ipcMain.handle('mark-permissions-completed', async () => { - try { - // This is a system-level setting, not user-specific. - await permissionRepository.markPermissionsAsCompleted(); - console.log('[Permissions] Marked permissions as completed'); - return { success: true }; - } catch (error) { - console.error('[Permissions] Error marking permissions as completed:', error); - return { success: false, error: error.message }; - } - }); - - ipcMain.handle('check-permissions-completed', async () => { - try { - const completed = await permissionRepository.checkPermissionsCompleted(); - console.log('[Permissions] Permissions completed status:', completed); - return completed; - } catch (error) { - console.error('[Permissions] Error checking permissions completed status:', error); - return false; - } - }); - - ipcMain.handle('toggle-all-windows-visibility', () => toggleAllWindowsVisibility()); - - // ipcMain.handle('toggle-feature', async (event, featureName) => { - // return toggleFeature(featureName); - // }); - - ipcMain.on('animation-finished', (event) => { - const win = BrowserWindow.fromWebContents(event.sender); - if (win && !win.isDestroyed()) { - console.log(`[WindowManager] Hiding window after animation.`); - win.hide(); - } - }); - - - ipcMain.handle('ask:closeAskWindow', async () => { - const askWindow = windowPool.get('ask'); - if (askWindow) { - askWindow.webContents.send('window-hide-animation'); - } - }); } +const handleHeaderStateChanged = (state) => { + console.log(`[WindowManager] Header state changed to: ${state}`); + currentHeaderState = state; -// /** -// * -// * @param {'listen'|'ask'|'settings'} featureName -// * @param {{ -// * listen?: { targetVisibility?: 'show'|'hide' }, -// * ask?: { targetVisibility?: 'show'|'hide', questionText?: string }, -// * settings?: { targetVisibility?: 'show'|'hide' } -// * }} [options={}] -// */ -// async function toggleFeature(featureName, options = {}) { -// if (!windowPool.get(featureName) && currentHeaderState === 'main') { -// createFeatureWindows(windowPool.get('header')); -// } - -// if (featureName === 'ask') { -// let askWindow = windowPool.get('ask'); - -// if (!askWindow || askWindow.isDestroyed()) { -// console.log('[WindowManager] Ask window not found, creating new one'); -// return; -// } - -// const questionText = options?.ask?.questionText ?? null; -// const targetVisibility = options?.ask?.targetVisibility ?? null; -// if (askWindow.isVisible()) { -// if (questionText) { -// askWindow.webContents.send('ask:sendQuestionToRenderer', questionText); -// } else { -// updateLayout(); -// if (targetVisibility === 'show') { -// askWindow.webContents.send('ask:showTextInput'); -// } else { -// askWindow.webContents.send('window-hide-animation'); -// } -// } -// } else { -// console.log('[WindowManager] Showing hidden Ask window'); -// askWindow.show(); -// updateLayout(); -// if (questionText) { -// askWindow.webContents.send('ask:sendQuestionToRenderer', questionText); -// } -// askWindow.webContents.send('window-show-animation'); -// } -// } -// } - -async function toggleFeature(featureName, options = {}) { - if (!windowPool.get(featureName) && currentHeaderState === 'main') { + if (state === 'main') { createFeatureWindows(windowPool.get('header')); + } else { // 'apikey' | 'permission' + destroyFeatureWindows(); } + internalBridge.emit('reregister-shortcuts'); +}; - if (featureName === 'ask') { - let askWindow = windowPool.get('ask'); +const handleHeaderAnimationFinished = (state) => { + const header = windowPool.get('header'); + if (!header || header.isDestroyed()) return; - if (!askWindow || askWindow.isDestroyed()) { - console.log('[WindowManager] Ask window not found, creating new one'); - return; + if (state === 'hidden') { + header.hide(); + console.log('[WindowManager] Header hidden after animation.'); + } else if (state === 'visible') { + console.log('[WindowManager] Header shown after animation.'); + updateLayout(); + } +}; + +const getHeaderPosition = () => { + const header = windowPool.get('header'); + if (header) { + const [x, y] = header.getPosition(); + return { x, y }; + } + return { x: 0, y: 0 }; +}; + +const moveHeader = (newX, newY) => { + const header = windowPool.get('header'); + if (header) { + const currentY = newY !== undefined ? newY : header.getBounds().y; + header.setPosition(newX, currentY, false); + updateLayout(); + } +}; + +const moveHeaderTo = (newX, newY) => { + const header = windowPool.get('header'); + if (header) { + const targetDisplay = screen.getDisplayNearestPoint({ x: newX, y: newY }); + const { x: workAreaX, y: workAreaY, width, height } = targetDisplay.workArea; + const headerBounds = header.getBounds(); + + let clampedX = newX; + let clampedY = newY; + + if (newX < workAreaX) { + clampedX = workAreaX; + } else if (newX + headerBounds.width > workAreaX + width) { + clampedX = workAreaX + width - headerBounds.width; } - if (askWindow.isVisible()) { - askWindow.webContents.send('ask:showTextInput'); + + if (newY < workAreaY) { + clampedY = workAreaY; + } else if (newY + headerBounds.height > workAreaY + height) { + clampedY = workAreaY + height - headerBounds.height; + } + + header.setPosition(clampedX, clampedY, false); + updateLayout(); + } +}; + +const adjustWindowHeight = (sender, targetHeight) => { + const senderWindow = BrowserWindow.fromWebContents(sender); + if (senderWindow) { + const wasResizable = senderWindow.isResizable(); + if (!wasResizable) { + senderWindow.setResizable(true); + } + + const currentBounds = senderWindow.getBounds(); + const minHeight = senderWindow.getMinimumSize()[1]; + const maxHeight = senderWindow.getMaximumSize()[1]; + + let adjustedHeight; + if (maxHeight === 0) { + adjustedHeight = Math.max(minHeight, targetHeight); } else { - console.log('[WindowManager] Showing hidden Ask window'); - askWindow.show(); - updateLayout(); - askWindow.webContents.send('window-show-animation'); + adjustedHeight = Math.max(minHeight, Math.min(maxHeight, targetHeight)); } + + senderWindow.setSize(currentBounds.width, adjustedHeight, false); + + if (!wasResizable) { + senderWindow.setResizable(false); + } + + updateLayout(); + } +}; + +const handleAnimationFinished = (sender) => { + const win = BrowserWindow.fromWebContents(sender); + if (win && !win.isDestroyed()) { + console.log(`[WindowManager] Hiding window after animation.`); + win.hide(); + } +}; + +const closeAskWindow = () => { + const askWindow = windowPool.get('ask'); + if (askWindow) { + askWindow.webContents.send('window-hide-animation'); + } +}; + +async function ensureAskWindowVisible() { + if (currentHeaderState !== 'main') { + console.log('[WindowManager] Not in main state, skipping ensureAskWindowVisible'); + return; + } + + let askWindow = windowPool.get('ask'); + + if (!askWindow || askWindow.isDestroyed()) { + console.log('[WindowManager] Ask window not found, creating new one'); + createFeatureWindows(windowPool.get('header'), 'ask'); + askWindow = windowPool.get('ask'); + } + + if (!askWindow.isVisible()) { + console.log('[WindowManager] Showing hidden Ask window'); + askWindow.show(); + updateLayout(); + askWindow.webContents.send('window-show-animation'); } } - //////// after_modelStateService //////// -async function getStoredApiKey() { - if (global.modelStateService) { - const provider = await getStoredProvider(); - return global.modelStateService.getApiKey(provider); - } - return null; // Fallback -} - -async function getStoredProvider() { - if (global.modelStateService) { - return global.modelStateService.getCurrentProvider('llm'); - } - return 'openai'; // Fallback -} - -/** - * - * @param {IpcMainInvokeEvent} event - * @param {{type: 'llm' | 'stt'}} - */ -async function getCurrentModelInfo(event, { type }) { - if (global.modelStateService && (type === 'llm' || type === 'stt')) { - return global.modelStateService.getCurrentModelInfo(type); - } - return null; -} - -function setupApiKeyIPC() { - const { ipcMain } = require('electron'); - - ipcMain.handle('get-stored-api-key', getStoredApiKey); - ipcMain.handle('get-ai-provider', getStoredProvider); - ipcMain.handle('get-current-model-info', getCurrentModelInfo); - - ipcMain.handle('api-key-validated', async (event, data) => { - console.warn("[DEPRECATED] 'api-key-validated' IPC was called. This logic is now handled by 'model:validate-key'."); - return { success: true }; - }); - - ipcMain.handle('remove-api-key', async () => { - console.warn("[DEPRECATED] 'remove-api-key' IPC was called. This is now handled by 'model:remove-api-key'."); - return { success: true }; - }); - - console.log('[WindowManager] API key related IPC handlers have been updated for ModelStateService.'); -} -//////// after_modelStateService //////// - const closeWindow = (windowName) => { const win = windowPool.get(windowName); @@ -985,10 +741,6 @@ module.exports = { createWindows, windowPool, fixedYPosition, - getStoredApiKey, - getStoredProvider, - getCurrentModelInfo, - toggleFeature, // Export toggleFeature so shortcutsService can use it toggleContentProtection, resizeHeaderWindow, getContentProtectionStatus, @@ -999,4 +751,14 @@ module.exports = { openLoginPage, moveWindowStep, closeWindow, + toggleAllWindowsVisibility, + handleHeaderStateChanged, + handleHeaderAnimationFinished, + getHeaderPosition, + moveHeader, + moveHeaderTo, + adjustWindowHeight, + handleAnimationFinished, + closeAskWindow, + ensureAskWindowVisible, }; \ No newline at end of file