diff --git a/src/bridge/featureBridge.js b/src/bridge/featureBridge.js index 8f08895..a323736 100644 --- a/src/bridge/featureBridge.js +++ b/src/bridge/featureBridge.js @@ -1,34 +1,90 @@ // src/bridge/featureBridge.js -const { ipcMain } = require('electron'); +const { ipcMain, app } = require('electron'); const settingsService = require('../features/settings/settingsService'); +const authService = require('../features/common/services/authService'); +const whisperService = require('../features/common/services/whisperService'); +const ollamaService = require('../features/common/services/ollamaService'); +const modelStateService = require('../features/common/services/modelStateService'); + const askService = require('../features/ask/askService'); const listenService = require('../features/listen/listenService'); module.exports = { // Renderer로부터의 요청을 수신 initialize() { - ipcMain.handle('settings:getPresets', async () => { - console.log('[FeatureBridge] settings:getPresets 호출됨'); - return await settingsService.getPresets(); - }); - - ipcMain.handle('settings:get-auto-update', async () => { - console.log('[FeatureBridge] settings:get-auto-update 호출됨'); - return await settingsService.getAutoUpdateSetting(); - }); - - ipcMain.handle('settings:set-auto-update', async (event, isEnabled) => { - console.log('[FeatureBridge] settings:set-auto-update 호출됨', isEnabled); - return await settingsService.setAutoUpdateSetting(isEnabled); - }); - - // New IPC handler for loadInitialData - ipcMain.handle('settings:loadInitialData', async () => { - console.log('[FeatureBridge] settings:loadInitialData called'); - return await settingsService.loadInitialData(); - }); - console.log('[FeatureBridge] Initialized with settings handlers.'); + // Settings Service + ipcMain.handle('settings:getPresets', async () => await settingsService.getPresets()); + ipcMain.handle('settings:get-auto-update', async () => await settingsService.getAutoUpdateSetting()); + ipcMain.handle('settings:set-auto-update', async (event, isEnabled) => await settingsService.setAutoUpdateSetting(isEnabled)); + ipcMain.handle('settings:get-model-settings', async () => await settingsService.getModelSettings()); + ipcMain.handle('settings:validate-and-save-key', async (e, { provider, key }) => await settingsService.validateAndSaveKey(provider, key)); + ipcMain.handle('settings:clear-api-key', async (e, { provider }) => await settingsService.clearApiKey(provider)); + ipcMain.handle('settings:set-selected-model', async (e, { type, modelId }) => await settingsService.setSelectedModel(type, modelId)); + + ipcMain.handle('settings:get-ollama-status', async () => await settingsService.getOllamaStatus()); + ipcMain.handle('settings:ensure-ollama-ready', async () => await settingsService.ensureOllamaReady()); + ipcMain.handle('settings:shutdown-ollama', async () => await settingsService.shutdownOllama()); + + // User/Auth + ipcMain.handle('get-current-user', () => authService.getCurrentUser()); + ipcMain.handle('start-firebase-auth', async () => await authService.startFirebaseAuthFlow()); + ipcMain.handle('firebase-logout', async () => await authService.signOut()); + + // App + ipcMain.handle('quit-application', () => app.quit()); + + // Whisper + ipcMain.handle('whisper:download-model', async (event, modelId) => await whisperService.handleDownloadModel(event, modelId)); + ipcMain.handle('whisper:get-installed-models', async () => await whisperService.handleGetInstalledModels()); + + // General + ipcMain.handle('get-preset-templates', () => presetRepository.getPresetTemplates()); + ipcMain.handle('get-web-url', () => process.env.pickleglass_WEB_URL || 'http://localhost:3000'); + + // Ollama + ipcMain.handle('ollama:get-status', async () => await ollamaService.handleGetStatus()); + ipcMain.handle('ollama:install', async (event) => await ollamaService.handleInstall(event)); + ipcMain.handle('ollama:start-service', async (event) => await ollamaService.handleStartService(event)); + ipcMain.handle('ollama:ensure-ready', async () => await ollamaService.handleEnsureReady()); + ipcMain.handle('ollama:get-models', async () => await ollamaService.handleGetModels()); + ipcMain.handle('ollama:get-model-suggestions', async () => await ollamaService.handleGetModelSuggestions()); + ipcMain.handle('ollama:pull-model', async (event, modelName) => await ollamaService.handlePullModel(event, modelName)); + ipcMain.handle('ollama:is-model-installed', async (event, modelName) => await ollamaService.handleIsModelInstalled(modelName)); + ipcMain.handle('ollama:warm-up-model', async (event, modelName) => await ollamaService.handleWarmUpModel(modelName)); + ipcMain.handle('ollama:auto-warm-up', async () => await ollamaService.handleAutoWarmUp()); + ipcMain.handle('ollama:get-warm-up-status', async () => await ollamaService.handleGetWarmUpStatus()); + ipcMain.handle('ollama:shutdown', async (event, force = false) => await ollamaService.handleShutdown(event, force)); + + // Ask + ipcMain.handle('ask:sendMessage', async (event, userPrompt, conversationHistoryRaw = []) => await askService.sendMessage(userPrompt, conversationHistoryRaw)); + + // Listen + ipcMain.handle('send-audio-content', async (event, { data, mimeType }) => await listenService.handleSendAudioContent(data, mimeType)); + ipcMain.handle('send-system-audio-content', async (event, { data, mimeType }) => { + const result = await listenService.sttService.sendSystemAudioContent(data, mimeType); + if(result.success) { + listenService.sendToRenderer('system-audio-data', { data }); + } + return result; + }); + ipcMain.handle('start-macos-audio', async () => await listenService.handleStartMacosAudio()); + ipcMain.handle('stop-macos-audio', async () => await listenService.handleStopMacosAudio()); + ipcMain.handle('update-google-search-setting', async (event, enabled) => await listenService.handleUpdateGoogleSearchSetting(enabled)); + + // ModelStateService + 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: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()); + + console.log('[FeatureBridge] Initialized with all feature handlers.'); ipcMain.handle('listen:changeSession', async (event, listenButtonText) => { diff --git a/src/bridge/windowBridge.js b/src/bridge/windowBridge.js index 2b0ae06..825e1f4 100644 --- a/src/bridge/windowBridge.js +++ b/src/bridge/windowBridge.js @@ -1,10 +1,155 @@ // src/bridge/windowBridge.js -const { ipcMain, BrowserWindow } = require('electron'); -const { windowPool, settingsHideTimer, app, shell } = require('../window/windowManager'); // 필요 변수 require +const { ipcMain, BrowserWindow, globalShortcut } = require('electron'); module.exports = { - // Renderer로부터의 요청을 수신 - initialize() { + // windowManager에서 필요한 변수들을 매개변수로 받도록 수정 + initialize(windowPool, app, shell, getCurrentDisplay, createFeatureWindows, movementManager, getContentProtectionStatus, setContentProtection, updateLayout) { + let settingsHideTimer = null; + + // 기존 + ipcMain.on('window:hide', (e) => BrowserWindow.fromWebContents(e.sender)?.hide()); + + // windowManager 관련 추가 + ipcMain.handle('toggle-content-protection', () => { + // windowManager의 toggle-content-protection 로직 + const newStatus = !getContentProtectionStatus(); + setContentProtection(newStatus); + return newStatus; + }); + + + ipcMain.handle('resize-header-window', (event, { width, height }) => { + const header = windowPool.get('header'); + if (header) { + console.log(`[WindowBridge] Resize request: ${width}x${height}`); + + // Prevent resizing during animations or if already at target size + if (movementManager && movementManager.isAnimating) { + console.log('[WindowBridge] Skipping resize during animation'); + return { success: false, error: 'Cannot resize during animation' }; + } + + const currentBounds = header.getBounds(); + console.log(`[WindowBridge] Current bounds: ${currentBounds.width}x${currentBounds.height} at (${currentBounds.x}, ${currentBounds.y})`); + + // Skip if already at target size to prevent unnecessary operations + if (currentBounds.width === width && currentBounds.height === height) { + console.log('[WindowBridge] Already at target size, skipping resize'); + return { success: true }; + } + + const wasResizable = header.isResizable(); + if (!wasResizable) { + header.setResizable(true); + } + + // Calculate the center point of the current window + const centerX = currentBounds.x + currentBounds.width / 2; + // Calculate new X position to keep the window centered + const newX = Math.round(centerX - width / 2); + + // Get the current display to ensure we stay within bounds + const display = getCurrentDisplay(header); + const { x: workAreaX, width: workAreaWidth } = display.workArea; + + // Clamp the new position to stay within display bounds + const clampedX = Math.max(workAreaX, Math.min(workAreaX + workAreaWidth - width, newX)); + + header.setBounds({ x: clampedX, y: currentBounds.y, width, height }); + + if (!wasResizable) { + header.setResizable(false); + } + + // Update layout after resize + if (updateLayout) { + updateLayout(); + } + + return { success: true }; + } + return { success: false, error: 'Header window not found' }; + }); + + ipcMain.handle('get-content-protection-status', () => { + return getContentProtectionStatus(); + }); + + ipcMain.handle('open-shortcut-editor', () => { + // open-shortcut-editor 로직 + const header = windowPool.get('header'); + if (!header) return; + globalShortcut.unregisterAll(); + createFeatureWindows(header, 'shortcut-settings'); + }); + + + // 추가: show-settings-window + ipcMain.on('show-settings-window', (event, bounds) => { + if (!bounds) return; + const win = windowPool.get('settings'); + if (win && !win.isDestroyed()) { + if (settingsHideTimer) { + clearTimeout(settingsHideTimer); + settingsHideTimer = null; + } + // 위치 조정 로직 + const header = windowPool.get('header'); + const headerBounds = header?.getBounds() ?? { x: 0, y: 0 }; + const settingsBounds = win.getBounds(); + const disp = getCurrentDisplay(header); + const { x: waX, y: waY, width: waW, height: waH } = disp.workArea; + let x = Math.round(headerBounds.x + (bounds?.x ?? 0) + (bounds?.width ?? 0) / 2 - settingsBounds.width / 2); + let y = Math.round(headerBounds.y + (bounds?.y ?? 0) + (bounds?.height ?? 0) + 31); + x = Math.max(waX + 10, Math.min(waX + waW - settingsBounds.width - 10, x)); + y = Math.max(waY + 10, Math.min(waY + waH - settingsBounds.height - 10, y)); + win.setBounds({ x, y }); + win.__lockedByButton = true; + win.show(); + win.moveTop(); + win.setAlwaysOnTop(true); + } + }); + + ipcMain.on('hide-settings-window', (event) => { + const window = windowPool.get("settings"); + if (window && !window.isDestroyed()) { + if (settingsHideTimer) { + clearTimeout(settingsHideTimer); + } + settingsHideTimer = setTimeout(() => { + if (window && !window.isDestroyed()) { + window.setAlwaysOnTop(false); + window.hide(); + } + settingsHideTimer = null; + }, 200); + + window.__lockedByButton = false; + } + }); + + ipcMain.on('cancel-hide-settings-window', (event) => { + if (settingsHideTimer) { + clearTimeout(settingsHideTimer); + settingsHideTimer = null; + } + }); + + // 로그인 페이지 열기 + ipcMain.handle('open-login-page', () => { + const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000'; + const personalizeUrl = `${webUrl}/personalize?desktop=true`; + shell.openExternal(personalizeUrl); + console.log('Opening personalization page:', personalizeUrl); + }); + + // 윈도우 이동 + ipcMain.handle('move-window-step', (event, direction) => { + if (movementManager) { + movementManager.moveStep(direction); + } + }); }, // Renderer로 상태를 전송 diff --git a/src/features/ask/askService.js b/src/features/ask/askService.js index 92f6f21..537243a 100644 --- a/src/features/ask/askService.js +++ b/src/features/ask/askService.js @@ -1,4 +1,4 @@ -const { ipcMain, BrowserWindow } = require('electron'); +const { BrowserWindow } = require('electron'); const { createStreamingLLM } = require('../common/ai/factory'); const { getCurrentModelInfo, windowPool, captureScreenshot } = require('../../window/windowManager'); const sessionRepository = require('../common/repositories/session'); @@ -17,17 +17,6 @@ class AskService { console.log('[AskService] Service instance created.'); } - /** - * IPC 리스너를 등록하여 렌더러 프로세스로부터의 요청을 처리합니다. - * Electron 애플리케이션의 메인 프로세스에서 한 번만 호출되어야 합니다. - */ - initialize() { - ipcMain.handle('ask:sendMessage', async (event, userPrompt, conversationHistoryRaw=[]) => { - return this.sendMessage(userPrompt, conversationHistoryRaw); - }); - console.log('[AskService] Initialized and ready.'); - } - /** * 대화 기록 배열을 프롬프트에 적합한 단일 문자열로 변환합니다. * @param {string[]} conversationTexts - 대화 내용 문자열의 배열 diff --git a/src/features/common/services/authService.js b/src/features/common/services/authService.js index 335c769..19cde51 100644 --- a/src/features/common/services/authService.js +++ b/src/features/common/services/authService.js @@ -1,5 +1,5 @@ const { onAuthStateChanged, signInWithCustomToken, signOut } = require('firebase/auth'); -const { BrowserWindow } = require('electron'); +const { BrowserWindow, shell } = require('electron'); const { getFirebaseAuth } = require('./firebaseClient'); const fetch = require('node-fetch'); const encryptionService = require('./encryptionService'); @@ -131,6 +131,19 @@ class AuthService { return this.initializationPromise; } + async startFirebaseAuthFlow() { + try { + const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000'; + const authUrl = `${webUrl}/login?mode=electron`; + console.log(`[AuthService] Opening Firebase auth URL in browser: ${authUrl}`); + await shell.openExternal(authUrl); + return { success: true }; + } catch (error) { + console.error('[AuthService] Failed to open Firebase auth URL:', error); + return { success: false, error: error.message }; + } + } + async signInWithCustomToken(token) { const auth = getFirebaseAuth(); try { diff --git a/src/features/common/services/localProgressTracker.js b/src/features/common/services/localProgressTracker.js index 48f5fcb..454b431 100644 --- a/src/features/common/services/localProgressTracker.js +++ b/src/features/common/services/localProgressTracker.js @@ -2,10 +2,10 @@ export class LocalProgressTracker { constructor(serviceName) { this.serviceName = serviceName; this.activeOperations = new Map(); // operationId -> { controller, onProgress } - this.ipcRenderer = window.require?.('electron')?.ipcRenderer; - if (!this.ipcRenderer) { - throw new Error(`${serviceName} requires Electron environment`); + // Check if we're in renderer process with window.api available + if (!window.api) { + throw new Error(`${serviceName} requires Electron environment with contextBridge`); } this.globalProgressHandler = (event, data) => { @@ -15,14 +15,14 @@ export class LocalProgressTracker { } }; - const progressEvents = { - 'ollama': 'ollama:pull-progress', - 'whisper': 'whisper:download-progress' - }; + // Set up progress listeners based on service name + if (serviceName.toLowerCase() === 'ollama') { + window.api.settingsView.onOllamaPullProgress(this.globalProgressHandler); + } else if (serviceName.toLowerCase() === 'whisper') { + window.api.settingsView.onWhisperDownloadProgress(this.globalProgressHandler); + } - const eventName = progressEvents[serviceName.toLowerCase()] || `${serviceName}:progress`; - this.progressEvent = eventName; - this.ipcRenderer.on(eventName, this.globalProgressHandler); + this.progressEvent = serviceName.toLowerCase(); } async trackOperation(operationId, operationType, onProgress) { @@ -35,15 +35,16 @@ export class LocalProgressTracker { this.activeOperations.set(operationId, operation); try { - const ipcChannels = { - 'ollama': { install: 'ollama:pull-model' }, - 'whisper': { download: 'whisper:download-model' } - }; + let result; - const channel = ipcChannels[this.serviceName.toLowerCase()]?.[operationType] || - `${this.serviceName}:${operationType}`; - - const result = await this.ipcRenderer.invoke(channel, operationId); + // Use appropriate API call based on service and operation + if (this.serviceName.toLowerCase() === 'ollama' && operationType === 'install') { + result = await window.api.settingsView.pullOllamaModel(operationId); + } else if (this.serviceName.toLowerCase() === 'whisper' && operationType === 'download') { + result = await window.api.settingsView.downloadWhisperModel(operationId); + } else { + throw new Error(`Unsupported operation: ${this.serviceName}:${operationType}`); + } if (!result.success) { throw new Error(result.error || `${operationType} failed`); @@ -93,8 +94,12 @@ export class LocalProgressTracker { destroy() { this.cancelAllOperations(); - if (this.ipcRenderer) { - this.ipcRenderer.removeListener(this.progressEvent, this.globalProgressHandler); + + // Remove progress listeners based on service name + if (this.progressEvent === 'ollama') { + window.api.settingsView.removeOnOllamaPullProgress(this.globalProgressHandler); + } else if (this.progressEvent === 'whisper') { + window.api.settingsView.removeOnWhisperDownloadProgress(this.globalProgressHandler); } } } diff --git a/src/features/common/services/modelStateService.js b/src/features/common/services/modelStateService.js index 14866d2..f2fa1c3 100644 --- a/src/features/common/services/modelStateService.js +++ b/src/features/common/services/modelStateService.js @@ -24,7 +24,6 @@ class ModelStateService { async initialize() { console.log('[ModelStateService] Initializing...'); await this._loadStateForCurrentUser(); - this.setupIpcHandlers(); console.log('[ModelStateService] Initialization complete'); } @@ -37,15 +36,17 @@ class ModelStateService { console.log(`[ModelStateService] Current Selection -> LLM: ${llmModel || 'None'} (Provider: ${llmProvider}), STT: ${sttModel || 'None'} (Provider: ${sttProvider})`); } - _autoSelectAvailableModels() { - console.log('[ModelStateService] Running auto-selection for models...'); + _autoSelectAvailableModels(forceReselectionForTypes = []) { + console.log(`[ModelStateService] Running auto-selection for models. Force re-selection for: [${forceReselectionForTypes.join(', ')}]`); const types = ['llm', 'stt']; types.forEach(type => { const currentModelId = this.state.selectedModels[type]; let isCurrentModelValid = false; - if (currentModelId) { + const forceReselection = forceReselectionForTypes.includes(type); + + if (currentModelId && !forceReselection) { const provider = this.getProviderForModel(type, currentModelId); const apiKey = this.getApiKey(provider); // For Ollama, 'local' is a valid API key @@ -55,7 +56,7 @@ class ModelStateService { } if (!isCurrentModelValid) { - console.log(`[ModelStateService] No valid ${type.toUpperCase()} model selected. Finding an alternative...`); + console.log(`[ModelStateService] No valid ${type.toUpperCase()} model selected or re-selection forced. Finding an alternative...`); const availableModels = this.getAvailableModels(type); if (availableModels.length > 0) { // Prefer API providers over local providers for auto-selection @@ -329,10 +330,20 @@ class ModelStateService { this._logCurrentSelection(); } - setApiKey(provider, key) { + async setApiKey(provider, key) { if (provider in this.state.apiKeys) { this.state.apiKeys[provider] = key; - this._saveState(); + + const supportedTypes = []; + if (PROVIDERS[provider]?.llmModels.length > 0 || provider === 'ollama') { + supportedTypes.push('llm'); + } + if (PROVIDERS[provider]?.sttModels.length > 0 || provider === 'whisper') { + supportedTypes.push('stt'); + } + + this._autoSelectAvailableModels(supportedTypes); + await this._saveState(); return true; } return false; @@ -395,6 +406,8 @@ class ModelStateService { areProvidersConfigured() { if (this.isLoggedInWithFirebase()) return true; + console.log('[DEBUG] Checking configured providers with apiKeys state:', JSON.stringify(this.state.apiKeys, (key, value) => (value ? '***' : null), 2)); + // LLM과 STT 모델을 제공하는 Provider 중 하나라도 API 키가 설정되었는지 확인 const hasLlmKey = Object.entries(this.state.apiKeys).some(([provider, key]) => { if (provider === 'ollama') { @@ -523,10 +536,7 @@ class ModelStateService { if (result.success) { // Use 'local' as placeholder for local services const finalKey = (provider === 'ollama' || provider === 'whisper') ? 'local' : key; - this.setApiKey(provider, finalKey); - // After setting the key, auto-select models - this._autoSelectAvailableModels(); - this._saveState(); // Ensure state is saved after model selection + await this.setApiKey(provider, finalKey); } return result; } @@ -569,26 +579,6 @@ class ModelStateService { return { provider, model, apiKey }; } - setupIpcHandlers() { - ipcMain.handle('model:validate-key', async (e, { provider, key }) => this.handleValidateKey(provider, key)); - ipcMain.handle('model:get-all-keys', () => this.getAllApiKeys()); - ipcMain.handle('model:set-api-key', async (e, { provider, key }) => { - const success = this.setApiKey(provider, key); - if (success) { - this._autoSelectAvailableModels(); - await this._saveState(); - } - return success; - }); - ipcMain.handle('model:remove-api-key', async (e, { provider }) => this.handleRemoveApiKey(provider)); - ipcMain.handle('model:get-selected-models', () => this.getSelectedModels()); - ipcMain.handle('model:set-selected-model', async (e, { type, modelId }) => this.handleSetSelectedModel(type, modelId)); - ipcMain.handle('model:get-available-models', (e, { type }) => this.getAvailableModels(type)); - ipcMain.handle('model:are-providers-configured', () => this.areProvidersConfigured()); - ipcMain.handle('model:get-current-model-info', (e, { type }) => this.getCurrentModelInfo(type)); - - ipcMain.handle('model:get-provider-config', () => this.getProviderConfig()); - } } // Export singleton instance diff --git a/src/features/common/services/ollamaService.js b/src/features/common/services/ollamaService.js index 7fb6b1c..8506651 100644 --- a/src/features/common/services/ollamaService.js +++ b/src/features/common/services/ollamaService.js @@ -7,6 +7,7 @@ const { app } = require('electron'); const LocalAIServiceBase = require('./localAIServiceBase'); const { spawnAsync } = require('../utils/spawnHelper'); const { DOWNLOAD_CHECKSUMS } = require('../config/checksums'); +const ollamaModelRepository = require('../repositories/ollamaModel'); class OllamaService extends LocalAIServiceBase { constructor() { @@ -822,6 +823,183 @@ class OllamaService extends LocalAIServiceBase { return models; } + + async handleGetStatus() { + try { + const installed = await this.isInstalled(); + if (!installed) { + return { success: true, installed: false, running: false, models: [] }; + } + + const running = await this.isServiceRunning(); + if (!running) { + return { success: true, installed: true, running: false, models: [] }; + } + + const models = await this.getAllModelsWithStatus(); + return { success: true, installed: true, running: true, models }; + } catch (error) { + console.error('[OllamaService] Error getting status:', error); + return { success: false, error: error.message, installed: false, running: false, models: [] }; + } + } + + async handleInstall(event) { + try { + const onProgress = (data) => { + event.sender.send('ollama:install-progress', data); + }; + + await this.autoInstall(onProgress); + + if (!await this.isServiceRunning()) { + onProgress({ stage: 'starting', message: 'Starting Ollama service...', progress: 0 }); + await this.startService(); + onProgress({ stage: 'starting', message: 'Ollama service started.', progress: 100 }); + } + event.sender.send('ollama:install-complete', { success: true }); + return { success: true }; + } catch (error) { + console.error('[OllamaService] Failed to install:', error); + event.sender.send('ollama:install-complete', { success: false, error: error.message }); + return { success: false, error: error.message }; + } + } + + async handleStartService(event) { + try { + if (!await this.isServiceRunning()) { + console.log('[OllamaService] Starting Ollama service...'); + await this.startService(); + } + event.sender.send('ollama:install-complete', { success: true }); + return { success: true }; + } catch (error) { + console.error('[OllamaService] Failed to start service:', error); + event.sender.send('ollama:install-complete', { success: false, error: error.message }); + return { success: false, error: error.message }; + } + } + + async handleEnsureReady() { + try { + if (await this.isInstalled() && !await this.isServiceRunning()) { + console.log('[OllamaService] Ollama installed but not running, starting service...'); + await this.startService(); + } + return { success: true }; + } catch (error) { + console.error('[OllamaService] Failed to ensure ready:', error); + return { success: false, error: error.message }; + } + } + + async handleGetModels() { + try { + const models = await this.getAllModelsWithStatus(); + return { success: true, models }; + } catch (error) { + console.error('[OllamaService] Failed to get models:', error); + return { success: false, error: error.message }; + } + } + + async handleGetModelSuggestions() { + try { + const suggestions = await this.getModelSuggestions(); + return { success: true, suggestions }; + } catch (error) { + console.error('[OllamaService] Failed to get model suggestions:', error); + return { success: false, error: error.message }; + } + } + + async handlePullModel(event, modelName) { + try { + console.log(`[OllamaService] Starting model pull: ${modelName}`); + + await ollamaModelRepository.updateInstallStatus(modelName, false, true); + + const progressHandler = (data) => { + if (data.model === modelName) { + event.sender.send('ollama:pull-progress', data); + } + }; + + const completeHandler = (data) => { + if (data.model === modelName) { + console.log(`[OllamaService] Model ${modelName} pull completed`); + this.removeListener('pull-progress', progressHandler); + this.removeListener('pull-complete', completeHandler); + } + }; + + this.on('pull-progress', progressHandler); + this.on('pull-complete', completeHandler); + + await this.pullModel(modelName); + + await ollamaModelRepository.updateInstallStatus(modelName, true, false); + + console.log(`[OllamaService] Model ${modelName} pull successful`); + return { success: true }; + } catch (error) { + console.error('[OllamaService] Failed to pull model:', error); + await ollamaModelRepository.updateInstallStatus(modelName, false, false); + return { success: false, error: error.message }; + } + } + + async handleIsModelInstalled(modelName) { + try { + const installed = await this.isModelInstalled(modelName); + return { success: true, installed }; + } catch (error) { + console.error('[OllamaService] Failed to check model installation:', error); + return { success: false, error: error.message }; + } + } + + async handleWarmUpModel(modelName) { + try { + const success = await this.warmUpModel(modelName); + return { success }; + } catch (error) { + console.error('[OllamaService] Failed to warm up model:', error); + return { success: false, error: error.message }; + } + } + + async handleAutoWarmUp() { + try { + const success = await this.autoWarmUpSelectedModel(); + return { success }; + } catch (error) { + console.error('[OllamaService] Failed to auto warm-up:', error); + return { success: false, error: error.message }; + } + } + + async handleGetWarmUpStatus() { + try { + const status = this.getWarmUpStatus(); + return { success: true, status }; + } catch (error) { + console.error('[OllamaService] Failed to get warm-up status:', error); + return { success: false, error: error.message }; + } + } + + async handleShutdown(event, force = false) { + try { + console.log(`[OllamaService] Manual shutdown requested (force: ${force})`); + const success = await this.shutdown(force); + return { success }; + } catch (error) { + console.error('[OllamaService] Failed to shutdown Ollama:', error); + return { success: false, error: error.message }; + } + } } // Export singleton instance diff --git a/src/features/common/services/whisperService.js b/src/features/common/services/whisperService.js index 1b70ca6..5d14f44 100644 --- a/src/features/common/services/whisperService.js +++ b/src/features/common/services/whisperService.js @@ -169,6 +169,47 @@ class WhisperService extends LocalAIServiceBase { console.log(`[WhisperService] Model ${modelId} downloaded successfully`); } + async handleDownloadModel(event, modelId) { + try { + console.log(`[WhisperService] Handling download for model: ${modelId}`); + + if (!this.isInitialized) { + await this.initialize(); + } + + const progressHandler = (data) => { + if (data.modelId === modelId && event && event.sender) { + event.sender.send('whisper:download-progress', data); + } + }; + + this.on('downloadProgress', progressHandler); + + try { + await this.ensureModelAvailable(modelId); + } finally { + this.removeListener('downloadProgress', progressHandler); + } + + return { success: true }; + } catch (error) { + console.error(`[WhisperService] Failed to handle download for model ${modelId}:`, error); + return { success: false, error: error.message }; + } + } + + async handleGetInstalledModels() { + try { + if (!this.isInitialized) { + await this.initialize(); + } + const models = await this.getInstalledModels(); + return { success: true, models }; + } catch (error) { + console.error('[WhisperService] Failed to get installed models:', error); + return { success: false, error: error.message }; + } + } async getModelPath(modelId) { if (!this.isInitialized || !this.modelsDir) { diff --git a/src/features/listen/listenService.js b/src/features/listen/listenService.js index d50ee21..215210a 100644 --- a/src/features/listen/listenService.js +++ b/src/features/listen/listenService.js @@ -1,4 +1,4 @@ -const { ipcMain, BrowserWindow } = require('electron'); +const { BrowserWindow } = require('electron'); const SttService = require('./stt/sttService'); const SummaryService = require('./summary/summaryService'); const authService = require('../common/services/authService'); @@ -263,70 +263,57 @@ class ListenService { return this.summaryService.getConversationHistory(); } - setupIpcHandlers() { - ipcMain.handle('send-audio-content', async (event, { data, mimeType }) => { + _createHandler(asyncFn, successMessage, errorMessage) { + return async (...args) => { try { - await this.sendAudioContent(data, mimeType); - return { success: true }; + const result = await asyncFn.apply(this, args); + if (successMessage) console.log(successMessage); + // `startMacOSAudioCapture`는 성공 시 { success, error } 객체를 반환하지 않으므로, + // 핸들러가 일관된 응답을 보내도록 여기서 success 객체를 반환합니다. + // 다른 함수들은 이미 success 객체를 반환합니다. + return result && typeof result.success !== 'undefined' ? result : { success: true }; } catch (e) { - console.error('Error sending user audio:', e); + console.error(errorMessage, e); return { success: false, error: e.message }; } - }); + }; + } - ipcMain.handle('send-system-audio-content', async (event, { data, mimeType }) => { - try { - await this.sttService.sendSystemAudioContent(data, mimeType); - - // Send system audio data back to renderer for AEC reference (like macOS does) - this.sendToRenderer('system-audio-data', { data }); - - return { success: true }; - } catch (error) { - console.error('Error sending system audio:', error); - return { success: false, error: error.message }; - } - }); + // `_createHandler`를 사용하여 핸들러들을 동적으로 생성합니다. + handleSendAudioContent = this._createHandler( + this.sendAudioContent, + null, + 'Error sending user audio:' + ); - ipcMain.handle('start-macos-audio', async () => { + handleStartMacosAudio = this._createHandler( + async () => { if (process.platform !== 'darwin') { return { success: false, error: 'macOS audio capture only available on macOS' }; } if (this.sttService.isMacOSAudioRunning?.()) { return { success: false, error: 'already_running' }; } + await this.startMacOSAudioCapture(); + return { success: true, error: null }; + }, + 'macOS audio capture started.', + 'Error starting macOS audio capture:' + ); + + handleStopMacosAudio = this._createHandler( + this.stopMacOSAudioCapture, + 'macOS audio capture stopped.', + 'Error stopping macOS audio capture:' + ); - try { - const success = await this.startMacOSAudioCapture(); - return { success, error: null }; - } catch (error) { - console.error('Error starting macOS audio capture:', error); - return { success: false, error: error.message }; - } - }); - - ipcMain.handle('stop-macos-audio', async () => { - try { - this.stopMacOSAudioCapture(); - return { success: true }; - } catch (error) { - console.error('Error stopping macOS audio capture:', error); - return { success: false, error: error.message }; - } - }); - - ipcMain.handle('update-google-search-setting', async (event, enabled) => { - try { - console.log('Google Search setting updated to:', enabled); - return { success: true }; - } catch (error) { - console.error('Error updating Google Search setting:', error); - return { success: false, error: error.message }; - } - }); - - console.log('✅ Listen service IPC handlers registered'); - } + handleUpdateGoogleSearchSetting = this._createHandler( + async (enabled) => { + console.log('Google Search setting updated to:', enabled); + }, + null, + 'Error updating Google Search setting:' + ); } const listenService = new ListenService(); diff --git a/src/features/listen/stt/sttService.js b/src/features/listen/stt/sttService.js index c6dcf45..fb317e9 100644 --- a/src/features/listen/stt/sttService.js +++ b/src/features/listen/stt/sttService.js @@ -41,6 +41,17 @@ class SttService { }); } + async handleSendSystemAudioContent(data, mimeType) { + try { + await this.sendSystemAudioContent(data, mimeType); + this.sendToRenderer('system-audio-data', { data }); + return { success: true }; + } catch (error) { + console.error('Error sending system audio:', error); + return { success: false, error: error.message }; + } + } + flushMyCompletion() { const finalText = (this.myCompletionBuffer + this.myCurrentUtterance).trim(); if (!this.modelInfo || !finalText) return; diff --git a/src/features/settings/settingsService.js b/src/features/settings/settingsService.js index 3d1a6f7..79011b7 100644 --- a/src/features/settings/settingsService.js +++ b/src/features/settings/settingsService.js @@ -423,17 +423,6 @@ function initialize() { // cleanup windowNotificationManager.cleanup(); - // IPC handlers for model settings - ipcMain.handle('settings:get-model-settings', getModelSettings); - ipcMain.handle('settings:validate-and-save-key', (e, { provider, key }) => validateAndSaveKey(provider, key)); - ipcMain.handle('settings:clear-api-key', (e, { provider }) => clearApiKey(provider)); - ipcMain.handle('settings:set-selected-model', (e, { type, modelId }) => setSelectedModel(type, modelId)); - - // IPC handlers for Ollama management - ipcMain.handle('settings:get-ollama-status', getOllamaStatus); - ipcMain.handle('settings:ensure-ollama-ready', ensureOllamaReady); - ipcMain.handle('settings:shutdown-ollama', shutdownOllama); - console.log('[SettingsService] Initialized and ready.'); } diff --git a/src/index.js b/src/index.js index ddb848a..f160e67 100644 --- a/src/index.js +++ b/src/index.js @@ -26,7 +26,6 @@ const askService = require('./features/ask/askService'); const settingsService = require('./features/settings/settingsService'); const sessionRepository = require('./features/common/repositories/session'); const modelStateService = require('./features/common/services/modelStateService'); -const sqliteClient = require('./features/common/services/sqliteClient'); const featureBridge = require('./bridge/featureBridge'); // Global variables @@ -198,13 +197,8 @@ app.whenReady().then(async () => { await modelStateService.initialize(); //////// after_modelStateService //////// - listenService.initialize(); - askService.initialize(); - settingsService.initialize(); featureBridge.initialize(); // 추가: featureBridge 초기화 - setupGeneralIpcHandlers(); - setupOllamaIpcHandlers(); - setupWhisperIpcHandlers(); + setupWebDataHandlers(); // Initialize Ollama models in database await ollamaModelRepository.initializeDefaultModels(); @@ -318,301 +312,6 @@ app.on('activate', () => { } }); -function setupWhisperIpcHandlers() { - const whisperService = require('./features/common/services/whisperService'); - - // Forward download progress events to renderer - whisperService.on('downloadProgress', (data) => { - const windows = BrowserWindow.getAllWindows(); - windows.forEach(window => { - window.webContents.send('whisper:download-progress', data); - }); - }); - - // IPC handlers for Whisper operations - ipcMain.handle('whisper:download-model', async (event, modelId) => { - try { - console.log(`[Whisper IPC] Starting download for model: ${modelId}`); - - // Ensure WhisperService is initialized first - if (!whisperService.isInitialized) { - console.log('[Whisper IPC] Initializing WhisperService...'); - await whisperService.initialize(); - } - - // Set up progress listener - const progressHandler = (data) => { - if (data.modelId === modelId) { - event.sender.send('whisper:download-progress', data); - } - }; - - whisperService.on('downloadProgress', progressHandler); - - try { - await whisperService.ensureModelAvailable(modelId); - console.log(`[Whisper IPC] Model ${modelId} download completed successfully`); - } finally { - // Cleanup listener - whisperService.removeListener('downloadProgress', progressHandler); - } - - return { success: true }; - } catch (error) { - console.error(`[Whisper IPC] Failed to download model ${modelId}:`, error); - return { success: false, error: error.message }; - } - }); - - ipcMain.handle('whisper:get-installed-models', async () => { - try { - // Ensure WhisperService is initialized first - if (!whisperService.isInitialized) { - console.log('[Whisper IPC] Initializing WhisperService for model list...'); - await whisperService.initialize(); - } - - const models = await whisperService.getInstalledModels(); - return { success: true, models }; - } catch (error) { - console.error('[Whisper IPC] Failed to get installed models:', error); - return { success: false, error: error.message }; - } - }); -} - -function setupGeneralIpcHandlers() { - const userRepository = require('./features/common/repositories/user'); - const presetRepository = require('./features/common/repositories/preset'); - - ipcMain.handle('get-user-presets', () => { - // The adapter injects the UID. - return presetRepository.getPresets(); - }); - - ipcMain.handle('get-preset-templates', () => { - return presetRepository.getPresetTemplates(); - }); - - ipcMain.handle('start-firebase-auth', async () => { - try { - const authUrl = `http://localhost:${WEB_PORT}/login?mode=electron`; - console.log(`[Auth] Opening Firebase auth URL in browser: ${authUrl}`); - await shell.openExternal(authUrl); - return { success: true }; - } catch (error) { - console.error('[Auth] Failed to open Firebase auth URL:', error); - return { success: false, error: error.message }; - } - }); - - ipcMain.handle('get-web-url', () => { - return process.env.pickleglass_WEB_URL || 'http://localhost:3000'; - }); - - ipcMain.handle('get-current-user', () => { - return authService.getCurrentUser(); - }); - - // --- Web UI Data Handlers (New) --- - setupWebDataHandlers(); -} - -function setupOllamaIpcHandlers() { - // Ollama status and installation - ipcMain.handle('ollama:get-status', async () => { - try { - const installed = await ollamaService.isInstalled(); - const running = installed ? await ollamaService.isServiceRunning() : false; - const models = await ollamaService.getAllModelsWithStatus(); - - return { - installed, - running, - models, - success: true - }; - } catch (error) { - console.error('[Ollama IPC] Failed to get status:', error); - return { success: false, error: error.message }; - } - }); - - ipcMain.handle('ollama:install', async (event) => { - try { - const onProgress = (data) => { - event.sender.send('ollama:install-progress', data); - }; - - await ollamaService.autoInstall(onProgress); - - if (!await ollamaService.isServiceRunning()) { - onProgress({ stage: 'starting', message: 'Starting Ollama service...', progress: 0 }); - await ollamaService.startService(); - onProgress({ stage: 'starting', message: 'Ollama service started.', progress: 100 }); - } - event.sender.send('ollama:install-complete', { success: true }); - return { success: true }; - } catch (error) { - console.error('[Ollama IPC] Failed to install:', error); - event.sender.send('ollama:install-complete', { success: false, error: error.message }); - return { success: false, error: error.message }; - } - }); - - ipcMain.handle('ollama:start-service', async (event) => { - try { - if (!await ollamaService.isServiceRunning()) { - console.log('[Ollama IPC] Starting Ollama service...'); - await ollamaService.startService(); - } - event.sender.send('ollama:install-complete', { success: true }); - return { success: true }; - } catch (error) { - console.error('[Ollama IPC] Failed to start service:', error); - event.sender.send('ollama:install-complete', { success: false, error: error.message }); - return { success: false, error: error.message }; - } - }); - - // Ensure Ollama is ready (starts service if installed but not running) - ipcMain.handle('ollama:ensure-ready', async () => { - try { - if (await ollamaService.isInstalled() && !await ollamaService.isServiceRunning()) { - console.log('[Ollama IPC] Ollama installed but not running, starting service...'); - await ollamaService.startService(); - } - return { success: true }; - } catch (error) { - console.error('[Ollama IPC] Failed to ensure ready:', error); - return { success: false, error: error.message }; - } - }); - - // Get all models with their status - ipcMain.handle('ollama:get-models', async () => { - try { - const models = await ollamaService.getAllModelsWithStatus(); - return { success: true, models }; - } catch (error) { - console.error('[Ollama IPC] Failed to get models:', error); - return { success: false, error: error.message }; - } - }); - - // Get model suggestions for autocomplete - ipcMain.handle('ollama:get-model-suggestions', async () => { - try { - const suggestions = await ollamaService.getModelSuggestions(); - return { success: true, suggestions }; - } catch (error) { - console.error('[Ollama IPC] Failed to get model suggestions:', error); - return { success: false, error: error.message }; - } - }); - - // Pull/install a specific model - ipcMain.handle('ollama:pull-model', async (event, modelName) => { - try { - console.log(`[Ollama IPC] Starting model pull: ${modelName}`); - - // Update DB status to installing - await ollamaModelRepository.updateInstallStatus(modelName, false, true); - - // Set up progress listener for real-time updates - const progressHandler = (data) => { - if (data.model === modelName) { - event.sender.send('ollama:pull-progress', data); - } - }; - - const completeHandler = (data) => { - if (data.model === modelName) { - console.log(`[Ollama IPC] Model ${modelName} pull completed`); - // Clean up listeners - ollamaService.removeListener('pull-progress', progressHandler); - ollamaService.removeListener('pull-complete', completeHandler); - } - }; - - ollamaService.on('pull-progress', progressHandler); - ollamaService.on('pull-complete', completeHandler); - - // Pull the model using REST API - await ollamaService.pullModel(modelName); - - // Update DB status to installed - await ollamaModelRepository.updateInstallStatus(modelName, true, false); - - console.log(`[Ollama IPC] Model ${modelName} pull successful`); - return { success: true }; - } catch (error) { - console.error('[Ollama IPC] Failed to pull model:', error); - // Reset status on error - await ollamaModelRepository.updateInstallStatus(modelName, false, false); - return { success: false, error: error.message }; - } - }); - - // Check if a specific model is installed - ipcMain.handle('ollama:is-model-installed', async (event, modelName) => { - try { - const installed = await ollamaService.isModelInstalled(modelName); - return { success: true, installed }; - } catch (error) { - console.error('[Ollama IPC] Failed to check model installation:', error); - return { success: false, error: error.message }; - } - }); - - // Warm up a specific model - ipcMain.handle('ollama:warm-up-model', async (event, modelName) => { - try { - const success = await ollamaService.warmUpModel(modelName); - return { success }; - } catch (error) { - console.error('[Ollama IPC] Failed to warm up model:', error); - return { success: false, error: error.message }; - } - }); - - // Auto warm-up currently selected model - ipcMain.handle('ollama:auto-warm-up', async () => { - try { - const success = await ollamaService.autoWarmUpSelectedModel(); - return { success }; - } catch (error) { - console.error('[Ollama IPC] Failed to auto warm-up:', error); - return { success: false, error: error.message }; - } - }); - - // Get warm-up status for debugging - ipcMain.handle('ollama:get-warm-up-status', async () => { - try { - const status = ollamaService.getWarmUpStatus(); - return { success: true, status }; - } catch (error) { - console.error('[Ollama IPC] Failed to get warm-up status:', error); - return { success: false, error: error.message }; - } - }); - - // Shutdown Ollama service manually - ipcMain.handle('ollama:shutdown', async (event, force = false) => { - try { - console.log(`[Ollama IPC] Manual shutdown requested (force: ${force})`); - const success = await ollamaService.shutdown(force); - return { success }; - } catch (error) { - console.error('[Ollama IPC] Failed to shutdown Ollama:', error); - return { success: false, error: error.message }; - } - }); - - console.log('[Ollama IPC] Handlers registered'); -} - function setupWebDataHandlers() { const sessionRepository = require('./features/common/repositories/session'); const sttRepository = require('./features/listen/stt/repositories'); diff --git a/src/preload.js b/src/preload.js index 80dd79d..02267b5 100644 --- a/src/preload.js +++ b/src/preload.js @@ -2,72 +2,231 @@ const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('api', { - feature: { - // 기존 ask 관련 유지 - submitAsk: (query) => ipcRenderer.invoke('feature:ask', query), - onAskProgress: (callback) => ipcRenderer.on('feature:ask:progress', (e, p) => callback(p)), - - settings: { - // invoke methods - getCurrentUser: () => ipcRenderer.invoke('get-current-user'), - getProviderConfig: () => ipcRenderer.invoke('model:get-provider-config'), - getAllKeys: () => ipcRenderer.invoke('model:get-all-keys'), - getAvailableModels: (type) => ipcRenderer.invoke('model:get-available-models', type), - getSelectedModels: () => ipcRenderer.invoke('model:get-selected-models'), - getPresets: () => ipcRenderer.invoke('settings:getPresets'), - getContentProtectionStatus: () => ipcRenderer.invoke('get-content-protection-status'), - getCurrentShortcuts: () => ipcRenderer.invoke('get-current-shortcuts'), - getOllamaStatus: () => ipcRenderer.invoke('ollama:get-status'), - getWhisperInstalledModels: () => ipcRenderer.invoke('whisper:get-installed-models'), - ollamaEnsureReady: () => ipcRenderer.invoke('ollama:ensure-ready'), - validateKey: (data) => ipcRenderer.invoke('model:validate-key', data), - getAutoUpdate: () => ipcRenderer.invoke('settings:get-auto-update'), - setAutoUpdate: (isEnabled) => ipcRenderer.invoke('settings:set-auto-update', isEnabled), - removeApiKey: (provider) => ipcRenderer.invoke('model:remove-api-key', provider), - setSelectedModel: (data) => ipcRenderer.invoke('model:set-selected-model', data), - downloadWhisperModel: (modelId) => ipcRenderer.invoke('whisper:download-model', modelId), - openLoginPage: () => ipcRenderer.invoke('open-login-page'), - toggleContentProtection: () => ipcRenderer.invoke('toggle-content-protection'), - openShortcutEditor: () => ipcRenderer.invoke('open-shortcut-editor'), + // Platform information for renderer processes + platform: { + isLinux: process.platform === 'linux', + isMacOS: process.platform === 'darwin', + isWindows: process.platform === 'win32', + platform: process.platform + }, + + // Common utilities used across multiple components + common: { + // User & Auth + getCurrentUser: () => ipcRenderer.invoke('get-current-user'), + startFirebaseAuth: () => ipcRenderer.invoke('start-firebase-auth'), + firebaseLogout: () => ipcRenderer.invoke('firebase-logout'), + + // App Control quitApplication: () => ipcRenderer.invoke('quit-application'), - firebaseLogout: () => ipcRenderer.invoke('firebase-logout'), - ollamaShutdown: (graceful) => ipcRenderer.invoke('ollama:shutdown', graceful), - startFirebaseAuth: () => ipcRenderer.invoke('start-firebase-auth'), - // on methods (listeners) + // User state listener (used by multiple components) onUserStateChanged: (callback) => ipcRenderer.on('user-state-changed', callback), removeOnUserStateChanged: (callback) => ipcRenderer.removeListener('user-state-changed', callback), - onSettingsUpdated: (callback) => ipcRenderer.on('settings-updated', callback), - removeOnSettingsUpdated: (callback) => ipcRenderer.removeListener('settings-updated', callback), - onPresetsUpdated: (callback) => ipcRenderer.on('presets-updated', callback), - removeOnPresetsUpdated: (callback) => ipcRenderer.removeListener('presets-updated', callback), - onShortcutsUpdated: (callback) => ipcRenderer.on('shortcuts-updated', callback), - removeOnShortcutsUpdated: (callback) => ipcRenderer.removeListener('shortcuts-updated', callback), - onWhisperDownloadProgress: (callback) => ipcRenderer.on('whisper:download-progress', callback), - removeOnWhisperDownloadProgress: (callback) => ipcRenderer.removeListener('whisper:download-progress', callback), + }, - // send methods - cancelHideSettingsWindow: () => ipcRenderer.send('cancel-hide-settings-window'), - hideSettingsWindow: () => ipcRenderer.send('hide-settings-window') + // UI Component specific namespaces + // src/ui/app/ApiKeyHeader.js + apiKeyHeader: { + // Model & Provider Management + getProviderConfig: () => ipcRenderer.invoke('model:get-provider-config'), + getOllamaStatus: () => ipcRenderer.invoke('ollama:get-status'), + getModelSuggestions: () => ipcRenderer.invoke('ollama:get-model-suggestions'), + ensureOllamaReady: () => ipcRenderer.invoke('ollama:ensure-ready'), + installOllama: () => ipcRenderer.invoke('ollama:install'), + startOllamaService: () => ipcRenderer.invoke('ollama:start-service'), + pullOllamaModel: (modelName) => ipcRenderer.invoke('ollama:pull-model', modelName), + downloadWhisperModel: (modelId) => ipcRenderer.invoke('whisper:download-model', modelId), + validateKey: (data) => ipcRenderer.invoke('model:validate-key', data), + setSelectedModel: (data) => ipcRenderer.invoke('model:set-selected-model', data), + areProvidersConfigured: () => ipcRenderer.invoke('model:are-providers-configured'), + + // Window Management + getHeaderPosition: () => ipcRenderer.invoke('get-header-position'), + moveHeaderTo: (x, y) => ipcRenderer.invoke('move-header-to', x, y), + + // Listeners + onOllamaInstallProgress: (callback) => ipcRenderer.on('ollama:install-progress', callback), + removeOnOllamaInstallProgress: (callback) => ipcRenderer.removeListener('ollama:install-progress', callback), + onceOllamaInstallComplete: (callback) => ipcRenderer.once('ollama:install-complete', callback), + removeOnceOllamaInstallComplete: (callback) => ipcRenderer.removeListener('ollama:install-complete', callback), + onOllamaPullProgress: (callback) => ipcRenderer.on('ollama:pull-progress', callback), + removeOnOllamaPullProgress: (callback) => ipcRenderer.removeListener('ollama:pull-progress', callback), + onWhisperDownloadProgress: (callback) => ipcRenderer.on('whisper:download-progress', callback), + removeOnWhisperDownloadProgress: (callback) => ipcRenderer.removeListener('whisper:download-progress', callback), + + // Remove all listeners (for cleanup) + removeAllListeners: () => { + ipcRenderer.removeAllListeners('whisper:download-progress'); + ipcRenderer.removeAllListeners('ollama:install-progress'); + ipcRenderer.removeAllListeners('ollama:pull-progress'); + ipcRenderer.removeAllListeners('ollama:install-complete'); } }, - // 기존 window 유지 - window: { - // 기존 - hide: () => ipcRenderer.send('window:hide'), - onFocusChange: (callback) => ipcRenderer.on('window:focus-change', (e, f) => callback(f)), - // 추가 + // src/ui/app/HeaderController.js + headerController: { + // State Management + sendHeaderStateChanged: (state) => ipcRenderer.send('header-state-changed', state), + + // Window Management + resizeHeaderWindow: (dimensions) => ipcRenderer.invoke('resize-header-window', dimensions), + + // Permissions + checkSystemPermissions: () => ipcRenderer.invoke('check-system-permissions'), + checkPermissionsCompleted: () => ipcRenderer.invoke('check-permissions-completed'), + + // Listeners + onUserStateChanged: (callback) => ipcRenderer.on('user-state-changed', callback), + removeOnUserStateChanged: (callback) => ipcRenderer.removeListener('user-state-changed', callback), + onAuthFailed: (callback) => ipcRenderer.on('auth-failed', callback), + removeOnAuthFailed: (callback) => ipcRenderer.removeListener('auth-failed', callback), + onForceShowApiKeyHeader: (callback) => ipcRenderer.on('force-show-apikey-header', callback), + removeOnForceShowApiKeyHeader: (callback) => ipcRenderer.removeListener('force-show-apikey-header', callback) + }, + + // src/ui/app/MainHeader.js + mainHeader: { + // Window Management + getHeaderPosition: () => ipcRenderer.invoke('get-header-position'), + moveHeaderTo: (x, y) => ipcRenderer.invoke('move-header-to', x, y), + sendHeaderAnimationFinished: (state) => ipcRenderer.send('header-animation-finished', state), + + // Settings Window Management + cancelHideSettingsWindow: () => ipcRenderer.send('cancel-hide-settings-window'), showSettingsWindow: (bounds) => ipcRenderer.send('show-settings-window', bounds), hideSettingsWindow: () => ipcRenderer.send('hide-settings-window'), - cancelHideSettingsWindow: () => ipcRenderer.send('cancel-hide-settings-window'), - moveWindowStep: (direction) => ipcRenderer.invoke('move-window-step', direction), + + // Generic invoke (for dynamic channel names) + invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args), + + // Listeners + onSessionStateText: (callback) => ipcRenderer.on('session-state-text', callback), + removeOnSessionStateText: (callback) => ipcRenderer.removeListener('session-state-text', callback), + onShortcutsUpdated: (callback) => ipcRenderer.on('shortcuts-updated', callback), + removeOnShortcutsUpdated: (callback) => ipcRenderer.removeListener('shortcuts-updated', callback) + }, + + // src/ui/app/PermissionHeader.js + permissionHeader: { + // Permission Management + checkSystemPermissions: () => ipcRenderer.invoke('check-system-permissions'), + requestMicrophonePermission: () => ipcRenderer.invoke('request-microphone-permission'), + openSystemPreferences: (preference) => ipcRenderer.invoke('open-system-preferences', preference), + markPermissionsCompleted: () => ipcRenderer.invoke('mark-permissions-completed') + }, + + // src/ui/app/PickleGlassApp.js + pickleGlassApp: { + // Listeners + onClickThroughToggled: (callback) => ipcRenderer.on('click-through-toggled', callback), + removeOnClickThroughToggled: (callback) => ipcRenderer.removeListener('click-through-toggled', callback), + removeAllClickThroughListeners: () => ipcRenderer.removeAllListeners('click-through-toggled') + }, + + // src/ui/ask/AskView.js + askView: { + // Window Management + closeAskWindow: () => ipcRenderer.invoke('ask:closeAskWindow'), + adjustWindowHeight: (height) => ipcRenderer.invoke('adjust-window-height', height), + + // Message Handling + sendMessage: (text) => ipcRenderer.invoke('ask:sendMessage', text), + + // Listeners + onSendQuestionToRenderer: (callback) => ipcRenderer.on('ask:sendQuestionToRenderer', callback), + removeOnSendQuestionToRenderer: (callback) => ipcRenderer.removeListener('ask:sendQuestionToRenderer', callback), + onHideTextInput: (callback) => ipcRenderer.on('hide-text-input', callback), + removeOnHideTextInput: (callback) => ipcRenderer.removeListener('hide-text-input', callback), + onShowTextInput: (callback) => ipcRenderer.on('ask:showTextInput', callback), + removeOnShowTextInput: (callback) => ipcRenderer.removeListener('ask:showTextInput', callback), + onResponseChunk: (callback) => ipcRenderer.on('ask-response-chunk', callback), + removeOnResponseChunk: (callback) => ipcRenderer.removeListener('ask-response-chunk', callback), + onResponseStreamEnd: (callback) => ipcRenderer.on('ask-response-stream-end', callback), + removeOnResponseStreamEnd: (callback) => ipcRenderer.removeListener('ask-response-stream-end', callback), + onScrollResponseUp: (callback) => ipcRenderer.on('scroll-response-up', callback), + removeOnScrollResponseUp: (callback) => ipcRenderer.removeListener('scroll-response-up', callback), + onScrollResponseDown: (callback) => ipcRenderer.on('scroll-response-down', callback), + removeOnScrollResponseDown: (callback) => ipcRenderer.removeListener('scroll-response-down', callback) + }, + + // src/ui/listen/ListenView.js + listenView: { + // Window Management + adjustWindowHeight: (height) => ipcRenderer.invoke('adjust-window-height', height), + + // Listeners + onSessionStateChanged: (callback) => ipcRenderer.on('session-state-changed', callback), + removeOnSessionStateChanged: (callback) => ipcRenderer.removeListener('session-state-changed', callback) + }, + + // src/ui/listen/stt/SttView.js + sttView: { + // Listeners + onSttUpdate: (callback) => ipcRenderer.on('stt-update', callback), + removeOnSttUpdate: (callback) => ipcRenderer.removeListener('stt-update', callback) + }, + + // src/ui/listen/summary/SummaryView.js + summaryView: { + // Message Handling + sendQuestionToMain: (text) => ipcRenderer.invoke('ask:sendQuestionToMain', text), + + // Listeners + onSummaryUpdate: (callback) => ipcRenderer.on('summary-update', callback), + removeOnSummaryUpdate: (callback) => ipcRenderer.removeListener('summary-update', callback), + removeAllSummaryUpdateListeners: () => ipcRenderer.removeAllListeners('summary-update') + }, + + // src/ui/settings/SettingsView.js + settingsView: { + // User & Auth + getCurrentUser: () => ipcRenderer.invoke('get-current-user'), openLoginPage: () => ipcRenderer.invoke('open-login-page'), firebaseLogout: () => ipcRenderer.invoke('firebase-logout'), - ollamaShutdown: (graceful) => ipcRenderer.invoke('ollama:shutdown', graceful), startFirebaseAuth: () => ipcRenderer.invoke('start-firebase-auth'), - // on methods (listeners) + // Model & Provider Management + getModelSettings: () => ipcRenderer.invoke('settings:get-model-settings'), // Facade call + getProviderConfig: () => ipcRenderer.invoke('model:get-provider-config'), + getAllKeys: () => ipcRenderer.invoke('model:get-all-keys'), + getAvailableModels: (type) => ipcRenderer.invoke('model:get-available-models', type), + getSelectedModels: () => ipcRenderer.invoke('model:get-selected-models'), + validateKey: (data) => ipcRenderer.invoke('model:validate-key', data), + saveApiKey: (key) => ipcRenderer.invoke('model:save-api-key', key), + removeApiKey: (provider) => ipcRenderer.invoke('model:remove-api-key', provider), + setSelectedModel: (data) => ipcRenderer.invoke('model:set-selected-model', data), + + // Ollama Management + getOllamaStatus: () => ipcRenderer.invoke('ollama:get-status'), + ensureOllamaReady: () => ipcRenderer.invoke('ollama:ensure-ready'), + shutdownOllama: (graceful) => ipcRenderer.invoke('ollama:shutdown', graceful), + + // Whisper Management + getWhisperInstalledModels: () => ipcRenderer.invoke('whisper:get-installed-models'), + downloadWhisperModel: (modelId) => ipcRenderer.invoke('whisper:download-model', modelId), + + // Settings Management + getPresets: () => ipcRenderer.invoke('settings:getPresets'), + getAutoUpdate: () => ipcRenderer.invoke('settings:get-auto-update'), + setAutoUpdate: (isEnabled) => ipcRenderer.invoke('settings:set-auto-update', isEnabled), + getContentProtectionStatus: () => ipcRenderer.invoke('get-content-protection-status'), + toggleContentProtection: () => ipcRenderer.invoke('toggle-content-protection'), + getCurrentShortcuts: () => ipcRenderer.invoke('get-current-shortcuts'), + openShortcutEditor: () => ipcRenderer.invoke('open-shortcut-editor'), + + // Window Management + moveWindowStep: (direction) => ipcRenderer.invoke('move-window-step', direction), + cancelHideSettingsWindow: () => ipcRenderer.send('cancel-hide-settings-window'), + hideSettingsWindow: () => ipcRenderer.send('hide-settings-window'), + + // App Control + quitApplication: () => ipcRenderer.invoke('quit-application'), + + // Progress Tracking + pullOllamaModel: (modelName) => ipcRenderer.invoke('ollama:pull-model', modelName), + + // Listeners onUserStateChanged: (callback) => ipcRenderer.on('user-state-changed', callback), removeOnUserStateChanged: (callback) => ipcRenderer.removeListener('user-state-changed', callback), onSettingsUpdated: (callback) => ipcRenderer.on('settings-updated', callback), @@ -78,9 +237,66 @@ contextBridge.exposeInMainWorld('api', { removeOnShortcutsUpdated: (callback) => ipcRenderer.removeListener('shortcuts-updated', callback), onWhisperDownloadProgress: (callback) => ipcRenderer.on('whisper:download-progress', callback), removeOnWhisperDownloadProgress: (callback) => ipcRenderer.removeListener('whisper:download-progress', callback), + onOllamaPullProgress: (callback) => ipcRenderer.on('ollama:pull-progress', callback), + removeOnOllamaPullProgress: (callback) => ipcRenderer.removeListener('ollama:pull-progress', callback) + }, - // send methods - cancelHideSettingsWindow: () => ipcRenderer.send('cancel-hide-settings-window'), - hideSettingsWindow: () => ipcRenderer.send('hide-settings-window') + // src/ui/settings/ShortCutSettingsView.js + shortcutSettingsView: { + // Shortcut Management + saveShortcuts: (shortcuts) => ipcRenderer.invoke('save-shortcuts', shortcuts), + getDefaultShortcuts: () => ipcRenderer.invoke('get-default-shortcuts'), + closeShortcutEditor: () => ipcRenderer.send('close-shortcut-editor'), + + // Listeners + onLoadShortcuts: (callback) => ipcRenderer.on('load-shortcuts', callback), + removeOnLoadShortcuts: (callback) => ipcRenderer.removeListener('load-shortcuts', callback) + }, + + // src/ui/app/content.html inline scripts + content: { + // Animation Management + sendAnimationFinished: () => ipcRenderer.send('animation-finished'), + + // Listeners + onWindowShowAnimation: (callback) => ipcRenderer.on('window-show-animation', callback), + removeOnWindowShowAnimation: (callback) => ipcRenderer.removeListener('window-show-animation', callback), + onWindowHideAnimation: (callback) => ipcRenderer.on('window-hide-animation', callback), + removeOnWindowHideAnimation: (callback) => ipcRenderer.removeListener('window-hide-animation', callback), + onSettingsWindowHideAnimation: (callback) => ipcRenderer.on('settings-window-hide-animation', callback), + removeOnSettingsWindowHideAnimation: (callback) => ipcRenderer.removeListener('settings-window-hide-animation', callback), + onListenWindowMoveToCenter: (callback) => ipcRenderer.on('listen-window-move-to-center', callback), + removeOnListenWindowMoveToCenter: (callback) => ipcRenderer.removeListener('listen-window-move-to-center', callback), + onListenWindowMoveToLeft: (callback) => ipcRenderer.on('listen-window-move-to-left', callback), + removeOnListenWindowMoveToLeft: (callback) => ipcRenderer.removeListener('listen-window-move-to-left', callback) + }, + + // src/ui/listen/audioCore/listenCapture.js + listenCapture: { + // Audio Management + sendAudioContent: (data) => ipcRenderer.invoke('send-audio-content', data), + sendSystemAudioContent: (data) => ipcRenderer.invoke('send-system-audio-content', data), + startMacosAudio: () => ipcRenderer.invoke('start-macos-audio'), + stopMacosAudio: () => ipcRenderer.invoke('stop-macos-audio'), + + // 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'), + + // Listeners + onSystemAudioData: (callback) => ipcRenderer.on('system-audio-data', callback), + removeOnSystemAudioData: (callback) => ipcRenderer.removeListener('system-audio-data', callback) + }, + + // src/ui/listen/audioCore/renderer.js + renderer: { + // Listeners + onChangeListenCaptureState: (callback) => ipcRenderer.on('change-listen-capture-state', callback), + removeOnChangeListenCaptureState: (callback) => ipcRenderer.removeListener('change-listen-capture-state', callback) } }); \ No newline at end of file diff --git a/src/ui/app/ApiKeyHeader.js b/src/ui/app/ApiKeyHeader.js index a0ed1fa..0bdbe03 100644 --- a/src/ui/app/ApiKeyHeader.js +++ b/src/ui/app/ApiKeyHeader.js @@ -370,13 +370,12 @@ export class ApiKeyHeader extends LitElement { } async loadProviderConfig() { - if (!window.require) return; - const { ipcRenderer } = window.require('electron'); + if (!window.api) return; try { const [config, ollamaStatus] = await Promise.all([ - ipcRenderer.invoke('model:get-provider-config'), - ipcRenderer.invoke('ollama:get-status') + window.api.apiKeyHeader.getProviderConfig(), + window.api.apiKeyHeader.getOllamaStatus() ]); const llmProviders = []; @@ -428,8 +427,7 @@ export class ApiKeyHeader extends LitElement { e.preventDefault() - const { ipcRenderer } = window.require("electron") - const initialPosition = await ipcRenderer.invoke("get-header-position") + const initialPosition = await window.api.apiKeyHeader.getHeaderPosition() this.dragState = { initialMouseX: e.screenX, @@ -456,8 +454,7 @@ export class ApiKeyHeader extends LitElement { 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) + window.api.apiKeyHeader.moveHeaderTo(newWindowX, newWindowY) } handleMouseUp(e) { @@ -652,9 +649,8 @@ export class ApiKeyHeader extends LitElement { try { // Lightweight health check - just ping the service const isHealthy = await this._executeOperation('health_check', async () => { - if (!window.require) return false; - const { ipcRenderer } = window.require('electron'); - const result = await ipcRenderer.invoke('ollama:get-status'); + if (!window.api) return false; + const result = await window.api.apiKeyHeader.getOllamaStatus(); return result?.success && result?.running; }, { timeout: 5000, priority: 'low' }); @@ -928,14 +924,13 @@ export class ApiKeyHeader extends LitElement { } async refreshOllamaStatus() { - if (!window.require) return; + if (!window.api) return; try { this._updateConnectionState('connecting', 'Checking Ollama status'); const result = await this._executeOperation('ollama_status', async () => { - const { ipcRenderer } = window.require('electron'); - return await ipcRenderer.invoke('ollama:get-status'); + return await window.api.apiKeyHeader.getOllamaStatus(); }); if (result?.success) { @@ -960,12 +955,11 @@ export class ApiKeyHeader extends LitElement { } async loadModelSuggestions() { - if (!window.require) return; + if (!window.api) return; try { const result = await this._executeOperation('model_suggestions', async () => { - const { ipcRenderer } = window.require('electron'); - return await ipcRenderer.invoke('ollama:get-model-suggestions'); + return await window.api.apiKeyHeader.getModelSuggestions(); }); if (result?.success) { @@ -988,14 +982,13 @@ export class ApiKeyHeader extends LitElement { } async ensureOllamaReady() { - if (!window.require) return false; + if (!window.api) return false; try { this._updateConnectionState('connecting', 'Ensuring Ollama is ready'); const result = await this._executeOperation('ollama_ensure_ready', async () => { - const { ipcRenderer } = window.require('electron'); - return await ipcRenderer.invoke('ollama:ensure-ready'); + return await window.api.apiKeyHeader.ensureOllamaReady(); }, { timeout: this.operationTimeout }); if (result?.success) { @@ -1015,8 +1008,7 @@ export class ApiKeyHeader extends LitElement { } async ensureOllamaReadyWithUI() { - if (!window.require) return false; - const { ipcRenderer } = window.require("electron"); + if (!window.api) return false; this.installingModel = "Setting up Ollama"; this.installProgress = 0; @@ -1074,21 +1066,21 @@ export class ApiKeyHeader extends LitElement { operationCompleted = true; clearTimeout(completionTimeout); - ipcRenderer.removeListener("ollama:install-progress", progressHandler); + window.api.apiKeyHeader.removeOnOllamaInstallProgress(progressHandler); await this._handleOllamaSetupCompletion(result.success, result.error); }; - ipcRenderer.once("ollama:install-complete", completionHandler); - ipcRenderer.on("ollama:install-progress", progressHandler); + window.api.apiKeyHeader.onceOllamaInstallComplete(completionHandler); + window.api.apiKeyHeader.onOllamaInstallProgress(progressHandler); try { let result; if (!this.ollamaStatus.installed) { console.log("[ApiKeyHeader] Ollama not installed. Starting installation."); - result = await ipcRenderer.invoke("ollama:install"); + result = await window.api.apiKeyHeader.installOllama(); } else { console.log("[ApiKeyHeader] Ollama installed. Starting service."); - result = await ipcRenderer.invoke("ollama:start-service"); + result = await window.api.apiKeyHeader.startOllamaService(); } // If IPC call succeeds but no event received, handle completion manually @@ -1106,8 +1098,8 @@ export class ApiKeyHeader extends LitElement { operationCompleted = true; clearTimeout(completionTimeout); console.error("[ApiKeyHeader] Ollama setup failed:", error); - ipcRenderer.removeListener("ollama:install-progress", progressHandler); - ipcRenderer.removeListener("ollama:install-complete", completionHandler); + window.api.apiKeyHeader.removeOnOllamaInstallProgress(progressHandler); + window.api.apiKeyHeader.removeOnceOllamaInstallComplete(completionHandler); await this._handleOllamaSetupCompletion(false, error.message); } } @@ -1229,7 +1221,6 @@ export class ApiKeyHeader extends LitElement { this.clearMessages(); this.requestUpdate(); - const { ipcRenderer } = window.require('electron'); let progressHandler = null; try { @@ -1249,10 +1240,10 @@ export class ApiKeyHeader extends LitElement { }; // Set up progress tracking - ipcRenderer.on('ollama:pull-progress', progressHandler); + window.api.apiKeyHeader.onOllamaPullProgress(progressHandler); // Execute the model pull with timeout - const installPromise = ipcRenderer.invoke('ollama:pull-model', modelName); + const installPromise = window.api.apiKeyHeader.pullOllamaModel(modelName); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Installation timeout after 10 minutes')), 600000) ); @@ -1281,7 +1272,7 @@ export class ApiKeyHeader extends LitElement { } finally { // Comprehensive cleanup if (progressHandler) { - ipcRenderer.removeListener('ollama:pull-progress', progressHandler); + window.api.apiKeyHeader.removeOnOllamaPullProgress(progressHandler); } this.installingModel = null; @@ -1307,7 +1298,6 @@ export class ApiKeyHeader extends LitElement { this.clearMessages(); this.requestUpdate(); - const { ipcRenderer } = window.require('electron'); let progressHandler = null; try { @@ -1321,10 +1311,10 @@ export class ApiKeyHeader extends LitElement { } }; - ipcRenderer.on('whisper:download-progress', progressHandler); + window.api.apiKeyHeader.onWhisperDownloadProgress(progressHandler); // Start download with timeout protection - const downloadPromise = ipcRenderer.invoke('whisper:download-model', modelId); + const downloadPromise = window.api.apiKeyHeader.downloadWhisperModel(modelId); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Download timeout after 10 minutes')), 600000) ); @@ -1351,7 +1341,7 @@ export class ApiKeyHeader extends LitElement { } finally { // Cleanup if (progressHandler) { - ipcRenderer.removeListener('whisper:download-progress', progressHandler); + window.api.apiKeyHeader.removeOnWhisperDownloadProgress(progressHandler); } delete this.whisperInstallingModels[modelId]; this.requestUpdate(); @@ -1411,8 +1401,6 @@ export class ApiKeyHeader extends LitElement { this.isLoading = true; this.clearMessages(); this.requestUpdate(); - - const { ipcRenderer } = window.require('electron'); try { // Handle LLM provider @@ -1436,14 +1424,14 @@ export class ApiKeyHeader extends LitElement { } // Validate Ollama is working - llmResult = await ipcRenderer.invoke('model:validate-key', { + llmResult = await window.api.apiKeyHeader.validateKey({ provider: 'ollama', key: 'local' }); if (llmResult.success) { // Set the selected model - await ipcRenderer.invoke('model:set-selected-model', { + await window.api.apiKeyHeader.setSelectedModel({ type: 'llm', modelId: this.selectedLlmModel }); @@ -1454,7 +1442,7 @@ export class ApiKeyHeader extends LitElement { throw new Error('Please enter LLM API key'); } - llmResult = await ipcRenderer.invoke('model:validate-key', { + llmResult = await window.api.apiKeyHeader.validateKey({ provider: this.llmProvider, key: this.llmApiKey.trim() }); @@ -1467,14 +1455,14 @@ export class ApiKeyHeader extends LitElement { sttResult = { success: true }; } else if (this.sttProvider === 'whisper') { // For Whisper, just validate it's enabled (model download already handled in handleSttModelChange) - sttResult = await ipcRenderer.invoke('model:validate-key', { + sttResult = await window.api.apiKeyHeader.validateKey({ provider: 'whisper', key: 'local' }); if (sttResult.success && this.selectedSttModel) { // Set the selected model - await ipcRenderer.invoke('model:set-selected-model', { + await window.api.apiKeyHeader.setSelectedModel({ type: 'stt', modelId: this.selectedSttModel }); @@ -1485,7 +1473,7 @@ export class ApiKeyHeader extends LitElement { throw new Error('Please enter STT API key'); } - sttResult = await ipcRenderer.invoke('model:validate-key', { + sttResult = await window.api.apiKeyHeader.validateKey({ provider: this.sttProvider, key: this.sttApiKey.trim() }); @@ -1522,15 +1510,15 @@ export class ApiKeyHeader extends LitElement { e.preventDefault() console.log("Requesting Firebase authentication from main process...") - if (window.require) { - window.require("electron").ipcRenderer.invoke("start-firebase-auth") + if (window.api) { + window.api.common.startFirebaseAuth() } } handleClose() { console.log("Close button clicked") - if (window.require) { - window.require("electron").ipcRenderer.invoke("quit-application") + if (window.api) { + window.api.common.quitApplication() } } @@ -1541,10 +1529,10 @@ export class ApiKeyHeader extends LitElement { this.classList.remove("sliding-out"); this.classList.add("hidden"); - console.log('[ApiKeyHeader] handleAnimationEnd: Animation completed, transitioning to next state...'); + console.log('[ApiKeyHeader] handleAnimationEnd: Transition completed, transitioning to next state...'); - if (!window.require) { - console.error('[ApiKeyHeader] handleAnimationEnd: window.require not available'); + if (!window.api) { + console.error('[ApiKeyHeader] handleAnimationEnd: window.api not available'); return; } @@ -1553,14 +1541,12 @@ export class ApiKeyHeader extends LitElement { return; } - const { ipcRenderer } = window.require('electron'); - - ipcRenderer.invoke('get-current-user') + window.api.common.getCurrentUser() .then(userState => { console.log('[ApiKeyHeader] handleAnimationEnd: User state retrieved:', userState); // Additional validation for local providers - return ipcRenderer.invoke('model:are-providers-configured').then(isConfigured => { + return window.api.apiKeyHeader.areProvidersConfigured().then(isConfigured => { console.log('[ApiKeyHeader] handleAnimationEnd: Providers configured check:', isConfigured); if (!isConfigured) { @@ -1585,7 +1571,8 @@ export class ApiKeyHeader extends LitElement { connectedCallback() { super.connectedCallback() - this.addEventListener("animationend", this.handleAnimationEnd) + // this.addEventListener("animationend", this.handleAnimationEnd) + this.addEventListener("transitionend", this.handleAnimationEnd) } handleMessageFadeEnd(e) { @@ -1603,8 +1590,8 @@ export class ApiKeyHeader extends LitElement { disconnectedCallback() { super.disconnectedCallback() - this.removeEventListener("animationend", this.handleAnimationEnd) - + // this.removeEventListener("animationend", this.handleAnimationEnd) + this.removeEventListener("transitionend", this.handleAnimationEnd) // Professional cleanup of all resources this._performCompleteCleanup(); } @@ -1624,12 +1611,8 @@ export class ApiKeyHeader extends LitElement { } // Cleanup event listeners - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.removeAllListeners('whisper:download-progress'); - ipcRenderer.removeAllListeners('ollama:install-progress'); - ipcRenderer.removeAllListeners('ollama:pull-progress'); - ipcRenderer.removeAllListeners('ollama:install-complete'); + if (window.api) { + window.api.apiKeyHeader.removeAllListeners(); } // Cancel any ongoing downloads diff --git a/src/ui/app/HeaderController.js b/src/ui/app/HeaderController.js index e2c5fe8..37c5aa9 100644 --- a/src/ui/app/HeaderController.js +++ b/src/ui/app/HeaderController.js @@ -32,6 +32,7 @@ class HeaderTransitionManager { this.apiKeyHeader = document.createElement('apikey-header'); this.apiKeyHeader.stateUpdateCallback = (userState) => this.handleStateUpdate(userState); this.headerContainer.appendChild(this.apiKeyHeader); + console.log('[HeaderController] ensureHeader: Header of type:', type, 'created.'); } else if (type === 'permission') { this.permissionHeader = document.createElement('permission-setup'); this.permissionHeader.continueCallback = () => this.transitionToMainHeader(); @@ -50,41 +51,39 @@ class HeaderTransitionManager { this._bootstrap(); - if (window.require) { - const { ipcRenderer } = window.require('electron'); - - ipcRenderer.on('user-state-changed', (event, userState) => { + if (window.api) { + window.api.headerController.onUserStateChanged((event, userState) => { console.log('[HeaderController] Received user state change:', userState); this.handleStateUpdate(userState); }); - ipcRenderer.on('auth-failed', (event, { message }) => { + window.api.headerController.onAuthFailed((event, { message }) => { console.error('[HeaderController] Received auth failure from main process:', message); if (this.apiKeyHeader) { this.apiKeyHeader.errorMessage = 'Authentication failed. Please try again.'; this.apiKeyHeader.isLoading = false; } }); - ipcRenderer.on('force-show-apikey-header', async () => { + window.api.headerController.onForceShowApiKeyHeader(async () => { console.log('[HeaderController] Received broadcast to show apikey header. Switching now.'); await this._resizeForApiKey(); this.ensureHeader('apikey'); - }); + }); } } notifyHeaderState(stateOverride) { const state = stateOverride || this.currentHeaderType || 'apikey'; - if (window.require) { - window.require('electron').ipcRenderer.send('header-state-changed', state); + if (window.api) { + window.api.headerController.sendHeaderStateChanged(state); } } async _bootstrap() { // The initial state will be sent by the main process via 'user-state-changed' // We just need to request it. - if (window.require) { - const userState = await window.require('electron').ipcRenderer.invoke('get-current-user'); + if (window.api) { + const userState = await window.api.common.getCurrentUser(); console.log('[HeaderController] Bootstrapping with initial user state:', userState); this.handleStateUpdate(userState); } else { @@ -96,8 +95,7 @@ class HeaderTransitionManager { //////// after_modelStateService //////// async handleStateUpdate(userState) { - const { ipcRenderer } = window.require('electron'); - const isConfigured = await ipcRenderer.invoke('model:are-providers-configured'); + const isConfigured = await window.api.apiKeyHeader.areProvidersConfigured(); if (isConfigured) { const { isLoggedIn } = userState; @@ -126,10 +124,9 @@ class HeaderTransitionManager { } // Check if permissions were previously completed - if (window.require) { - const { ipcRenderer } = window.require('electron'); + if (window.api) { try { - const permissionsCompleted = await ipcRenderer.invoke('check-permissions-completed'); + const permissionsCompleted = await window.api.headerController.checkPermissionsCompleted(); if (permissionsCompleted) { console.log('[HeaderController] Permissions were previously completed, checking current status...'); @@ -161,39 +158,33 @@ class HeaderTransitionManager { this.ensureHeader('main'); } - _resizeForMain() { - if (!window.require) return; - return window - .require('electron') - .ipcRenderer.invoke('resize-header-window', { width: 353, height: 47 }) + async _resizeForMain() { + if (!window.api) return; + console.log('[HeaderController] _resizeForMain: Resizing window to 353x47'); + return window.api.headerController.resizeHeaderWindow({ width: 353, height: 47 }) .catch(() => {}); } async _resizeForApiKey() { - if (!window.require) return; - return window - .require('electron') - .ipcRenderer.invoke('resize-header-window', { width: 350, height: 300 }) + if (!window.api) return; + console.log('[HeaderController] _resizeForApiKey: Resizing window to 350x300'); + return window.api.headerController.resizeHeaderWindow({ width: 350, height: 300 }) .catch(() => {}); } async _resizeForPermissionHeader() { - if (!window.require) return; - return window - .require('electron') - .ipcRenderer.invoke('resize-header-window', { width: 285, height: 220 }) + if (!window.api) return; + return window.api.headerController.resizeHeaderWindow({ width: 285, height: 220 }) .catch(() => {}); } async checkPermissions() { - if (!window.require) { + if (!window.api) { return { success: true }; } - - const { ipcRenderer } = window.require('electron'); try { - const permissions = await ipcRenderer.invoke('check-system-permissions'); + const permissions = await window.api.headerController.checkSystemPermissions(); console.log('[HeaderController] Current permissions:', permissions); if (!permissions.needsSetup) { diff --git a/src/ui/app/MainHeader.js b/src/ui/app/MainHeader.js index d0c4fa1..f4f2d82 100644 --- a/src/ui/app/MainHeader.js +++ b/src/ui/app/MainHeader.js @@ -370,8 +370,7 @@ export class MainHeader extends LitElement { async handleMouseDown(e) { e.preventDefault(); - const { ipcRenderer } = window.require('electron'); - const initialPosition = await ipcRenderer.invoke('get-header-position'); + const initialPosition = await window.api.mainHeader.getHeaderPosition(); this.dragState = { initialMouseX: e.screenX, @@ -398,8 +397,7 @@ export class MainHeader extends LitElement { 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); + window.api.mainHeader.moveHeaderTo(newWindowX, newWindowY); } handleMouseUp(e) { @@ -455,12 +453,12 @@ export class MainHeader extends LitElement { if (this.classList.contains('hiding')) { this.classList.add('hidden'); - if (window.require) { - window.require('electron').ipcRenderer.send('header-animation-finished', 'hidden'); + if (window.api) { + window.api.mainHeader.sendHeaderAnimationFinished('hidden'); } } else if (this.classList.contains('showing')) { - if (window.require) { - window.require('electron').ipcRenderer.send('header-animation-finished', 'visible'); + if (window.api) { + window.api.mainHeader.sendHeaderAnimationFinished('visible'); } } } @@ -474,8 +472,7 @@ export class MainHeader extends LitElement { super.connectedCallback(); this.addEventListener('animationend', this.handleAnimationEnd); - if (window.require) { - const { ipcRenderer } = window.require('electron'); + if (window.api) { this._sessionStateTextListener = (event, { success }) => { if (success) { @@ -489,13 +486,14 @@ export class MainHeader extends LitElement { } this.isTogglingSession = false; // ✨ 로딩 상태만 해제 }; + // window.api.mainHeader.onSessionStateText(this._sessionStateTextListener); ipcRenderer.on('listen:changeSessionResult', this._sessionStateTextListener); this._shortcutListener = (event, keybinds) => { console.log('[MainHeader] Received updated shortcuts:', keybinds); this.shortcuts = keybinds; }; - ipcRenderer.on('shortcuts-updated', this._shortcutListener); + window.api.mainHeader.onShortcutsUpdated(this._shortcutListener); } } @@ -508,36 +506,35 @@ export class MainHeader extends LitElement { this.animationEndTimer = null; } - if (window.require) { - const { ipcRenderer } = window.require('electron'); + if (window.api) { if (this._sessionStateTextListener) { + // window.api.mainHeader.removeOnSessionStateText(this._sessionStateTextListener); ipcRenderer.removeListener('listen:changeSessionResult', this._sessionStateTextListener); } if (this._shortcutListener) { - ipcRenderer.removeListener('shortcuts-updated', this._shortcutListener); + window.api.mainHeader.removeOnShortcutsUpdated(this._shortcutListener); } } } invoke(channel, ...args) { if (this.wasJustDragged) return; - if (window.require) { - window.require('electron').ipcRenderer.invoke(channel, ...args); + if (window.api) { + window.api.mainHeader.invoke(channel, ...args); } // return Promise.resolve(); } showSettingsWindow(element) { if (this.wasJustDragged) return; - if (window.require) { - const { ipcRenderer } = window.require('electron'); + if (window.api) { console.log(`[MainHeader] showSettingsWindow called at ${Date.now()}`); - ipcRenderer.send('cancel-hide-settings-window'); + window.api.mainHeader.cancelHideSettingsWindow(); if (element) { const { left, top, width, height } = element.getBoundingClientRect(); - ipcRenderer.send('show-settings-window', { + window.api.mainHeader.showSettingsWindow({ x: left, y: top, width, @@ -549,9 +546,9 @@ export class MainHeader extends LitElement { hideSettingsWindow() { if (this.wasJustDragged) return; - if (window.require) { + if (window.api) { console.log(`[MainHeader] hideSettingsWindow called at ${Date.now()}`); - window.require('electron').ipcRenderer.send('hide-settings-window'); + window.api.mainHeader.hideSettingsWindow(); } } diff --git a/src/ui/app/PermissionHeader.js b/src/ui/app/PermissionHeader.js index 96a01c1..d5761b1 100644 --- a/src/ui/app/PermissionHeader.js +++ b/src/ui/app/PermissionHeader.js @@ -288,13 +288,12 @@ export class PermissionHeader extends LitElement { } async checkPermissions() { - if (!window.require || this.isChecking) return; + if (!window.api || this.isChecking) return; this.isChecking = true; - const { ipcRenderer } = window.require('electron'); try { - const permissions = await ipcRenderer.invoke('check-system-permissions'); + const permissions = await window.api.permissionHeader.checkSystemPermissions(); console.log('[PermissionHeader] Permission check result:', permissions); const prevMic = this.microphoneGranted; @@ -324,13 +323,12 @@ export class PermissionHeader extends LitElement { } async handleMicrophoneClick() { - if (!window.require || this.microphoneGranted === 'granted') return; + if (!window.api || this.microphoneGranted === 'granted') return; console.log('[PermissionHeader] Requesting microphone permission...'); - const { ipcRenderer } = window.require('electron'); try { - const result = await ipcRenderer.invoke('check-system-permissions'); + const result = await window.api.permissionHeader.checkSystemPermissions(); console.log('[PermissionHeader] Microphone permission result:', result); if (result.microphone === 'granted') { @@ -340,7 +338,7 @@ export class PermissionHeader extends LitElement { } if (result.microphone === 'not-determined' || result.microphone === 'denied' || result.microphone === 'unknown' || result.microphone === 'restricted') { - const res = await ipcRenderer.invoke('request-microphone-permission'); + const res = await window.api.permissionHeader.requestMicrophonePermission(); if (res.status === 'granted' || res.success === true) { this.microphoneGranted = 'granted'; this.requestUpdate(); @@ -357,13 +355,12 @@ export class PermissionHeader extends LitElement { } async handleScreenClick() { - if (!window.require || this.screenGranted === 'granted') return; + if (!window.api || this.screenGranted === 'granted') return; console.log('[PermissionHeader] Checking screen recording permission...'); - const { ipcRenderer } = window.require('electron'); try { - const permissions = await ipcRenderer.invoke('check-system-permissions'); + const permissions = await window.api.permissionHeader.checkSystemPermissions(); console.log('[PermissionHeader] Screen permission check result:', permissions); if (permissions.screen === 'granted') { @@ -373,7 +370,7 @@ export class PermissionHeader extends LitElement { } if (permissions.screen === 'not-determined' || permissions.screen === 'denied' || permissions.screen === 'unknown' || permissions.screen === 'restricted') { console.log('[PermissionHeader] Opening screen recording preferences...'); - await ipcRenderer.invoke('open-system-preferences', 'screen-recording'); + await window.api.permissionHeader.openSystemPreferences('screen-recording'); } // Check permissions again after a delay @@ -389,10 +386,9 @@ export class PermissionHeader extends LitElement { this.microphoneGranted === 'granted' && this.screenGranted === 'granted') { // Mark permissions as completed - if (window.require) { - const { ipcRenderer } = window.require('electron'); + if (window.api) { try { - await ipcRenderer.invoke('mark-permissions-completed'); + await window.api.permissionHeader.markPermissionsCompleted(); console.log('[PermissionHeader] Marked permissions as completed'); } catch (error) { console.error('[PermissionHeader] Error marking permissions as completed:', error); @@ -405,8 +401,8 @@ export class PermissionHeader extends LitElement { handleClose() { console.log('Close button clicked'); - if (window.require) { - window.require('electron').ipcRenderer.invoke('quit-application'); + if (window.api) { + window.api.common.quitApplication(); } } diff --git a/src/ui/app/PickleGlassApp.js b/src/ui/app/PickleGlassApp.js index fc4939f..df920c3 100644 --- a/src/ui/app/PickleGlassApp.js +++ b/src/ui/app/PickleGlassApp.js @@ -74,10 +74,8 @@ export class PickleGlassApp extends LitElement { connectedCallback() { super.connectedCallback(); - if (window.require) { - const { ipcRenderer } = window.require('electron'); - - ipcRenderer.on('click-through-toggled', (_, isEnabled) => { + if (window.api) { + window.api.pickleGlassApp.onClickThroughToggled((_, isEnabled) => { this._isClickThrough = isEnabled; }); } @@ -85,9 +83,8 @@ export class PickleGlassApp extends LitElement { disconnectedCallback() { super.disconnectedCallback(); - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.removeAllListeners('click-through-toggled'); + if (window.api) { + window.api.pickleGlassApp.removeAllClickThroughListeners(); } } @@ -121,9 +118,8 @@ export class PickleGlassApp extends LitElement { } async handleClose() { - if (window.require) { - const { ipcRenderer } = window.require('electron'); - await ipcRenderer.invoke('quit-application'); + if (window.api) { + await window.api.common.quitApplication(); } } diff --git a/src/ui/app/content.html b/src/ui/app/content.html index 868e0db..8757011 100644 --- a/src/ui/app/content.html +++ b/src/ui/app/content.html @@ -1,7 +1,7 @@ - + Pickle Glass Content