refactoring the bridge

This commit is contained in:
samtiz 2025-07-12 19:49:16 +09:00
parent 9f29fa5873
commit 1bdc5fd1bd
9 changed files with 312 additions and 503 deletions

2
aec

@ -1 +1 @@
Subproject commit 3be088c6cff8021c74eca714150e68e2cc74bee0
Subproject commit f00bb1fb948053c752b916adfee19f90644a0b2f

View File

@ -0,0 +1,71 @@
// src/bridge/featureBridge.js
const { ipcMain } = require('electron');
const settingsService = require('../features/settings/settingsService');
module.exports = {
// Renderer로부터의 요청을 수신
initialize() {
// 기존 ask 핸들러 유지
ipcMain.handle('feature:ask', (e, query) => {
// 실제로는 여기서 Controller -> Service 로직 수행
return `"${query}"에 대한 답변입니다.`;
});
// settings 관련 핸들러 추가
ipcMain.handle('settings:getSettings', async () => {
return await settingsService.getSettings();
});
ipcMain.handle('settings:saveSettings', async (event, settings) => {
return await settingsService.saveSettings(settings);
});
ipcMain.handle('settings:getPresets', async () => {
return await settingsService.getPresets();
});
ipcMain.handle('settings:getPresetTemplates', async () => {
return await settingsService.getPresetTemplates();
});
ipcMain.handle('settings:createPreset', async (event, title, prompt) => {
return await settingsService.createPreset(title, prompt);
});
ipcMain.handle('settings:updatePreset', async (event, id, title, prompt) => {
return await settingsService.updatePreset(id, title, prompt);
});
ipcMain.handle('settings:deletePreset', async (event, id) => {
return await settingsService.deletePreset(id);
});
ipcMain.handle('settings:saveApiKey', async (event, apiKey, provider) => {
return await settingsService.saveApiKey(apiKey, provider);
});
ipcMain.handle('settings:removeApiKey', async () => {
return await settingsService.removeApiKey();
});
ipcMain.handle('settings:updateContentProtection', async (event, enabled) => {
return await settingsService.updateContentProtection(enabled);
});
ipcMain.handle('settings:get-auto-update', async () => {
return await settingsService.getAutoUpdateSetting();
});
ipcMain.handle('settings:set-auto-update', async (event, isEnabled) => {
console.log('[SettingsService] Setting auto update setting:', isEnabled);
return await settingsService.setAutoUpdateSetting(isEnabled);
});
console.log('[FeatureBridge] Initialized with settings handlers.');
},
// Renderer로 상태를 전송
sendAskProgress(win, progress) {
win.webContents.send('feature:ask:progress', progress);
},
};

View File

@ -0,0 +1,10 @@
// src/bridge/internalBridge.js
const { EventEmitter } = require('events');
// FeatureCore와 WindowCore를 잇는 내부 이벤트 버스
module.exports = new EventEmitter();
// 예시 이벤트
internalBridge.on('content-protection-changed', (enabled) => {
// windowManager에서 처리
});

View File

@ -0,0 +1,74 @@
// src/bridge/windowBridge.js
const { ipcMain, BrowserWindow } = require('electron');
const { windowPool, settingsHideTimer, app, shell } = require('../electron/windowManager'); // 필요 변수 require
module.exports = {
// Renderer로부터의 요청을 수신
initialize() {
// 기존
ipcMain.on('window:hide', (e) => BrowserWindow.fromWebContents(e.sender)?.hide());
// windowManager 관련 추가
ipcMain.handle('toggle-content-protection', () => {
// windowManager의 toggle-content-protection 로직
isContentProtectionOn = !isContentProtectionOn;
windowPool.forEach(win => {
if (win && !win.isDestroyed()) {
win.setContentProtection(isContentProtectionOn);
}
});
return isContentProtectionOn;
});
ipcMain.handle('get-content-protection-status', () => {
return isContentProtectionOn;
});
ipcMain.handle('open-shortcut-editor', () => {
// open-shortcut-editor 로직 (windowPool 등 필요시 require)
const header = windowPool.get('header');
if (!header) return;
globalShortcut.unregisterAll();
createFeatureWindows(header, 'shortcut-settings');
});
// 다른 관련 핸들러 추가 (quit-application, etc.)
ipcMain.handle('quit-application', () => {
app.quit();
});
// 추가: 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);
// 위치 조정 로직 (기존 복사)
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);
}
});
// 추가: hide-settings-window 등 다른 핸들러 복사
// ... (hide-settings-window, cancel-hide-settings-window, quit-application, open-login-page, firebase-logout, move-window-step 등)
// 예: ipcMain.handle('open-login-page', () => { shell.openExternal(...); });
},
// Renderer로 상태를 전송
notifyFocusChange(win, isFocused) {
win.webContents.send('window:focus-change', isFocused);
},
};

View File

@ -76,6 +76,7 @@ function updateLayout() {
}
let movementManager = null;
const windowBridge = require('../bridge/windowBridge');
async function toggleFeature(featureName) {

View File

@ -543,11 +543,9 @@ export class SettingsView extends LitElement {
}
async loadAutoUpdateSetting() {
if (!window.require) return;
const { ipcRenderer } = window.require('electron');
this.autoUpdateLoading = true;
try {
const enabled = await ipcRenderer.invoke('settings:get-auto-update');
const enabled = await window.api.feature.settings.getAutoUpdate();
this.autoUpdateEnabled = enabled;
console.log('Auto-update setting loaded:', enabled);
} catch (e) {
@ -559,13 +557,12 @@ export class SettingsView extends LitElement {
}
async handleToggleAutoUpdate() {
if (!window.require || this.autoUpdateLoading) return;
const { ipcRenderer } = window.require('electron');
if (this.autoUpdateLoading) return;
this.autoUpdateLoading = true;
this.requestUpdate();
try {
const newValue = !this.autoUpdateEnabled;
const result = await ipcRenderer.invoke('settings:set-auto-update', newValue);
const result = await window.api.feature.settings.setAutoUpdate(newValue);
if (result && result.success) {
this.autoUpdateEnabled = newValue;
} else {
@ -580,22 +577,20 @@ export class SettingsView extends LitElement {
//////// after_modelStateService ////////
async loadInitialData() {
if (!window.require) return;
this.isLoading = true;
const { ipcRenderer } = window.require('electron');
try {
const [userState, config, storedKeys, availableLlm, availableStt, selectedModels, presets, contentProtection, shortcuts, ollamaStatus, whisperModelsResult] = await Promise.all([
ipcRenderer.invoke('get-current-user'),
ipcRenderer.invoke('model:get-provider-config'), // Provider 설정 로드
ipcRenderer.invoke('model:get-all-keys'),
ipcRenderer.invoke('model:get-available-models', { type: 'llm' }),
ipcRenderer.invoke('model:get-available-models', { type: 'stt' }),
ipcRenderer.invoke('model:get-selected-models'),
ipcRenderer.invoke('settings:getPresets'),
ipcRenderer.invoke('get-content-protection-status'),
ipcRenderer.invoke('get-current-shortcuts'),
ipcRenderer.invoke('ollama:get-status'),
ipcRenderer.invoke('whisper:get-installed-models')
window.api.feature.settings.getCurrentUser(),
window.api.feature.settings.getProviderConfig(), // Provider 설정 로드
window.api.feature.settings.getAllKeys(),
window.api.feature.settings.getAvailableModels({ type: 'llm' }),
window.api.feature.settings.getAvailableModels({ type: 'stt' }),
window.api.feature.settings.getSelectedModels(),
window.api.feature.settings.getPresets(),
window.api.feature.settings.getContentProtectionStatus(),
window.api.feature.settings.getCurrentShortcuts(),
window.api.feature.settings.getOllamaStatus(),
window.api.feature.settings.getWhisperInstalledModels()
]);
if (userState && userState.isLoggedIn) this.firebaseUser = userState;
@ -644,10 +639,9 @@ export class SettingsView extends LitElement {
// For Ollama, we need to ensure it's ready first
if (provider === 'ollama') {
this.saving = true;
const { ipcRenderer } = window.require('electron');
// First ensure Ollama is installed and running
const ensureResult = await ipcRenderer.invoke('ollama:ensure-ready');
const ensureResult = await window.api.feature.settings.ensureOllamaReady();
if (!ensureResult.success) {
alert(`Failed to setup Ollama: ${ensureResult.error}`);
this.saving = false;
@ -655,7 +649,7 @@ export class SettingsView extends LitElement {
}
// Now validate (which will check if service is running)
const result = await ipcRenderer.invoke('model:validate-key', { provider, key: 'local' });
const result = await window.api.feature.settings.validateKey({ provider, key: 'local' });
if (result.success) {
this.apiKeys = { ...this.apiKeys, [provider]: 'local' };
@ -671,8 +665,7 @@ export class SettingsView extends LitElement {
// For Whisper, just enable it
if (provider === 'whisper') {
this.saving = true;
const { ipcRenderer } = window.require('electron');
const result = await ipcRenderer.invoke('model:validate-key', { provider, key: 'local' });
const result = await window.api.feature.settings.validateKey({ provider, key: 'local' });
if (result.success) {
this.apiKeys = { ...this.apiKeys, [provider]: 'local' };
@ -686,8 +679,7 @@ export class SettingsView extends LitElement {
// For other providers, use the normal flow
this.saving = true;
const { ipcRenderer } = window.require('electron');
const result = await ipcRenderer.invoke('model:validate-key', { provider, key });
const result = await window.api.feature.settings.validateKey({ provider, key });
if (result.success) {
this.apiKeys = { ...this.apiKeys, [provider]: key };
@ -701,20 +693,18 @@ export class SettingsView extends LitElement {
async handleClearKey(provider) {
this.saving = true;
const { ipcRenderer } = window.require('electron');
await ipcRenderer.invoke('model:remove-api-key', { provider });
await window.api.feature.settings.removeApiKey({ provider });
this.apiKeys = { ...this.apiKeys, [provider]: '' };
await this.refreshModelData();
this.saving = false;
}
async refreshModelData() {
const { ipcRenderer } = window.require('electron');
const [availableLlm, availableStt, selected, storedKeys] = await Promise.all([
ipcRenderer.invoke('model:get-available-models', { type: 'llm' }),
ipcRenderer.invoke('model:get-available-models', { type: 'stt' }),
ipcRenderer.invoke('model:get-selected-models'),
ipcRenderer.invoke('model:get-all-keys')
window.api.feature.settings.getAvailableModels({ type: 'llm' }),
window.api.feature.settings.getAvailableModels({ type: 'stt' }),
window.api.feature.settings.getSelectedModels(),
window.api.feature.settings.getAllKeys()
]);
this.availableLlmModels = availableLlm;
this.availableSttModels = availableStt;
@ -765,8 +755,7 @@ export class SettingsView extends LitElement {
}
this.saving = true;
const { ipcRenderer } = window.require('electron');
await ipcRenderer.invoke('model:set-selected-model', { type, modelId });
await window.api.feature.settings.setSelectedModel({ type, modelId });
if (type === 'llm') this.selectedLlm = modelId;
if (type === 'stt') this.selectedStt = modelId;
this.isLlmListVisible = false;
@ -776,8 +765,7 @@ export class SettingsView extends LitElement {
}
async refreshOllamaStatus() {
const { ipcRenderer } = window.require('electron');
const ollamaStatus = await ipcRenderer.invoke('ollama:get-status');
const ollamaStatus = await window.api.feature.settings.getOllamaStatus();
if (ollamaStatus?.success) {
this.ollamaStatus = { installed: ollamaStatus.installed, running: ollamaStatus.running };
this.ollamaModels = ollamaStatus.models || [];
@ -821,8 +809,6 @@ export class SettingsView extends LitElement {
this.requestUpdate();
try {
const { ipcRenderer } = window.require('electron');
// Set up progress listener
const progressHandler = (event, { modelId: id, progress }) => {
if (id === modelId) {
@ -831,10 +817,10 @@ export class SettingsView extends LitElement {
}
};
ipcRenderer.on('whisper:download-progress', progressHandler);
window.api.feature.settings.onWhisperDownloadProgress(progressHandler);
// Start download
const result = await ipcRenderer.invoke('whisper:download-model', modelId);
const result = await window.api.feature.settings.downloadWhisperModel(modelId);
if (result.success) {
// Auto-select the model after download
@ -844,7 +830,7 @@ export class SettingsView extends LitElement {
}
// Cleanup
ipcRenderer.removeListener('whisper:download-progress', progressHandler);
window.api.feature.settings.removeOnWhisperDownloadProgress(progressHandler);
} catch (error) {
console.error(`[SettingsView] Error downloading Whisper model ${modelId}:`, error);
alert(`Error downloading ${modelId}: ${error.message}`);
@ -876,17 +862,12 @@ export class SettingsView extends LitElement {
if (this.wasJustDragged) return
console.log("Requesting Firebase authentication from main process...")
if (window.require) {
window.require("electron").ipcRenderer.invoke("start-firebase-auth")
}
}
window.api.feature.settings.startFirebaseAuth();
}
//////// after_modelStateService ////////
openShortcutEditor() {
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.invoke('open-shortcut-editor');
}
window.api.feature.settings.openShortcutEditor();
}
connectedCallback() {
@ -924,10 +905,6 @@ export class SettingsView extends LitElement {
}
setupIpcListeners() {
if (!window.require) return;
const { ipcRenderer } = window.require('electron');
this._userStateListener = (event, userState) => {
console.log('[SettingsView] Received user-state-changed:', userState);
if (userState && userState.isLoggedIn) {
@ -949,7 +926,7 @@ export class SettingsView extends LitElement {
this._presetsUpdatedListener = async (event) => {
console.log('[SettingsView] Received presets-updated, refreshing presets');
try {
const presets = await ipcRenderer.invoke('settings:getPresets');
const presets = await window.api.feature.settings.getPresets();
this.presets = presets || [];
// 현재 선택된 프리셋이 삭제되었는지 확인 (사용자 프리셋만 고려)
@ -968,28 +945,24 @@ export class SettingsView extends LitElement {
this.shortcuts = keybinds;
};
ipcRenderer.on('user-state-changed', this._userStateListener);
ipcRenderer.on('settings-updated', this._settingsUpdatedListener);
ipcRenderer.on('presets-updated', this._presetsUpdatedListener);
ipcRenderer.on('shortcuts-updated', this._shortcutListener);
window.api.feature.settings.onUserStateChanged(this._userStateListener);
window.api.feature.settings.onSettingsUpdated(this._settingsUpdatedListener);
window.api.feature.settings.onPresetsUpdated(this._presetsUpdatedListener);
window.api.feature.settings.onShortcutsUpdated(this._shortcutListener);
}
cleanupIpcListeners() {
if (!window.require) return;
const { ipcRenderer } = window.require('electron');
if (this._userStateListener) {
ipcRenderer.removeListener('user-state-changed', this._userStateListener);
window.api.feature.settings.removeOnUserStateChanged(this._userStateListener);
}
if (this._settingsUpdatedListener) {
ipcRenderer.removeListener('settings-updated', this._settingsUpdatedListener);
window.api.feature.settings.removeOnSettingsUpdated(this._settingsUpdatedListener);
}
if (this._presetsUpdatedListener) {
ipcRenderer.removeListener('presets-updated', this._presetsUpdatedListener);
window.api.feature.settings.removeOnPresetsUpdated(this._presetsUpdatedListener);
}
if (this._shortcutListener) {
ipcRenderer.removeListener('shortcuts-updated', this._shortcutListener);
window.api.feature.settings.removeOnShortcutsUpdated(this._shortcutListener);
}
}
@ -1023,17 +996,11 @@ export class SettingsView extends LitElement {
}
handleMouseEnter = () => {
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.send('cancel-hide-settings-window');
}
window.api.window.cancelHideSettingsWindow();
}
handleMouseLeave = () => {
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.send('hide-settings-window');
}
window.api.window.hideSettingsWindow();
}
// getMainShortcuts() {
@ -1083,39 +1050,27 @@ export class SettingsView extends LitElement {
handleMoveLeft() {
console.log('Move Left clicked');
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.invoke('move-window-step', 'left');
}
window.api.feature.settings.moveWindowStep('left');
}
handleMoveRight() {
console.log('Move Right clicked');
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.invoke('move-window-step', 'right');
}
window.api.feature.settings.moveWindowStep('right');
}
async handlePersonalize() {
console.log('Personalize clicked');
if (window.require) {
const { ipcRenderer } = window.require('electron');
try {
await ipcRenderer.invoke('open-login-page');
} catch (error) {
console.error('Failed to open personalize page:', error);
}
try {
await window.api.window.openLoginPage();
} catch (error) {
console.error('Failed to open personalize page:', error);
}
}
async handleToggleInvisibility() {
console.log('Toggle Invisibility clicked');
if (window.require) {
const { ipcRenderer } = window.require('electron');
this.isContentProtectionOn = await ipcRenderer.invoke('toggle-content-protection');
this.requestUpdate();
}
this.isContentProtectionOn = await window.api.window.toggleContentProtection();
this.requestUpdate();
}
async handleSaveApiKey() {
@ -1123,62 +1078,46 @@ export class SettingsView extends LitElement {
if (!input || !input.value) return;
const newApiKey = input.value;
if (window.require) {
const { ipcRenderer } = window.require('electron');
try {
const result = await ipcRenderer.invoke('settings:saveApiKey', newApiKey);
if (result.success) {
console.log('API Key saved successfully via IPC.');
this.apiKey = newApiKey;
this.requestUpdate();
} else {
console.error('Failed to save API Key via IPC:', result.error);
}
} catch(e) {
console.error('Error invoking save-api-key IPC:', e);
try {
const result = await window.api.feature.settings.saveApiKey(newApiKey);
if (result.success) {
console.log('API Key saved successfully via IPC.');
this.apiKey = newApiKey;
this.requestUpdate();
} else {
console.error('Failed to save API Key via IPC:', result.error);
}
} catch(e) {
console.error('Error invoking save-api-key IPC:', e);
}
}
async handleClearApiKey() {
console.log('Clear API Key clicked');
if (window.require) {
const { ipcRenderer } = window.require('electron');
await ipcRenderer.invoke('settings:removeApiKey');
this.apiKey = null;
this.requestUpdate();
}
await window.api.feature.settings.removeApiKey();
this.apiKey = null;
this.requestUpdate();
}
handleQuit() {
console.log('Quit clicked');
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.invoke('quit-application');
}
window.api.window.quitApplication();
}
handleFirebaseLogout() {
console.log('Firebase Logout clicked');
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.invoke('firebase-logout');
}
window.api.window.firebaseLogout();
}
async handleOllamaShutdown() {
console.log('[SettingsView] Shutting down Ollama service...');
if (!window.require) return;
const { ipcRenderer } = window.require('electron');
try {
// Show loading state
this.ollamaStatus = { ...this.ollamaStatus, running: false };
this.requestUpdate();
const result = await ipcRenderer.invoke('ollama:shutdown', false); // Graceful shutdown
const result = await window.api.feature.settings.shutdownOllama(false); // Graceful shutdown
if (result.success) {
console.log('[SettingsView] Ollama shut down successfully');
@ -1330,329 +1269,6 @@ export class SettingsView extends LitElement {
//////// before_modelStateService ////////
//////// after_modelStateService ////////
render() {
if (this.isLoading) {
return html`
<div class="settings-container">
<div class="loading-state">
<div class="loading-spinner"></div>
<span>Loading...</span>
</div>
</div>
`;
}
const loggedIn = !!this.firebaseUser;
const apiKeyManagementHTML = html`
<div class="api-key-section">
${Object.entries(this.providerConfig)
.filter(([id, config]) => !id.includes('-glass'))
.map(([id, config]) => {
if (id === 'ollama') {
// Special UI for Ollama
return html`
<div class="provider-key-group">
<label>${config.name} (Local)</label>
${this.ollamaStatus.installed && this.ollamaStatus.running ? html`
<div style="padding: 8px; background: rgba(0,255,0,0.1); border-radius: 4px; font-size: 11px; color: rgba(0,255,0,0.8);">
Ollama is running
</div>
<button class="settings-button full-width danger" @click=${this.handleOllamaShutdown}>
Stop Ollama Service
</button>
` : this.ollamaStatus.installed ? html`
<div style="padding: 8px; background: rgba(255,200,0,0.1); border-radius: 4px; font-size: 11px; color: rgba(255,200,0,0.8);">
Ollama installed but not running
</div>
<button class="settings-button full-width" @click=${() => this.handleSaveKey(id)}>
Start Ollama
</button>
` : html`
<div style="padding: 8px; background: rgba(255,100,100,0.1); border-radius: 4px; font-size: 11px; color: rgba(255,100,100,0.8);">
Ollama not installed
</div>
<button class="settings-button full-width" @click=${() => this.handleSaveKey(id)}>
Install & Setup Ollama
</button>
`}
</div>
`;
}
if (id === 'whisper') {
// Special UI for Whisper with model selection
const whisperModels = config.sttModels || [];
const selectedWhisperModel = this.selectedStt && this.getProviderForModel('stt', this.selectedStt) === 'whisper'
? this.selectedStt
: null;
return html`
<div class="provider-key-group">
<label>${config.name} (Local STT)</label>
${this.apiKeys[id] === 'local' ? html`
<div style="padding: 8px; background: rgba(0,255,0,0.1); border-radius: 4px; font-size: 11px; color: rgba(0,255,0,0.8); margin-bottom: 8px;">
Whisper is enabled
</div>
<!-- Whisper Model Selection Dropdown -->
<label style="font-size: 10px; margin-top: 8px;">Select Model:</label>
<select
class="model-dropdown"
style="width: 100%; padding: 6px; background: rgba(0,0,0,0.2); border: 1px solid rgba(255,255,255,0.2); color: white; border-radius: 4px; font-size: 11px; margin-bottom: 8px;"
@change=${(e) => this.handleWhisperModelSelect(e.target.value)}
.value=${selectedWhisperModel || ''}
>
<option value="">Choose a model...</option>
${whisperModels.map(model => {
const isInstalling = this.installingModels[model.id] !== undefined;
const progress = this.installingModels[model.id] || 0;
let statusText = '';
if (isInstalling) {
statusText = ` (Downloading ${progress}%)`;
} else if (model.installed) {
statusText = ' (Installed)';
}
return html`
<option value="${model.id}" ?disabled=${isInstalling}>
${model.name}${statusText}
</option>
`;
})}
</select>
${Object.entries(this.installingModels).map(([modelId, progress]) => {
if (modelId.startsWith('whisper-') && progress !== undefined) {
return html`
<div style="margin: 8px 0;">
<div style="font-size: 10px; color: rgba(255,255,255,0.7); margin-bottom: 4px;">
Downloading ${modelId}...
</div>
<div class="install-progress" style="height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; overflow: hidden;">
<div class="install-progress-bar" style="height: 100%; background: rgba(0, 122, 255, 0.8); width: ${progress}%; transition: width 0.3s ease;"></div>
</div>
</div>
`;
}
return null;
})}
<button class="settings-button full-width danger" @click=${() => this.handleClearKey(id)}>
Disable Whisper
</button>
` : html`
<button class="settings-button full-width" @click=${() => this.handleSaveKey(id)}>
Enable Whisper STT
</button>
`}
</div>
`;
}
// Regular providers
return html`
<div class="provider-key-group">
<label for="key-input-${id}">${config.name} API Key</label>
<input type="password" id="key-input-${id}"
placeholder=${loggedIn ? "Using Pickle's Key" : `Enter ${config.name} API Key`}
.value=${this.apiKeys[id] || ''}
>
<div class="key-buttons">
<button class="settings-button" @click=${() => this.handleSaveKey(id)} >Save</button>
<button class="settings-button danger" @click=${() => this.handleClearKey(id)} }>Clear</button>
</div>
</div>
`;
})}
</div>
`;
const getModelName = (type, id) => {
const models = type === 'llm' ? this.availableLlmModels : this.availableSttModels;
const model = models.find(m => m.id === id);
return model ? model.name : id;
}
const modelSelectionHTML = html`
<div class="model-selection-section">
<div class="model-select-group">
<label>LLM Model: <strong>${getModelName('llm', this.selectedLlm) || 'Not Set'}</strong></label>
<button class="settings-button full-width" @click=${() => this.toggleModelList('llm')} ?disabled=${this.saving || this.availableLlmModels.length === 0}>
Change LLM Model
</button>
${this.isLlmListVisible ? html`
<div class="model-list">
${this.availableLlmModels.map(model => {
const isOllama = this.getProviderForModel('llm', model.id) === 'ollama';
const ollamaModel = isOllama ? this.ollamaModels.find(m => m.name === model.id) : null;
const isInstalling = this.installingModels[model.id] !== undefined;
const installProgress = this.installingModels[model.id] || 0;
return html`
<div class="model-item ${this.selectedLlm === model.id ? 'selected' : ''}"
@click=${() => this.selectModel('llm', model.id)}>
<span>${model.name}</span>
${isOllama ? html`
${isInstalling ? html`
<div class="install-progress">
<div class="install-progress-bar" style="width: ${installProgress}%"></div>
</div>
` : ollamaModel?.installed ? html`
<span class="model-status installed"> Installed</span>
` : html`
<span class="model-status not-installed">Click to install</span>
`}
` : ''}
</div>
`;
})}
</div>
` : ''}
</div>
<div class="model-select-group">
<label>STT Model: <strong>${getModelName('stt', this.selectedStt) || 'Not Set'}</strong></label>
<button class="settings-button full-width" @click=${() => this.toggleModelList('stt')} ?disabled=${this.saving || this.availableSttModels.length === 0}>
Change STT Model
</button>
${this.isSttListVisible ? html`
<div class="model-list">
${this.availableSttModels.map(model => {
const isWhisper = this.getProviderForModel('stt', model.id) === 'whisper';
const isInstalling = this.installingModels[model.id] !== undefined;
const installProgress = this.installingModels[model.id] || 0;
return html`
<div class="model-item ${this.selectedStt === model.id ? 'selected' : ''}"
@click=${() => this.selectModel('stt', model.id)}>
<span>${model.name}</span>
${isWhisper && isInstalling ? html`
<div class="install-progress">
<div class="install-progress-bar" style="width: ${installProgress}%"></div>
</div>
` : ''}
</div>
`;
})}
</div>
` : ''}
</div>
</div>
`;
return html`
<div class="settings-container">
<div class="header-section">
<div>
<h1 class="app-title">Pickle Glass</h1>
<div class="account-info">
${this.firebaseUser
? html`Account: ${this.firebaseUser.email || 'Logged In'}`
: `Account: Not Logged In`
}
</div>
</div>
<div class="invisibility-icon ${this.isContentProtectionOn ? 'visible' : ''}" title="Invisibility is On">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.785 7.41787C8.7 7.41787 7.79 8.19371 7.55667 9.22621C7.0025 8.98704 6.495 9.05121 6.11 9.22037C5.87083 8.18204 4.96083 7.41787 3.88167 7.41787C2.61583 7.41787 1.58333 8.46204 1.58333 9.75121C1.58333 11.0404 2.61583 12.0845 3.88167 12.0845C5.08333 12.0845 6.06333 11.1395 6.15667 9.93787C6.355 9.79787 6.87417 9.53537 7.51 9.94954C7.615 11.1454 8.58333 12.0845 9.785 12.0845C11.0508 12.0845 12.0833 11.0404 12.0833 9.75121C12.0833 8.46204 11.0508 7.41787 9.785 7.41787ZM3.88167 11.4195C2.97167 11.4195 2.2425 10.6729 2.2425 9.75121C2.2425 8.82954 2.9775 8.08287 3.88167 8.08287C4.79167 8.08287 5.52083 8.82954 5.52083 9.75121C5.52083 10.6729 4.79167 11.4195 3.88167 11.4195ZM9.785 11.4195C8.875 11.4195 8.14583 10.6729 8.14583 9.75121C8.14583 8.82954 8.875 8.08287 9.785 8.08287C10.695 8.08287 11.43 8.82954 11.43 9.75121C11.43 10.6729 10.6892 11.4195 9.785 11.4195ZM12.6667 5.95954H1V6.83454H12.6667V5.95954ZM8.8925 1.36871C8.76417 1.08287 8.4375 0.931207 8.12833 1.03037L6.83333 1.46204L5.5325 1.03037L5.50333 1.02454C5.19417 0.93704 4.8675 1.10037 4.75083 1.39787L3.33333 5.08454H10.3333L8.91 1.39787L8.8925 1.36871Z" fill="white"/>
</svg>
</div>
</div>
${apiKeyManagementHTML}
${modelSelectionHTML}
<div class="buttons-section" style="border-top: 1px solid rgba(255, 255, 255, 0.1); padding-top: 6px; margin-top: 6px;">
<button class="settings-button full-width" @click=${this.openShortcutEditor}>
Edit Shortcuts
</button>
</div>
<div class="shortcuts-section">
${this.getMainShortcuts().map(shortcut => html`
<div class="shortcut-item">
<span class="shortcut-name">${shortcut.name}</span>
<div class="shortcut-keys">
${this.renderShortcutKeys(shortcut.accelerator)}
</div>
</div>
`)}
</div>
<div class="preset-section">
<div class="preset-header">
<span class="preset-title">
My Presets
<span class="preset-count">(${this.presets.filter(p => p.is_default === 0).length})</span>
</span>
<span class="preset-toggle" @click=${this.togglePresets}>
${this.showPresets ? '▼' : '▶'}
</span>
</div>
<div class="preset-list ${this.showPresets ? '' : 'hidden'}">
${this.presets.filter(p => p.is_default === 0).length === 0 ? html`
<div class="no-presets-message">
No custom presets yet.<br>
<span class="web-link" @click=${this.handlePersonalize}>
Create your first preset
</span>
</div>
` : this.presets.filter(p => p.is_default === 0).map(preset => html`
<div class="preset-item ${this.selectedPreset?.id === preset.id ? 'selected' : ''}"
@click=${() => this.handlePresetSelect(preset)}>
<span class="preset-name">${preset.title}</span>
${this.selectedPreset?.id === preset.id ? html`<span class="preset-status">Selected</span>` : ''}
</div>
`)}
</div>
</div>
<div class="buttons-section">
<button class="settings-button full-width" @click=${this.handlePersonalize}>
<span>Personalize / Meeting Notes</span>
</button>
<button class="settings-button full-width" @click=${this.handleToggleAutoUpdate} ?disabled=${this.autoUpdateLoading}>
<span>Automatic Updates: ${this.autoUpdateEnabled ? 'On' : 'Off'}</span>
</button>
<div class="move-buttons">
<button class="settings-button half-width" @click=${this.handleMoveLeft}>
<span> Move</span>
</button>
<button class="settings-button half-width" @click=${this.handleMoveRight}>
<span>Move </span>
</button>
</div>
<button class="settings-button full-width" @click=${this.handleToggleInvisibility}>
<span>${this.isContentProtectionOn ? 'Disable Invisibility' : 'Enable Invisibility'}</span>
</button>
<div class="bottom-buttons">
${this.firebaseUser
? html`
<button class="settings-button half-width danger" @click=${this.handleFirebaseLogout}>
<span>Logout</span>
</button>
`
: html`
<button class="settings-button half-width" @click=${this.handleUsePicklesKey}>
<span>Login</span>
</button>
`
}
<button class="settings-button half-width danger" @click=${this.handleQuit}>
<span>Quit</span>
</button>
</div>
</div>
</div>
`;
}
//////// after_modelStateService ////////
}
customElements.define('settings-view', SettingsView);

View File

@ -373,56 +373,7 @@ function initialize() {
// cleanup
windowNotificationManager.cleanup();
// IPC handlers for settings
ipcMain.handle('settings:getSettings', async () => {
return await getSettings();
});
ipcMain.handle('settings:saveSettings', async (event, settings) => {
return await saveSettings(settings);
});
// IPC handlers for presets
ipcMain.handle('settings:getPresets', async () => {
return await getPresets();
});
ipcMain.handle('settings:getPresetTemplates', async () => {
return await getPresetTemplates();
});
ipcMain.handle('settings:createPreset', async (event, title, prompt) => {
return await createPreset(title, prompt);
});
ipcMain.handle('settings:updatePreset', async (event, id, title, prompt) => {
return await updatePreset(id, title, prompt);
});
ipcMain.handle('settings:deletePreset', async (event, id) => {
return await deletePreset(id);
});
ipcMain.handle('settings:saveApiKey', async (event, apiKey, provider) => {
return await saveApiKey(apiKey, provider);
});
ipcMain.handle('settings:removeApiKey', async () => {
return await removeApiKey();
});
ipcMain.handle('settings:updateContentProtection', async (event, enabled) => {
return await updateContentProtection(enabled);
});
ipcMain.handle('settings:get-auto-update', async () => {
return await getAutoUpdateSetting();
});
ipcMain.handle('settings:set-auto-update', async (event, isEnabled) => {
console.log('[SettingsService] Setting auto update setting:', isEnabled);
return await setAutoUpdateSetting(isEnabled);
});
// IPC handlers 제거 (featureBridge로 이동)
console.log('[SettingsService] Initialized and ready.');
}

View File

@ -27,6 +27,7 @@ const settingsService = require('./features/settings/settingsService');
const sessionRepository = require('./common/repositories/session');
const ModelStateService = require('./common/services/modelStateService');
const sqliteClient = require('./common/services/sqliteClient');
const featureBridge = require('./bridge/featureBridge');
// Global variables
const eventBridge = new EventEmitter();
@ -205,6 +206,7 @@ app.whenReady().then(async () => {
listenService.setupIpcHandlers();
askService.initialize();
settingsService.initialize();
featureBridge.initialize(); // 추가: featureBridge 초기화
setupGeneralIpcHandlers();
setupOllamaIpcHandlers();
setupWhisperIpcHandlers();

View File

@ -1,2 +1,86 @@
// See the Electron documentation for details on how to use preload scripts:
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
// src/preload.js
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'),
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)
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')
}
},
// 기존 window 유지
window: {
// 기존
hide: () => ipcRenderer.send('window:hide'),
onFocusChange: (callback) => ipcRenderer.on('window:focus-change', (e, f) => callback(f)),
// 추가
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),
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)
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')
}
});