rough refactor done

This commit is contained in:
samtiz 2025-07-13 15:12:05 +09:00
parent 586d44e57b
commit d936af46a3
15 changed files with 316 additions and 552 deletions

2
aec

@ -1 +1 @@
Subproject commit 9e11f4f95707714464194bdfc9db0222ec5c6163 Subproject commit f00bb1fb948053c752b916adfee19f90644a0b2f

View File

@ -9,6 +9,7 @@ const shortcutsService = require('../features/shortcuts/shortcutsService');
const askService = require('../features/ask/askService'); const askService = require('../features/ask/askService');
const listenService = require('../features/listen/listenService'); const listenService = require('../features/listen/listenService');
const permissionService = require('../features/common/services/permissionService');
module.exports = { module.exports = {
// Renderer로부터의 요청을 수신 // Renderer로부터의 요청을 수신
@ -33,6 +34,14 @@ module.exports = {
ipcMain.handle('save-shortcuts', async (event, newKeybinds) => await shortcutsService.handleSaveShortcuts(newKeybinds)); ipcMain.handle('save-shortcuts', async (event, newKeybinds) => await shortcutsService.handleSaveShortcuts(newKeybinds));
// Permissions
ipcMain.handle('check-system-permissions', async () => await permissionService.checkSystemPermissions());
ipcMain.handle('request-microphone-permission', async () => await permissionService.requestMicrophonePermission());
ipcMain.handle('open-system-preferences', async (event, section) => await permissionService.openSystemPreferences(section));
ipcMain.handle('mark-permissions-completed', async () => await permissionService.markPermissionsAsCompleted());
ipcMain.handle('check-permissions-completed', async () => await permissionService.checkPermissionsCompleted());
// User/Auth // User/Auth
ipcMain.handle('get-current-user', () => authService.getCurrentUser()); ipcMain.handle('get-current-user', () => authService.getCurrentUser());
ipcMain.handle('start-firebase-auth', async () => await authService.startFirebaseAuthFlow()); ipcMain.handle('start-firebase-auth', async () => await authService.startFirebaseAuthFlow());
@ -67,7 +76,6 @@ module.exports = {
ipcMain.handle('ask:sendQuestionFromAsk', async (event, userPrompt) => await askService.sendMessage(userPrompt)); ipcMain.handle('ask:sendQuestionFromAsk', async (event, userPrompt) => await askService.sendMessage(userPrompt));
ipcMain.handle('ask:sendQuestionFromSummary', async (event, userPrompt) => await askService.sendMessage(userPrompt)); ipcMain.handle('ask:sendQuestionFromSummary', async (event, userPrompt) => await askService.sendMessage(userPrompt));
ipcMain.handle('ask:toggleAskButton', async () => await askService.toggleAskButton()); ipcMain.handle('ask:toggleAskButton', async () => await askService.toggleAskButton());
ipcMain.handle('stop-screen-capture', async () => askService.handleStopScreenCapture());
// Listen // Listen
ipcMain.handle('listen:sendMicAudio', async (event, { data, mimeType }) => await listenService.handleSendMicAudioContent(data, mimeType)); ipcMain.handle('listen:sendMicAudio', async (event, { data, mimeType }) => await listenService.handleSendMicAudioContent(data, mimeType));
@ -98,12 +106,11 @@ module.exports = {
ipcMain.handle('model:validate-key', async (e, { provider, key }) => await modelStateService.handleValidateKey(provider, key)); ipcMain.handle('model:validate-key', async (e, { provider, key }) => await modelStateService.handleValidateKey(provider, key));
ipcMain.handle('model:get-all-keys', () => modelStateService.getAllApiKeys()); 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:set-api-key', async (e, { provider, key }) => await modelStateService.setApiKey(provider, key));
ipcMain.handle('model:remove-api-key', async (e, { provider }) => await modelStateService.handleRemoveApiKey(provider)); ipcMain.handle('model:remove-api-key', async (e, provider) => await modelStateService.handleRemoveApiKey(provider));
ipcMain.handle('model:get-selected-models', () => modelStateService.getSelectedModels()); ipcMain.handle('model:get-selected-models', () => modelStateService.getSelectedModels());
ipcMain.handle('model:set-selected-model', async (e, { type, modelId }) => await modelStateService.handleSetSelectedModel(type, modelId)); 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:get-available-models', (e, { type }) => modelStateService.getAvailableModels(type));
ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured()); 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()); ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());

View File

@ -1,5 +1,5 @@
// src/bridge/windowBridge.js // src/bridge/windowBridge.js
const { ipcMain } = require('electron'); const { ipcMain, BrowserWindow } = require('electron');
const windowManager = require('../window/windowManager'); const windowManager = require('../window/windowManager');
module.exports = { module.exports = {
@ -15,6 +15,16 @@ module.exports = {
ipcMain.handle('move-window-step', (event, direction) => windowManager.moveWindowStep(direction)); ipcMain.handle('move-window-step', (event, direction) => windowManager.moveWindowStep(direction));
ipcMain.on('close-shortcut-editor', () => windowManager.closeWindow('shortcut-settings')); ipcMain.on('close-shortcut-editor', () => windowManager.closeWindow('shortcut-settings'));
// Newly moved handlers from windowManager
ipcMain.on('header-state-changed', (event, state) => windowManager.handleHeaderStateChanged(state));
ipcMain.on('header-animation-finished', (event, state) => windowManager.handleHeaderAnimationFinished(state));
ipcMain.handle('get-header-position', () => windowManager.getHeaderPosition());
ipcMain.handle('move-header', (event, newX, newY) => windowManager.moveHeader(newX, newY));
ipcMain.handle('move-header-to', (event, newX, newY) => windowManager.moveHeaderTo(newX, newY));
ipcMain.handle('adjust-window-height', (event, targetHeight) => windowManager.adjustWindowHeight(event.sender, targetHeight));
ipcMain.handle('toggle-all-windows-visibility', () => windowManager.toggleAllWindowsVisibility());
ipcMain.on('animation-finished', (event) => windowManager.handleAnimationFinished(event.sender));
ipcMain.handle('ask:closeAskWindow', () => windowManager.closeAskWindow());
}, },
notifyFocusChange(win, isFocused) { notifyFocusChange(win, isFocused) {

View File

@ -1,6 +1,18 @@
const { BrowserWindow } = require('electron'); const { BrowserWindow } = require('electron');
const { createStreamingLLM } = require('../common/ai/factory'); const { createStreamingLLM } = require('../common/ai/factory');
const { getCurrentModelInfo, windowPool, updateLayout } = require('../../window/windowManager'); // Lazy require helper to avoid circular dependency issues
const getWindowManager = () => require('../../window/windowManager');
const getWindowPool = () => {
try {
return getWindowManager().windowPool;
} catch {
return null;
}
};
const updateLayout = () => getWindowManager().updateLayout();
const ensureAskWindowVisible = () => getWindowManager().ensureAskWindowVisible();
const sessionRepository = require('../common/repositories/session'); const sessionRepository = require('../common/repositories/session');
const askRepository = require('./repositories'); const askRepository = require('./repositories');
const { getSystemPrompt } = require('../common/prompts/promptBuilder'); const { getSystemPrompt } = require('../common/prompts/promptBuilder');
@ -10,6 +22,7 @@ const os = require('os');
const util = require('util'); const util = require('util');
const execFile = util.promisify(require('child_process').execFile); const execFile = util.promisify(require('child_process').execFile);
const { desktopCapturer } = require('electron'); const { desktopCapturer } = require('electron');
const modelStateService = require('../common/services/modelStateService');
// Try to load sharp, but don't fail if it's not available // Try to load sharp, but don't fail if it's not available
let sharp; let sharp;
@ -126,33 +139,33 @@ class AskService {
} }
_broadcastState() { _broadcastState() {
const askWindow = windowPool.get('ask'); const askWindow = getWindowPool()?.get('ask');
if (askWindow && !askWindow.isDestroyed()) { if (askWindow && !askWindow.isDestroyed()) {
askWindow.webContents.send('ask:stateUpdate', this.state); askWindow.webContents.send('ask:stateUpdate', this.state);
} }
} }
async toggleAskButton() { async toggleAskButton() {
const askWindow = windowPool.get('ask'); const askWindow = getWindowPool()?.get('ask');
// 답변이 있거나 스트리밍 중일 때 // 답변이 있거나 스트리밍 중일 때
const hasContent = this.state.isStreaming || (this.state.currentResponse && this.state.currentResponse.length > 0); const hasContent = this.state.isStreaming || (this.state.currentResponse && this.state.currentResponse.length > 0);
if (askWindow.isVisible() && hasContent) { if (askWindow && askWindow.isVisible() && hasContent) {
// 창을 닫는 대신, 텍스트 입력창만 토글합니다. // 창을 닫는 대신, 텍스트 입력창만 토글합니다.
this.state.showTextInput = !this.state.showTextInput; this.state.showTextInput = !this.state.showTextInput;
this._broadcastState(); // 변경된 상태 전파 this._broadcastState(); // 변경된 상태 전파
} else { } else {
// 기존의 창 보이기/숨기기 로직 // 기존의 창 보이기/숨기기 로직
if (askWindow.isVisible()) { if (askWindow && askWindow.isVisible()) {
askWindow.webContents.send('window-hide-animation'); askWindow.webContents.send('window-hide-animation');
this.state.isVisible = false; this.state.isVisible = false;
} else { } else {
console.log('[AskService] Showing hidden Ask window'); console.log('[AskService] Showing hidden Ask window');
this.state.isVisible = true; this.state.isVisible = true;
askWindow.show(); askWindow?.show();
updateLayout(); updateLayout();
askWindow.webContents.send('window-show-animation'); askWindow?.webContents.send('window-show-animation');
} }
// 창이 다시 열릴 때를 대비해 상태를 초기화하고 전파합니다. // 창이 다시 열릴 때를 대비해 상태를 초기화하고 전파합니다.
if (this.state.isVisible) { if (this.state.isVisible) {
@ -182,6 +195,8 @@ class AskService {
* @returns {Promise<{success: boolean, response?: string, error?: string}>} * @returns {Promise<{success: boolean, response?: string, error?: string}>}
*/ */
async sendMessage(userPrompt, conversationHistoryRaw=[]) { async sendMessage(userPrompt, conversationHistoryRaw=[]) {
ensureAskWindowVisible();
if (this.abortController) { if (this.abortController) {
this.abortController.abort('New request received.'); this.abortController.abort('New request received.');
} }
@ -212,7 +227,7 @@ class AskService {
await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() }); await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() });
console.log(`[AskService] DB: Saved user prompt to session ${sessionId}`); console.log(`[AskService] DB: Saved user prompt to session ${sessionId}`);
const modelInfo = await getCurrentModelInfo(null, { type: 'llm' }); const modelInfo = modelStateService.getCurrentModelInfo('llm');
if (!modelInfo || !modelInfo.apiKey) { if (!modelInfo || !modelInfo.apiKey) {
throw new Error('AI model or API key not configured.'); throw new Error('AI model or API key not configured.');
} }
@ -252,7 +267,7 @@ class AskService {
}); });
const response = await streamingLLM.streamChat(messages); const response = await streamingLLM.streamChat(messages);
const askWin = windowPool.get('ask'); const askWin = getWindowPool()?.get('ask');
if (!askWin || askWin.isDestroyed()) { if (!askWin || askWin.isDestroyed()) {
console.error("[AskService] Ask window is not available to send stream to."); console.error("[AskService] Ask window is not available to send stream to.");
@ -351,11 +366,6 @@ class AskService {
} }
} }
handleStopScreenCapture() {
lastScreenshot = null;
console.log('[AskService] Stopped screen capture and cleared cache.');
return { success: true };
}
} }
const askService = new AskService(); const askService = new AskService();

View File

@ -82,7 +82,7 @@ async function createSTT({ apiKey, language = 'en', callbacks = {}, usePortkey =
silence_duration_ms: 100, silence_duration_ms: 100,
}, },
input_audio_noise_reduction: { input_audio_noise_reduction: {
type: 'far_field' type: 'near_field'
} }
} }
}; };

View File

@ -359,6 +359,7 @@ class ModelStateService {
} }
removeApiKey(provider) { removeApiKey(provider) {
console.log(`[ModelStateService] Removing API key for provider: ${provider}`);
if (provider in this.state.apiKeys) { if (provider in this.state.apiKeys) {
this.state.apiKeys[provider] = null; this.state.apiKeys[provider] = null;
const llmProvider = this.getProviderForModel('llm', this.state.selectedModels.llm); const llmProvider = this.getProviderForModel('llm', this.state.selectedModels.llm);
@ -542,6 +543,7 @@ class ModelStateService {
} }
async handleRemoveApiKey(provider) { async handleRemoveApiKey(provider) {
console.log(`[ModelStateService] handleRemoveApiKey: ${provider}`);
const success = this.removeApiKey(provider); const success = this.removeApiKey(provider);
if (success) { if (success) {
const selectedModels = this.getSelectedModels(); const selectedModels = this.getSelectedModels();

View File

@ -0,0 +1,119 @@
const { systemPreferences, shell, desktopCapturer } = require('electron');
const permissionRepository = require('../repositories/permission');
class PermissionService {
async checkSystemPermissions() {
const permissions = {
microphone: 'unknown',
screen: 'unknown',
needsSetup: true
};
try {
if (process.platform === 'darwin') {
const micStatus = systemPreferences.getMediaAccessStatus('microphone');
console.log('[Permissions] Microphone status:', micStatus);
permissions.microphone = micStatus;
const screenStatus = systemPreferences.getMediaAccessStatus('screen');
console.log('[Permissions] Screen status:', screenStatus);
permissions.screen = screenStatus;
permissions.needsSetup = micStatus !== 'granted' || screenStatus !== 'granted';
} else {
permissions.microphone = 'granted';
permissions.screen = 'granted';
permissions.needsSetup = false;
}
console.log('[Permissions] System permissions status:', permissions);
return permissions;
} catch (error) {
console.error('[Permissions] Error checking permissions:', error);
return {
microphone: 'unknown',
screen: 'unknown',
needsSetup: true,
error: error.message
};
}
}
async requestMicrophonePermission() {
if (process.platform !== 'darwin') {
return { success: true };
}
try {
const status = systemPreferences.getMediaAccessStatus('microphone');
console.log('[Permissions] Microphone status:', status);
if (status === 'granted') {
return { success: true, status: 'granted' };
}
const granted = await systemPreferences.askForMediaAccess('microphone');
return {
success: granted,
status: granted ? 'granted' : 'denied'
};
} catch (error) {
console.error('[Permissions] Error requesting microphone permission:', error);
return {
success: false,
error: error.message
};
}
}
async openSystemPreferences(section) {
if (process.platform !== 'darwin') {
return { success: false, error: 'Not supported on this platform' };
}
try {
if (section === 'screen-recording') {
try {
console.log('[Permissions] Triggering screen capture request to register app...');
await desktopCapturer.getSources({
types: ['screen'],
thumbnailSize: { width: 1, height: 1 }
});
console.log('[Permissions] App registered for screen recording');
} catch (captureError) {
console.log('[Permissions] Screen capture request triggered (expected to fail):', captureError.message);
}
// await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture');
}
return { success: true };
} catch (error) {
console.error('[Permissions] Error opening system preferences:', error);
return { success: false, error: error.message };
}
}
async markPermissionsAsCompleted() {
try {
await permissionRepository.markPermissionsAsCompleted();
console.log('[Permissions] Marked permissions as completed');
return { success: true };
} catch (error) {
console.error('[Permissions] Error marking permissions as completed:', error);
return { success: false, error: error.message };
}
}
async checkPermissionsCompleted() {
try {
const completed = await permissionRepository.checkPermissionsCompleted();
console.log('[Permissions] Permissions completed status:', completed);
return completed;
} catch (error) {
console.error('[Permissions] Error checking permissions completed status:', error);
return false;
}
}
}
const permissionService = new PermissionService();
module.exports = permissionService;

View File

@ -1,6 +1,7 @@
const { BrowserWindow } = require('electron'); const { BrowserWindow } = require('electron');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const { createSTT } = require('../../common/ai/factory'); const { createSTT } = require('../../common/ai/factory');
const modelStateService = require('../../common/services/modelStateService');
// const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager'); // const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager');
const COMPLETION_DEBOUNCE_MS = 2000; const COMPLETION_DEBOUNCE_MS = 2000;
@ -131,8 +132,7 @@ class SttService {
async initializeSttSessions(language = 'en') { async initializeSttSessions(language = 'en') {
const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || language || 'en'; const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || language || 'en';
const { getCurrentModelInfo } = require('../../../window/windowManager'); const modelInfo = modelStateService.getCurrentModelInfo('stt');
const modelInfo = await getCurrentModelInfo(null, { type: 'stt' });
if (!modelInfo || !modelInfo.apiKey) { if (!modelInfo || !modelInfo.apiKey) {
throw new Error('AI model or API key is not configured.'); throw new Error('AI model or API key is not configured.');
} }
@ -144,6 +144,7 @@ class SttService {
console.log('[SttService] Ignoring message - session already closed'); console.log('[SttService] Ignoring message - session already closed');
return; return;
} }
console.log('[SttService] handleMyMessage', message);
if (this.modelInfo.provider === 'whisper') { if (this.modelInfo.provider === 'whisper') {
// Whisper STT emits 'transcription' events with different structure // Whisper STT emits 'transcription' events with different structure
@ -411,8 +412,7 @@ class SttService {
let modelInfo = this.modelInfo; let modelInfo = this.modelInfo;
if (!modelInfo) { if (!modelInfo) {
console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...'); console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
const { getCurrentModelInfo } = require('../../../window/windowManager'); modelInfo = modelStateService.getCurrentModelInfo('stt');
modelInfo = await getCurrentModelInfo(null, { type: 'stt' });
} }
if (!modelInfo) { if (!modelInfo) {
throw new Error('STT model info could not be retrieved.'); throw new Error('STT model info could not be retrieved.');
@ -433,8 +433,7 @@ class SttService {
let modelInfo = this.modelInfo; let modelInfo = this.modelInfo;
if (!modelInfo) { if (!modelInfo) {
console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...'); console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
const { getCurrentModelInfo } = require('../../../window/windowManager'); modelInfo = modelStateService.getCurrentModelInfo('stt');
modelInfo = await getCurrentModelInfo(null, { type: 'stt' });
} }
if (!modelInfo) { if (!modelInfo) {
throw new Error('STT model info could not be retrieved.'); throw new Error('STT model info could not be retrieved.');
@ -515,8 +514,7 @@ class SttService {
let modelInfo = this.modelInfo; let modelInfo = this.modelInfo;
if (!modelInfo) { if (!modelInfo) {
console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...'); console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
const { getCurrentModelInfo } = require('../../../window/windowManager'); modelInfo = modelStateService.getCurrentModelInfo('stt');
modelInfo = await getCurrentModelInfo(null, { type: 'stt' });
} }
if (!modelInfo) { if (!modelInfo) {
throw new Error('STT model info could not be retrieved.'); throw new Error('STT model info could not be retrieved.');

View File

@ -3,6 +3,7 @@ const { getSystemPrompt } = require('../../common/prompts/promptBuilder.js');
const { createLLM } = require('../../common/ai/factory'); const { createLLM } = require('../../common/ai/factory');
const sessionRepository = require('../../common/repositories/session'); const sessionRepository = require('../../common/repositories/session');
const summaryRepository = require('./repositories'); const summaryRepository = require('./repositories');
const modelStateService = require('../../common/services/modelStateService');
// const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager.js'); // const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager.js');
class SummaryService { class SummaryService {
@ -97,8 +98,7 @@ Please build upon this context while analyzing the new conversation segments.
await sessionRepository.touch(this.currentSessionId); await sessionRepository.touch(this.currentSessionId);
} }
const { getCurrentModelInfo } = require('../../../window/windowManager'); const modelInfo = modelStateService.getCurrentModelInfo('llm');
const modelInfo = await getCurrentModelInfo(null, { type: 'llm' });
if (!modelInfo || !modelInfo.apiKey) { if (!modelInfo || !modelInfo.apiKey) {
throw new Error('AI model or API key is not configured.'); throw new Error('AI model or API key is not configured.');
} }

View File

@ -374,6 +374,7 @@ async function removeApiKey() {
} }
}); });
console.log('[SettingsService] API key removed for all providers');
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('[SettingsService] Error removing API key:', error); console.error('[SettingsService] Error removing API key:', error);

View File

@ -1,6 +1,7 @@
const { globalShortcut, screen } = require('electron'); const { globalShortcut, screen } = require('electron');
const shortcutsRepository = require('./repositories'); const shortcutsRepository = require('./repositories');
const internalBridge = require('../../bridge/internalBridge'); const internalBridge = require('../../bridge/internalBridge');
const askService = require('../ask/askService');
class ShortcutsService { class ShortcutsService {
@ -210,8 +211,7 @@ class ShortcutsService {
callback = () => this.toggleAllWindowsVisibility(this.windowPool); callback = () => this.toggleAllWindowsVisibility(this.windowPool);
break; break;
case 'nextStep': case 'nextStep':
// Late require to prevent circular dependency callback = () => askService.toggleAskButton();
callback = () => require('../../window/windowManager').toggleFeature('ask', {ask: { targetVisibility: 'show' }});
break; break;
case 'scrollUp': case 'scrollUp':
callback = () => { callback = () => {

View File

@ -276,12 +276,6 @@ contextBridge.exposeInMainWorld('api', {
startMacosSystemAudio: () => ipcRenderer.invoke('listen:startMacosSystemAudio'), startMacosSystemAudio: () => ipcRenderer.invoke('listen:startMacosSystemAudio'),
stopMacosSystemAudio: () => ipcRenderer.invoke('listen:stopMacosSystemAudio'), stopMacosSystemAudio: () => ipcRenderer.invoke('listen:stopMacosSystemAudio'),
// Screen Capture
captureScreenshot: (options) => ipcRenderer.invoke('capture-screenshot', options),
getCurrentScreenshot: () => ipcRenderer.invoke('get-current-screenshot'),
startScreenCapture: () => ipcRenderer.invoke('start-screen-capture'),
stopScreenCapture: () => ipcRenderer.invoke('stop-screen-capture'),
// Session Management // Session Management
isSessionActive: () => ipcRenderer.invoke('is-session-active'), isSessionActive: () => ipcRenderer.invoke('is-session-active'),

View File

@ -38,13 +38,10 @@ const isMacOS = window.api.platform.isMacOS;
let mediaStream = null; let mediaStream = null;
let micMediaStream = null; let micMediaStream = null;
let screenshotInterval = null;
let audioContext = null; let audioContext = null;
let audioProcessor = null; let audioProcessor = null;
let systemAudioContext = null; let systemAudioContext = null;
let systemAudioProcessor = null; let systemAudioProcessor = null;
let currentImageQuality = 'medium';
let lastScreenshotBase64 = null;
let systemAudioBuffer = []; let systemAudioBuffer = [];
const MAX_SYSTEM_BUFFER_SIZE = 10; const MAX_SYSTEM_BUFFER_SIZE = 10;
@ -140,10 +137,6 @@ function runAecSync(micF32, sysF32) {
return micF32; return micF32;
} }
// ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
// 새로운 프레임 단위 처리 로직
// ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
const frameSize = 160; // AEC 모듈 초기화 시 설정한 프레임 크기 const frameSize = 160; // AEC 모듈 초기화 시 설정한 프레임 크기
const numFrames = Math.floor(micF32.length / frameSize); const numFrames = Math.floor(micF32.length / frameSize);
@ -418,94 +411,10 @@ function setupSystemAudioProcessing(systemStream) {
return { context: systemAudioContext, processor: systemProcessor }; return { context: systemAudioContext, processor: systemProcessor };
} }
// ---------------------------
// Screenshot functions (exact from renderer.js)
// ---------------------------
async function captureScreenshot(imageQuality = 'medium', isManual = false) {
console.log(`Capturing ${isManual ? 'manual' : 'automated'} screenshot...`);
// Check rate limiting for automated screenshots only
if (!isManual && tokenTracker.shouldThrottle()) {
console.log('Automated screenshot skipped due to rate limiting');
return;
}
try {
// Request screenshot from main process
const result = await window.api.listenCapture.captureScreenshot({
quality: imageQuality,
});
if (result.success && result.base64) {
// Store the latest screenshot
lastScreenshotBase64 = result.base64;
// Note: sendResult is not defined in the original, this was likely an error
// Commenting out this section as it references undefined variable
/*
if (sendResult.success) {
// Track image tokens after successful send
const imageTokens = tokenTracker.calculateImageTokens(result.width || 1920, result.height || 1080);
tokenTracker.addTokens(imageTokens, 'image');
console.log(`📊 Image sent successfully - ${imageTokens} tokens used (${result.width}x${result.height})`);
} else {
console.error('Failed to send image:', sendResult.error);
}
*/
} else {
console.error('Failed to capture screenshot:', result.error);
}
} catch (error) {
console.error('Error capturing screenshot:', error);
}
}
async function captureManualScreenshot(imageQuality = null) {
console.log('Manual screenshot triggered');
const quality = imageQuality || currentImageQuality;
await captureScreenshot(quality, true);
}
async function getCurrentScreenshot() {
try {
// First try to get a fresh screenshot from main process
const result = await window.api.listenCapture.getCurrentScreenshot();
if (result.success && result.base64) {
console.log('Got fresh screenshot from main process');
return result.base64;
}
// If no screenshot available, capture one now
console.log('No screenshot available, capturing new one');
const captureResult = await window.api.listenCapture.captureScreenshot({
quality: currentImageQuality,
});
if (captureResult.success && captureResult.base64) {
lastScreenshotBase64 = captureResult.base64;
return captureResult.base64;
}
// Fallback to last stored screenshot
if (lastScreenshotBase64) {
console.log('Using cached screenshot');
return lastScreenshotBase64;
}
throw new Error('Failed to get screenshot');
} catch (error) {
console.error('Error getting current screenshot:', error);
return null;
}
}
// --------------------------- // ---------------------------
// Main capture functions (exact from renderer.js) // Main capture functions (exact from renderer.js)
// --------------------------- // ---------------------------
async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'medium') { async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'medium') {
// Store the image quality for manual screenshots
currentImageQuality = imageQuality;
// Reset token tracker when starting new capture session // Reset token tracker when starting new capture session
tokenTracker.reset(); tokenTracker.reset();
@ -534,13 +443,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
} }
} }
// Initialize screen capture in main process
const screenResult = await window.api.listenCapture.startScreenCapture();
if (!screenResult.success) {
throw new Error('Failed to start screen capture: ' + screenResult.error);
}
try { try {
micMediaStream = await navigator.mediaDevices.getUserMedia({ micMediaStream = await navigator.mediaDevices.getUserMedia({
audio: { audio: {
@ -602,12 +504,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
// Windows - capture mic and system audio separately using native loopback // Windows - capture mic and system audio separately using native loopback
console.log('Starting Windows capture with native loopback audio...'); console.log('Starting Windows capture with native loopback audio...');
// Start screen capture in main process for screenshots
const screenResult = await window.api.listenCapture.startScreenCapture();
if (!screenResult.success) {
throw new Error('Failed to start screen capture: ' + screenResult.error);
}
// Ensure STT sessions are initialized before starting audio capture // Ensure STT sessions are initialized before starting audio capture
const sessionActive = await window.api.listenCapture.isSessionActive(); const sessionActive = await window.api.listenCapture.isSessionActive();
if (!sessionActive) { if (!sessionActive) {
@ -656,20 +552,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
// Continue without system audio // Continue without system audio
} }
} }
// Start capturing screenshots - check if manual mode
if (screenshotIntervalSeconds === 'manual' || screenshotIntervalSeconds === 'Manual') {
console.log('Manual mode enabled - screenshots will be captured on demand only');
// Don't start automatic capture in manual mode
} else {
// 스크린샷 기능 활성화 (chatModel에서 사용)
const intervalMilliseconds = parseInt(screenshotIntervalSeconds) * 1000;
screenshotInterval = setInterval(() => captureScreenshot(imageQuality), intervalMilliseconds);
// Capture first screenshot immediately
setTimeout(() => captureScreenshot(imageQuality), 100);
console.log(`📸 Screenshot capture enabled with ${screenshotIntervalSeconds}s interval`);
}
} catch (err) { } catch (err) {
console.error('Error starting capture:', err); console.error('Error starting capture:', err);
// Note: pickleGlass.e() is not available in this context, commenting out // Note: pickleGlass.e() is not available in this context, commenting out
@ -678,11 +560,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
} }
function stopCapture() { function stopCapture() {
if (screenshotInterval) {
clearInterval(screenshotInterval);
screenshotInterval = null;
}
// Clean up microphone resources // Clean up microphone resources
if (audioProcessor) { if (audioProcessor) {
audioProcessor.disconnect(); audioProcessor.disconnect();
@ -713,11 +590,6 @@ function stopCapture() {
micMediaStream = null; micMediaStream = null;
} }
// Stop screen capture in main process
window.api.listenCapture.stopScreenCapture().catch(err => {
console.error('Error stopping screen capture:', err);
});
// Stop macOS audio capture if running // Stop macOS audio capture if running
if (isMacOS) { if (isMacOS) {
window.api.listenCapture.stopMacosSystemAudio().catch(err => { window.api.listenCapture.stopMacosSystemAudio().catch(err => {
@ -735,19 +607,14 @@ module.exports = {
disposeAec, // 필요시 Rust 객체 파괴 disposeAec, // 필요시 Rust 객체 파괴
startCapture, startCapture,
stopCapture, stopCapture,
captureManualScreenshot,
getCurrentScreenshot,
isLinux, isLinux,
isMacOS, isMacOS,
}; };
// Expose functions to global scope for external access (exact from renderer.js) // Expose functions to global scope for external access (exact from renderer.js)
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.captureManualScreenshot = captureManualScreenshot;
window.listenCapture = module.exports; window.listenCapture = module.exports;
window.pickleGlass = window.pickleGlass || {}; window.pickleGlass = window.pickleGlass || {};
window.pickleGlass.startCapture = startCapture; window.pickleGlass.startCapture = startCapture;
window.pickleGlass.stopCapture = stopCapture; window.pickleGlass.stopCapture = stopCapture;
window.pickleGlass.captureManualScreenshot = captureManualScreenshot;
window.pickleGlass.getCurrentScreenshot = getCurrentScreenshot;
} }

View File

@ -693,6 +693,7 @@ export class SettingsView extends LitElement {
} }
async handleClearKey(provider) { async handleClearKey(provider) {
console.log(`[SettingsView] handleClearKey: ${provider}`);
this.saving = true; this.saving = true;
await window.api.settingsView.removeApiKey(provider); await window.api.settingsView.removeApiKey(provider);
this.apiKeys = { ...this.apiKeys, [provider]: '' }; this.apiKeys = { ...this.apiKeys, [provider]: '' };
@ -1097,13 +1098,6 @@ export class SettingsView extends LitElement {
} }
} }
async handleClearApiKey() {
console.log('Clear API Key clicked');
await window.api.settingsView.removeApiKey();
this.apiKey = null;
this.requestUpdate();
}
handleQuit() { handleQuit() {
console.log('Quit clicked'); console.log('Quit clicked');
window.api.settingsView.quitApplication(); window.api.settingsView.quitApplication();

View File

@ -1,4 +1,4 @@
const { BrowserWindow, globalShortcut, ipcMain, screen, app, shell, desktopCapturer } = require('electron'); const { BrowserWindow, globalShortcut, screen, app, shell } = require('electron');
const WindowLayoutManager = require('./windowLayoutManager'); const WindowLayoutManager = require('./windowLayoutManager');
const SmoothMovementManager = require('./smoothMovementManager'); const SmoothMovementManager = require('./smoothMovementManager');
const path = require('node:path'); const path = require('node:path');
@ -570,10 +570,7 @@ function createWindows() {
} }
function setupIpcHandlers(movementManager) { function setupIpcHandlers(movementManager) {
setupApiKeyIPC();
// quit-application handler moved to windowBridge.js to avoid duplication // quit-application handler moved to windowBridge.js to avoid duplication
screen.on('display-added', (event, newDisplay) => { screen.on('display-added', (event, newDisplay) => {
console.log('[Display] New display added:', newDisplay.id); console.log('[Display] New display added:', newDisplay.id);
}); });
@ -591,10 +588,9 @@ function setupIpcHandlers(movementManager) {
// console.log('[Display] Display metrics changed:', display.id, changedMetrics); // console.log('[Display] Display metrics changed:', display.id, changedMetrics);
updateLayout(); updateLayout();
}); });
}
// Content protection handlers moved to windowBridge.js to avoid duplication const handleHeaderStateChanged = (state) => {
ipcMain.on('header-state-changed', (event, state) => {
console.log(`[WindowManager] Header state changed to: ${state}`); console.log(`[WindowManager] Header state changed to: ${state}`);
currentHeaderState = state; currentHeaderState = state;
@ -604,11 +600,9 @@ function setupIpcHandlers(movementManager) {
destroyFeatureWindows(); destroyFeatureWindows();
} }
internalBridge.emit('reregister-shortcuts'); internalBridge.emit('reregister-shortcuts');
}); };
// resize-header-window handler moved to windowBridge.js to avoid duplication const handleHeaderAnimationFinished = (state) => {
ipcMain.on('header-animation-finished', (event, state) => {
const header = windowPool.get('header'); const header = windowPool.get('header');
if (!header || header.isDestroyed()) return; if (!header || header.isDestroyed()) return;
@ -619,47 +613,42 @@ function setupIpcHandlers(movementManager) {
console.log('[WindowManager] Header shown after animation.'); console.log('[WindowManager] Header shown after animation.');
updateLayout(); updateLayout();
} }
}); };
ipcMain.handle('get-header-position', () => { const getHeaderPosition = () => {
const header = windowPool.get('header'); const header = windowPool.get('header');
if (header) { if (header) {
const [x, y] = header.getPosition(); const [x, y] = header.getPosition();
return { x, y }; return { x, y };
} }
return { x: 0, y: 0 }; return { x: 0, y: 0 };
}); };
ipcMain.handle('move-header', (event, newX, newY) => { const moveHeader = (newX, newY) => {
const header = windowPool.get('header'); const header = windowPool.get('header');
if (header) { if (header) {
const currentY = newY !== undefined ? newY : header.getBounds().y; const currentY = newY !== undefined ? newY : header.getBounds().y;
header.setPosition(newX, currentY, false); header.setPosition(newX, currentY, false);
updateLayout(); updateLayout();
} }
}); };
ipcMain.handle('move-header-to', (event, newX, newY) => { const moveHeaderTo = (newX, newY) => {
const header = windowPool.get('header'); const header = windowPool.get('header');
if (header) { if (header) {
const targetDisplay = screen.getDisplayNearestPoint({ x: newX, y: newY }); const targetDisplay = screen.getDisplayNearestPoint({ x: newX, y: newY });
const { x: workAreaX, y: workAreaY, width, height } = targetDisplay.workArea; const { x: workAreaX, y: workAreaY, width, height } = targetDisplay.workArea;
const headerBounds = header.getBounds(); const headerBounds = header.getBounds();
// Only clamp if the new position would actually go out of bounds
// This prevents progressive restriction of movement
let clampedX = newX; let clampedX = newX;
let clampedY = newY; let clampedY = newY;
// Check if we need to clamp X position
if (newX < workAreaX) { if (newX < workAreaX) {
clampedX = workAreaX; clampedX = workAreaX;
} else if (newX + headerBounds.width > workAreaX + width) { } else if (newX + headerBounds.width > workAreaX + width) {
clampedX = workAreaX + width - headerBounds.width; clampedX = workAreaX + width - headerBounds.width;
} }
// Check if we need to clamp Y position
if (newY < workAreaY) { if (newY < workAreaY) {
clampedY = workAreaY; clampedY = workAreaY;
} else if (newY + headerBounds.height > workAreaY + height) { } else if (newY + headerBounds.height > workAreaY + height) {
@ -667,16 +656,12 @@ function setupIpcHandlers(movementManager) {
} }
header.setPosition(clampedX, clampedY, false); header.setPosition(clampedX, clampedY, false);
updateLayout(); updateLayout();
} }
}); };
const adjustWindowHeight = (sender, targetHeight) => {
// move-window-step handler moved to windowBridge.js to avoid duplication const senderWindow = BrowserWindow.fromWebContents(sender);
ipcMain.handle('adjust-window-height', (event, targetHeight) => {
const senderWindow = BrowserWindow.fromWebContents(event.sender);
if (senderWindow) { if (senderWindow) {
const wasResizable = senderWindow.isResizable(); const wasResizable = senderWindow.isResizable();
if (!wasResizable) { if (!wasResizable) {
@ -702,276 +687,47 @@ function setupIpcHandlers(movementManager) {
updateLayout(); updateLayout();
} }
});
ipcMain.handle('check-system-permissions', async () => {
const { systemPreferences } = require('electron');
const permissions = {
microphone: 'unknown',
screen: 'unknown',
needsSetup: true
}; };
try { const handleAnimationFinished = (sender) => {
if (process.platform === 'darwin') { const win = BrowserWindow.fromWebContents(sender);
// Check microphone permission on macOS
const micStatus = systemPreferences.getMediaAccessStatus('microphone');
console.log('[Permissions] Microphone status:', micStatus);
permissions.microphone = micStatus;
// Check screen recording permission using the system API
const screenStatus = systemPreferences.getMediaAccessStatus('screen');
console.log('[Permissions] Screen status:', screenStatus);
permissions.screen = screenStatus;
permissions.needsSetup = micStatus !== 'granted' || screenStatus !== 'granted';
} else {
permissions.microphone = 'granted';
permissions.screen = 'granted';
permissions.needsSetup = false;
}
console.log('[Permissions] System permissions status:', permissions);
return permissions;
} catch (error) {
console.error('[Permissions] Error checking permissions:', error);
return {
microphone: 'unknown',
screen: 'unknown',
needsSetup: true,
error: error.message
};
}
});
ipcMain.handle('request-microphone-permission', async () => {
if (process.platform !== 'darwin') {
return { success: true };
}
const { systemPreferences } = require('electron');
try {
const status = systemPreferences.getMediaAccessStatus('microphone');
console.log('[Permissions] Microphone status:', status);
if (status === 'granted') {
return { success: true, status: 'granted' };
}
// Req mic permission
const granted = await systemPreferences.askForMediaAccess('microphone');
return {
success: granted,
status: granted ? 'granted' : 'denied'
};
} catch (error) {
console.error('[Permissions] Error requesting microphone permission:', error);
return {
success: false,
error: error.message
};
}
});
ipcMain.handle('open-system-preferences', async (event, section) => {
if (process.platform !== 'darwin') {
return { success: false, error: 'Not supported on this platform' };
}
try {
if (section === 'screen-recording') {
// First trigger screen capture request to register the app in system preferences
try {
console.log('[Permissions] Triggering screen capture request to register app...');
await desktopCapturer.getSources({
types: ['screen'],
thumbnailSize: { width: 1, height: 1 }
});
console.log('[Permissions] App registered for screen recording');
} catch (captureError) {
console.log('[Permissions] Screen capture request triggered (expected to fail):', captureError.message);
}
// Then open system preferences
// await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture');
}
// if (section === 'microphone') {
// await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone');
// }
return { success: true };
} catch (error) {
console.error('[Permissions] Error opening system preferences:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('mark-permissions-completed', async () => {
try {
// This is a system-level setting, not user-specific.
await permissionRepository.markPermissionsAsCompleted();
console.log('[Permissions] Marked permissions as completed');
return { success: true };
} catch (error) {
console.error('[Permissions] Error marking permissions as completed:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('check-permissions-completed', async () => {
try {
const completed = await permissionRepository.checkPermissionsCompleted();
console.log('[Permissions] Permissions completed status:', completed);
return completed;
} catch (error) {
console.error('[Permissions] Error checking permissions completed status:', error);
return false;
}
});
ipcMain.handle('toggle-all-windows-visibility', () => toggleAllWindowsVisibility());
// ipcMain.handle('toggle-feature', async (event, featureName) => {
// return toggleFeature(featureName);
// });
ipcMain.on('animation-finished', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win && !win.isDestroyed()) { if (win && !win.isDestroyed()) {
console.log(`[WindowManager] Hiding window after animation.`); console.log(`[WindowManager] Hiding window after animation.`);
win.hide(); win.hide();
} }
}); };
const closeAskWindow = () => {
ipcMain.handle('ask:closeAskWindow', async () => {
const askWindow = windowPool.get('ask'); const askWindow = windowPool.get('ask');
if (askWindow) { if (askWindow) {
askWindow.webContents.send('window-hide-animation'); askWindow.webContents.send('window-hide-animation');
} }
}); };
async function ensureAskWindowVisible() {
if (currentHeaderState !== 'main') {
console.log('[WindowManager] Not in main state, skipping ensureAskWindowVisible');
return;
} }
// /**
// *
// * @param {'listen'|'ask'|'settings'} featureName
// * @param {{
// * listen?: { targetVisibility?: 'show'|'hide' },
// * ask?: { targetVisibility?: 'show'|'hide', questionText?: string },
// * settings?: { targetVisibility?: 'show'|'hide' }
// * }} [options={}]
// */
// async function toggleFeature(featureName, options = {}) {
// if (!windowPool.get(featureName) && currentHeaderState === 'main') {
// createFeatureWindows(windowPool.get('header'));
// }
// if (featureName === 'ask') {
// let askWindow = windowPool.get('ask');
// if (!askWindow || askWindow.isDestroyed()) {
// console.log('[WindowManager] Ask window not found, creating new one');
// return;
// }
// const questionText = options?.ask?.questionText ?? null;
// const targetVisibility = options?.ask?.targetVisibility ?? null;
// if (askWindow.isVisible()) {
// if (questionText) {
// askWindow.webContents.send('ask:sendQuestionToRenderer', questionText);
// } else {
// updateLayout();
// if (targetVisibility === 'show') {
// askWindow.webContents.send('ask:showTextInput');
// } else {
// askWindow.webContents.send('window-hide-animation');
// }
// }
// } else {
// console.log('[WindowManager] Showing hidden Ask window');
// askWindow.show();
// updateLayout();
// if (questionText) {
// askWindow.webContents.send('ask:sendQuestionToRenderer', questionText);
// }
// askWindow.webContents.send('window-show-animation');
// }
// }
// }
async function toggleFeature(featureName, options = {}) {
if (!windowPool.get(featureName) && currentHeaderState === 'main') {
createFeatureWindows(windowPool.get('header'));
}
if (featureName === 'ask') {
let askWindow = windowPool.get('ask'); let askWindow = windowPool.get('ask');
if (!askWindow || askWindow.isDestroyed()) { if (!askWindow || askWindow.isDestroyed()) {
console.log('[WindowManager] Ask window not found, creating new one'); console.log('[WindowManager] Ask window not found, creating new one');
return; createFeatureWindows(windowPool.get('header'), 'ask');
askWindow = windowPool.get('ask');
} }
if (askWindow.isVisible()) {
askWindow.webContents.send('ask:showTextInput'); if (!askWindow.isVisible()) {
} else {
console.log('[WindowManager] Showing hidden Ask window'); console.log('[WindowManager] Showing hidden Ask window');
askWindow.show(); askWindow.show();
updateLayout(); updateLayout();
askWindow.webContents.send('window-show-animation'); askWindow.webContents.send('window-show-animation');
} }
} }
}
//////// after_modelStateService //////// //////// after_modelStateService ////////
async function getStoredApiKey() {
if (global.modelStateService) {
const provider = await getStoredProvider();
return global.modelStateService.getApiKey(provider);
}
return null; // Fallback
}
async function getStoredProvider() {
if (global.modelStateService) {
return global.modelStateService.getCurrentProvider('llm');
}
return 'openai'; // Fallback
}
/**
*
* @param {IpcMainInvokeEvent} event
* @param {{type: 'llm' | 'stt'}}
*/
async function getCurrentModelInfo(event, { type }) {
if (global.modelStateService && (type === 'llm' || type === 'stt')) {
return global.modelStateService.getCurrentModelInfo(type);
}
return null;
}
function setupApiKeyIPC() {
const { ipcMain } = require('electron');
ipcMain.handle('get-stored-api-key', getStoredApiKey);
ipcMain.handle('get-ai-provider', getStoredProvider);
ipcMain.handle('get-current-model-info', getCurrentModelInfo);
ipcMain.handle('api-key-validated', async (event, data) => {
console.warn("[DEPRECATED] 'api-key-validated' IPC was called. This logic is now handled by 'model:validate-key'.");
return { success: true };
});
ipcMain.handle('remove-api-key', async () => {
console.warn("[DEPRECATED] 'remove-api-key' IPC was called. This is now handled by 'model:remove-api-key'.");
return { success: true };
});
console.log('[WindowManager] API key related IPC handlers have been updated for ModelStateService.');
}
//////// after_modelStateService ////////
const closeWindow = (windowName) => { const closeWindow = (windowName) => {
const win = windowPool.get(windowName); const win = windowPool.get(windowName);
@ -985,10 +741,6 @@ module.exports = {
createWindows, createWindows,
windowPool, windowPool,
fixedYPosition, fixedYPosition,
getStoredApiKey,
getStoredProvider,
getCurrentModelInfo,
toggleFeature, // Export toggleFeature so shortcutsService can use it
toggleContentProtection, toggleContentProtection,
resizeHeaderWindow, resizeHeaderWindow,
getContentProtectionStatus, getContentProtectionStatus,
@ -999,4 +751,14 @@ module.exports = {
openLoginPage, openLoginPage,
moveWindowStep, moveWindowStep,
closeWindow, closeWindow,
toggleAllWindowsVisibility,
handleHeaderStateChanged,
handleHeaderAnimationFinished,
getHeaderPosition,
moveHeader,
moveHeaderTo,
adjustWindowHeight,
handleAnimationFinished,
closeAskWindow,
ensureAskWindowVisible,
}; };