From 64111891eb8dd2d44659894c665373f80e99b24a Mon Sep 17 00:00:00 2001 From: jhyang0 Date: Mon, 7 Jul 2025 05:42:05 +0900 Subject: [PATCH 1/3] settings big update --- src/app/PickleGlassApp.js | 21 +- src/electron/windowManager.js | 8 +- src/features/customize/CustomizeView.js | 1097 ----------------- src/features/settings/SettingsView.js | 809 ++++++++++++ src/features/settings/repositories/index.js | 23 + .../repositories/sqlite.repository.js | 225 ++++ src/features/settings/settingsService.js | 331 +++++ src/index.js | 20 + 8 files changed, 1419 insertions(+), 1115 deletions(-) delete mode 100644 src/features/customize/CustomizeView.js create mode 100644 src/features/settings/SettingsView.js create mode 100644 src/features/settings/repositories/index.js create mode 100644 src/features/settings/repositories/sqlite.repository.js create mode 100644 src/features/settings/settingsService.js diff --git a/src/app/PickleGlassApp.js b/src/app/PickleGlassApp.js index eb639eb..9662df4 100644 --- a/src/app/PickleGlassApp.js +++ b/src/app/PickleGlassApp.js @@ -1,7 +1,6 @@ import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js'; -import { CustomizeView } from '../features/customize/CustomizeView.js'; +import { SettingsView } from '../features/settings/SettingsView.js'; import { AssistantView } from '../features/listen/AssistantView.js'; -import { OnboardingView } from '../features/onboarding/OnboardingView.js'; import { AskView } from '../features/ask/AskView.js'; import '../features/listen/renderer.js'; @@ -21,7 +20,7 @@ export class PickleGlassApp extends LitElement { width: 100%; } - ask-view, customize-view, history-view, help-view, onboarding-view, setup-view { + ask-view, settings-view, history-view, help-view, setup-view { display: block; width: 100%; } @@ -180,8 +179,8 @@ export class PickleGlassApp extends LitElement { this.isMainViewVisible = !this.isMainViewVisible; } - handleCustomizeClick() { - this.currentView = 'customize'; + handleSettingsClick() { + this.currentView = 'settings'; this.isMainViewVisible = true; } @@ -247,10 +246,6 @@ export class PickleGlassApp extends LitElement { this.currentResponseIndex = e.detail.index; } - handleOnboardingComplete() { - this.currentView = 'main'; - } - render() { switch (this.currentView) { case 'listen': @@ -263,19 +258,17 @@ export class PickleGlassApp extends LitElement { >`; case 'ask': return html``; - case 'customize': - return html` (this.selectedProfile = profile)} .onLanguageChange=${lang => (this.selectedLanguage = lang)} - >`; + >`; case 'history': return html``; case 'help': return html``; - case 'onboarding': - return html``; case 'setup': return html``; default: diff --git a/src/electron/windowManager.js b/src/electron/windowManager.js index 586e3f8..c9b08c4 100644 --- a/src/electron/windowManager.js +++ b/src/electron/windowManager.js @@ -102,8 +102,8 @@ function createFeatureWindows(header) { const settings = new BrowserWindow({ ...commonChildOptions, width:240, height:450, parent:undefined }); settings.setContentProtection(isContentProtectionOn); settings.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); - settings.loadFile(path.join(__dirname,'../app/content.html'),{query:{view:'customize'}}) - .catch(console.error); + settings.loadFile(path.join(__dirname,'../app/content.html'),{query:{view:'settings'}}) + .catch(console.error); windowPool.set('settings', settings); } @@ -1950,7 +1950,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, openaiSessi globalShortcut.unregisterAll(); if (movementManager) { - movementManager.destroy(); + movementManager.destroy();nd } movementManager = new SmoothMovementManager(); @@ -2162,7 +2162,7 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, openaiSessionRef) { if (isMainViewVisible) { const viewHeights = { listen: 400, - customize: 600, + settings: 600, help: 550, history: 550, setup: 200, diff --git a/src/features/customize/CustomizeView.js b/src/features/customize/CustomizeView.js deleted file mode 100644 index b3c6dba..0000000 --- a/src/features/customize/CustomizeView.js +++ /dev/null @@ -1,1097 +0,0 @@ -import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js'; - -export class CustomizeView extends LitElement { - static styles = css` - * { - font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - cursor: default; - user-select: none; - } - - :host { - display: block; - width: 180px; - min-height: 180px; - color: white; - } - - .settings-container { - display: flex; - flex-direction: column; - height: 100%; - width: 100%; - background: rgba(20, 20, 20, 0.8); - border-radius: 12px; - outline: 0.5px rgba(255, 255, 255, 0.2) solid; - outline-offset: -1px; - box-sizing: border-box; - position: relative; - overflow-y: auto; - padding: 12px 12px; - z-index: 1000; - } - - .settings-container::-webkit-scrollbar { - width: 6px; - } - - .settings-container::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.05); - border-radius: 3px; - } - - .settings-container::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.2); - border-radius: 3px; - } - - .settings-container::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.3); - } - - .settings-container::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.15); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); - border-radius: 12px; - filter: blur(10px); - z-index: -1; - } - - .settings-button[disabled], - .api-key-section input[disabled] { - opacity: 0.4; - cursor: not-allowed; - pointer-events: none; - } - - .header-section { - display: flex; - justify-content: space-between; - align-items: flex-start; - padding-bottom: 6px; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - position: relative; - z-index: 1; - } - - .title-line { - display: flex; - justify-content: space-between; - align-items: center; - } - - .app-title { - font-size: 13px; - font-weight: 500; - color: white; - margin: 0 0 4px 0; - } - - .account-info { - font-size: 11px; - color: rgba(255, 255, 255, 0.7); - margin: 0; - } - - .invisibility-icon { - padding-top: 2px; - opacity: 0; - transition: opacity 0.3s ease; - } - - .invisibility-icon.visible { - opacity: 1; - } - - .invisibility-icon svg { - width: 16px; - height: 16px; - } - - .shortcuts-section { - display: flex; - flex-direction: column; - gap: 2px; - padding: 4px 0; - position: relative; - z-index: 1; - } - - .shortcut-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 4px 0; - color: white; - font-size: 11px; - } - - .shortcut-name { - font-weight: 300; - } - - .shortcut-keys { - display: flex; - align-items: center; - gap: 3px; - } - - .cmd-key, .shortcut-key { - background: rgba(255, 255, 255, 0.1); - // border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 3px; - width: 16px; - height: 16px; - display: flex; - align-items: center; - justify-content: center; - font-size: 11px; - font-weight: 500; - color: rgba(255, 255, 255, 0.9); - } - - /* Buttons Section */ - .buttons-section { - display: flex; - flex-direction: column; - gap: 4px; - padding-top: 6px; - border-top: 1px solid rgba(255, 255, 255, 0.1); - position: relative; - z-index: 1; - flex: 1; - } - - .settings-button { - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 4px; - color: white; - padding: 5px 10px; - font-size: 11px; - font-weight: 400; - cursor: pointer; - transition: all 0.15s ease; - display: flex; - align-items: center; - justify-content: center; - white-space: nowrap; - } - - .settings-button:hover { - background: rgba(255, 255, 255, 0.15); - border-color: rgba(255, 255, 255, 0.3); - } - - .settings-button:active { - transform: translateY(1px); - } - - .settings-button.full-width { - width: 100%; - } - - .settings-button.half-width { - flex: 1; - } - - .settings-button.danger { - background: rgba(255, 59, 48, 0.1); - border-color: rgba(255, 59, 48, 0.3); - color: rgba(255, 59, 48, 0.9); - } - - .settings-button.danger:hover { - background: rgba(255, 59, 48, 0.15); - border-color: rgba(255, 59, 48, 0.4); - } - - .move-buttons, .bottom-buttons { - display: flex; - gap: 4px; - } - - .api-key-section { - padding: 6px 0; - border-top: 1px solid rgba(255, 255, 255, 0.1); - } - - .api-key-section input { - width: 100%; - background: rgba(0,0,0,0.2); - border: 1px solid rgba(255,255,255,0.2); - color: white; - border-radius: 4px; - padding: 4px; - font-size: 11px; - margin-bottom: 4px; - } - - `; - - static properties = { - selectedProfile: { type: String }, - selectedLanguage: { type: String }, - selectedScreenshotInterval: { type: String }, - selectedImageQuality: { type: String }, - layoutMode: { type: String }, - keybinds: { type: Object }, - throttleTokens: { type: Number }, - maxTokens: { type: Number }, - throttlePercent: { type: Number }, - googleSearchEnabled: { type: Boolean }, - backgroundTransparency: { type: Number }, - fontSize: { type: Number }, - onProfileChange: { type: Function }, - onLanguageChange: { type: Function }, - onScreenshotIntervalChange: { type: Function }, - onImageQualityChange: { type: Function }, - onLayoutModeChange: { type: Function }, - contentProtection: { type: Boolean }, - userPresets: { type: Array }, - presetTemplates: { type: Array }, - currentUser: { type: String }, - isContentProtectionOn: { type: Boolean }, - firebaseUser: { type: Object, state: true }, - apiKey: { type: String, state: true }, - isLoading: { type: Boolean }, - activeTab: { type: String }, - }; - - constructor() { - super(); - - this.selectedProfile = localStorage.getItem('selectedProfile') || 'school'; - - // Language format migration for legacy users - let lang = localStorage.getItem('selectedLanguage') || 'en'; - if (lang.includes('-')) { - const newLang = lang.split('-')[0]; - console.warn(`[Migration] Correcting language format from "${lang}" to "${newLang}".`); - localStorage.setItem('selectedLanguage', newLang); - lang = newLang; - } - this.selectedLanguage = lang; - - this.selectedScreenshotInterval = localStorage.getItem('selectedScreenshotInterval') || '5000'; - this.selectedImageQuality = localStorage.getItem('selectedImageQuality') || '0.8'; - this.layoutMode = localStorage.getItem('layoutMode') || 'stacked'; - this.keybinds = this.getDefaultKeybinds(); - this.throttleTokens = 500; - this.maxTokens = 2000; - this.throttlePercent = 80; - this.backgroundTransparency = 0.5; - this.fontSize = 14; - this.userPresets = []; - this.presetTemplates = []; - this.currentUser = 'default_user'; - this.firebaseUser = null; - this.apiKey = null; - this.isContentProtectionOn = true; - this.isLoading = false; - this.activeTab = 'prompts'; - - this.loadKeybinds(); - this.loadRateLimitSettings(); - this.loadGoogleSearchSettings(); - this.loadBackgroundTransparency(); - this.loadFontSize(); - this.loadContentProtectionSettings(); - this.checkContentProtectionStatus(); - this.getApiKeyFromStorage(); - } - - connectedCallback() { - super.connectedCallback(); - - this.loadLayoutMode(); - this.loadInitialData(); - - this.resizeHandler = () => { - this.requestUpdate(); - this.updateScrollHeight(); - }; - window.addEventListener('resize', this.resizeHandler); - - setTimeout(() => this.updateScrollHeight(), 100); - - this.addEventListener('mouseenter', () => { - if (window.require) { - window.require('electron').ipcRenderer.send('cancel-hide-window', 'settings'); - } - }); - - this.addEventListener('mouseleave', () => { - if (window.require) { - window.require('electron').ipcRenderer.send('hide-window', 'settings'); - } - }); - - if (window.require) { - const { ipcRenderer } = window.require('electron'); - - this._userStateListener = (event, userState) => { - console.log('[CustomizeView] Received user-state-changed:', userState); - if (userState && userState.isLoggedIn) { - this.firebaseUser = userState; - } else { - this.firebaseUser = null; - } - this.getApiKeyFromStorage(); // Also update API key display - this.requestUpdate(); - }; - ipcRenderer.on('user-state-changed', this._userStateListener); - - ipcRenderer.on('api-key-validated', (event, newApiKey) => { - console.log('[CustomizeView] Received api-key-validated, updating state.'); - this.apiKey = newApiKey; - this.requestUpdate(); - }); - - ipcRenderer.on('api-key-updated', () => { - console.log('[CustomizeView] Received api-key-updated, refreshing state.'); - this.getApiKeyFromStorage(); - }); - - ipcRenderer.on('api-key-removed', () => { - console.log('[CustomizeView] Received api-key-removed, clearing state.'); - this.apiKey = null; - this.requestUpdate(); - }); - - this.loadInitialUser(); - } - } - - disconnectedCallback() { - super.disconnectedCallback(); - if (this.resizeHandler) { - window.removeEventListener('resize', this.resizeHandler); - } - - if (window.require) { - const { ipcRenderer } = window.require('electron'); - if (this._userStateListener) { - ipcRenderer.removeListener('user-state-changed', this._userStateListener); - } - ipcRenderer.removeAllListeners('api-key-validated'); - ipcRenderer.removeAllListeners('api-key-updated'); - ipcRenderer.removeAllListeners('api-key-removed'); - } - } - - updateScrollHeight() { - const windowHeight = window.innerHeight; - const headerHeight = 60; - const padding = 40; - const maxHeight = windowHeight - headerHeight - padding; - - this.style.maxHeight = `${maxHeight}px`; - } - - async checkContentProtectionStatus() { - if (window.require) { - const { ipcRenderer } = window.require('electron'); - this.isContentProtectionOn = await ipcRenderer.invoke('get-content-protection-status'); - this.requestUpdate(); - } - } - - getProfiles() { - if (this.presetTemplates && this.presetTemplates.length > 0) { - return this.presetTemplates.map(t => ({ - value: t.id || t._id, - name: t.title, - description: t.prompt?.slice(0, 60) + '...', - })); - } - - return [ - { value: 'school', name: 'School', description: '' }, - { value: 'meetings', name: 'Meetings', description: '' }, - { value: 'sales', name: 'Sales', description: '' }, - { value: 'recruiting', name: 'Recruiting', description: '' }, - { value: 'customer-support', name: 'Customer Support', description: '' }, - ]; - } - - getLanguages() { - return [ - { value: 'en', name: 'English' }, - { value: 'de', name: 'German' }, - { value: 'es', name: 'Spanish' }, - { value: 'fr', name: 'French' }, - { value: 'hi', name: 'Hindi' }, - { value: 'pt', name: 'Portuguese' }, - { value: 'ar', name: 'Arabic' }, - { value: 'id', name: 'Indonesian' }, - { value: 'it', name: 'Italian' }, - { value: 'ja', name: 'Japanese' }, - { value: 'tr', name: 'Turkish' }, - { value: 'vi', name: 'Vietnamese' }, - { value: 'bn', name: 'Bengali' }, - { value: 'gu', name: 'Gujarati' }, - { value: 'kn', name: 'Kannada' }, - { value: 'ml', name: 'Malayalam' }, - { value: 'mr', name: 'Marathi' }, - { value: 'ta', name: 'Tamil' }, - { value: 'te', name: 'Telugu' }, - { value: 'nl', name: 'Dutch' }, - { value: 'ko', name: 'Korean' }, - { value: 'zh', name: 'Chinese' }, - { value: 'pl', name: 'Polish' }, - { value: 'ru', name: 'Russian' }, - { value: 'th', name: 'Thai' }, - ]; - } - - getProfileNames() { - return { - interview: 'Job Interview', - sales: 'Sales Call', - meeting: 'Business Meeting', - presentation: 'Presentation', - negotiation: 'Negotiation', - }; - } - - handleProfileSelect(e) { - this.selectedProfile = e.target.value; - localStorage.setItem('selectedProfile', this.selectedProfile); - this.onProfileChange(this.selectedProfile); - } - - handleLanguageSelect(e) { - this.selectedLanguage = e.target.value; - localStorage.setItem('selectedLanguage', this.selectedLanguage); - this.onLanguageChange(this.selectedLanguage); - } - - handleScreenshotIntervalSelect(e) { - this.selectedScreenshotInterval = e.target.value; - localStorage.setItem('selectedScreenshotInterval', this.selectedScreenshotInterval); - this.onScreenshotIntervalChange(this.selectedScreenshotInterval); - } - - handleImageQualitySelect(e) { - this.selectedImageQuality = e.target.value; - this.onImageQualityChange(e.target.value); - } - - handleLayoutModeSelect(e) { - this.layoutMode = e.target.value; - localStorage.setItem('layoutMode', this.layoutMode); - this.onLayoutModeChange(e.target.value); - } - - getUserCustomPrompt() { - console.log('[CustomizeView] getUserCustomPrompt called'); - console.log('[CustomizeView] userPresets:', this.userPresets); - console.log('[CustomizeView] selectedProfile:', this.selectedProfile); - - if (!this.userPresets || this.userPresets.length === 0) { - console.log('[CustomizeView] No presets - returning loading message'); - return 'Loading personalized prompt... Please set it in the web.'; - } - - let preset = this.userPresets.find(p => p.id === 'personalized' || p._id === 'personalized'); - console.log('[CustomizeView] personalized preset:', preset); - - if (!preset) { - preset = this.userPresets.find(p => p.id === this.selectedProfile || p._id === this.selectedProfile); - console.log('[CustomizeView] selectedProfile preset:', preset); - } - - if (!preset) { - preset = this.userPresets[0]; - console.log('[CustomizeView] Using first preset:', preset); - } - - const result = preset?.prompt || 'No personalized prompt set.'; - console.log('[CustomizeView] Final returned prompt:', result); - return result; - } - - async loadInitialData() { - if (window.require) { - const { ipcRenderer } = window.require('electron'); - try { - this.isLoading = true; - this.userPresets = await ipcRenderer.invoke('get-user-presets'); - this.presetTemplates = await ipcRenderer.invoke('get-preset-templates'); - console.log('[CustomizeView] Loaded presets and templates via IPC'); - } catch (error) { - console.error('[CustomizeView] Failed to load data via IPC:', error); - } finally { - this.isLoading = false; - } - } else { - console.log('[CustomizeView] IPC not available'); - } - } - - getDefaultKeybinds() { - const isMac = window.pickleGlass?.isMacOS || navigator.platform.includes('Mac'); - return { - moveUp: isMac ? 'Cmd+Up' : 'Ctrl+Up', - moveDown: isMac ? 'Cmd+Down' : 'Ctrl+Down', - moveLeft: isMac ? 'Cmd+Left' : 'Ctrl+Left', - moveRight: isMac ? 'Cmd+Right' : 'Ctrl+Right', - toggleVisibility: isMac ? 'Cmd+\\' : 'Ctrl+\\', - toggleClickThrough: isMac ? 'Cmd+M' : 'Ctrl+M', - nextStep: isMac ? 'Cmd+Enter' : 'Ctrl+Enter', - manualScreenshot: isMac ? 'Cmd+Shift+S' : 'Ctrl+Shift+S', - previousResponse: isMac ? 'Cmd+[' : 'Ctrl+[', - nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]', - scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up', - scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down', - }; - } - - loadKeybinds() { - const savedKeybinds = localStorage.getItem('customKeybinds'); - if (savedKeybinds) { - try { - this.keybinds = { ...this.getDefaultKeybinds(), ...JSON.parse(savedKeybinds) }; - } catch (e) { - console.error('Failed to parse saved keybinds:', e); - this.keybinds = this.getDefaultKeybinds(); - } - } - } - - saveKeybinds() { - localStorage.setItem('customKeybinds', JSON.stringify(this.keybinds)); - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.send('update-keybinds', this.keybinds); - } - } - - handleKeybindChange(action, value) { - this.keybinds = { ...this.keybinds, [action]: value }; - this.saveKeybinds(); - this.requestUpdate(); - } - - resetKeybinds() { - this.keybinds = this.getDefaultKeybinds(); - localStorage.removeItem('customKeybinds'); - this.requestUpdate(); - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.send('update-keybinds', this.keybinds); - } - } - - getKeybindActions() { - return [ - { - key: 'moveUp', - name: 'Move Window Up', - description: 'Move the application window up', - }, - { - key: 'moveDown', - name: 'Move Window Down', - description: 'Move the application window down', - }, - { - key: 'moveLeft', - name: 'Move Window Left', - description: 'Move the application window left', - }, - { - key: 'moveRight', - name: 'Move Window Right', - description: 'Move the application window right', - }, - { - key: 'toggleVisibility', - name: 'Toggle Window Visibility', - description: 'Show/hide the application window', - }, - { - key: 'toggleClickThrough', - name: 'Toggle Click-through Mode', - description: 'Enable/disable click-through functionality', - }, - { - key: 'nextStep', - name: 'Ask Next Step', - description: 'Ask AI for the next step suggestion', - }, - { - key: 'manualScreenshot', - name: 'Manual Screenshot', - description: 'Take a manual screenshot for AI analysis', - }, - { - key: 'previousResponse', - name: 'Previous Response', - description: 'Navigate to the previous AI response', - }, - { - key: 'nextResponse', - name: 'Next Response', - description: 'Navigate to the next AI response', - }, - { - key: 'scrollUp', - name: 'Scroll Response Up', - description: 'Scroll the AI response content up', - }, - { - key: 'scrollDown', - name: 'Scroll Response Down', - description: 'Scroll the AI response content down', - }, - ]; - } - - handleKeybindFocus(e) { - e.target.placeholder = 'Press key combination...'; - e.target.select(); - } - - handleKeybindInput(e) { - e.preventDefault(); - - const modifiers = []; - const keys = []; - - if (e.ctrlKey) modifiers.push('Ctrl'); - if (e.metaKey) modifiers.push('Cmd'); - if (e.altKey) modifiers.push('Alt'); - if (e.shiftKey) modifiers.push('Shift'); - - let mainKey = e.key; - - switch (e.code) { - case 'ArrowUp': - mainKey = 'Up'; - break; - case 'ArrowDown': - mainKey = 'Down'; - break; - case 'ArrowLeft': - mainKey = 'Left'; - break; - case 'ArrowRight': - mainKey = 'Right'; - break; - case 'Enter': - mainKey = 'Enter'; - break; - case 'Space': - mainKey = 'Space'; - break; - case 'Backslash': - mainKey = '\\'; - break; - case 'KeyS': - if (e.shiftKey) mainKey = 'S'; - break; - case 'KeyM': - mainKey = 'M'; - break; - default: - if (e.key.length === 1) { - mainKey = e.key.toUpperCase(); - } - break; - } - - if (['Control', 'Meta', 'Alt', 'Shift'].includes(e.key)) { - return; - } - - const keybind = [...modifiers, mainKey].join('+'); - - const action = e.target.dataset.action; - - this.handleKeybindChange(action, keybind); - - e.target.value = keybind; - e.target.blur(); - } - - loadRateLimitSettings() { - const throttleTokens = localStorage.getItem('throttleTokens'); - const maxTokens = localStorage.getItem('maxTokens'); - const throttlePercent = localStorage.getItem('throttlePercent'); - - if (throttleTokens !== null) { - this.throttleTokens = parseInt(throttleTokens, 10) || 500; - } - if (maxTokens !== null) { - this.maxTokens = parseInt(maxTokens, 10) || 2000; - } - if (throttlePercent !== null) { - this.throttlePercent = parseInt(throttlePercent, 10) || 80; - } - } - - handleThrottleTokensChange(e) { - this.throttleTokens = parseInt(e.target.value, 10); - localStorage.setItem('throttleTokens', this.throttleTokens.toString()); - this.requestUpdate(); - } - - handleMaxTokensChange(e) { - const value = parseInt(e.target.value, 10); - if (!isNaN(value) && value > 0) { - this.maxTokens = value; - localStorage.setItem('maxTokens', this.maxTokens.toString()); - } - } - - handleThrottlePercentChange(e) { - const value = parseInt(e.target.value, 10); - if (!isNaN(value) && value >= 0 && value <= 100) { - this.throttlePercent = value; - localStorage.setItem('throttlePercent', this.throttlePercent.toString()); - } - } - - resetRateLimitSettings() { - this.throttleTokens = 500; - this.maxTokens = 2000; - this.throttlePercent = 80; - - localStorage.removeItem('throttleTokens'); - localStorage.removeItem('maxTokens'); - localStorage.removeItem('throttlePercent'); - - this.requestUpdate(); - } - - loadGoogleSearchSettings() { - const googleSearchEnabled = localStorage.getItem('googleSearchEnabled'); - if (googleSearchEnabled !== null) { - this.googleSearchEnabled = googleSearchEnabled === 'true'; - } - } - - async handleGoogleSearchChange(e) { - this.googleSearchEnabled = e.target.checked; - localStorage.setItem('googleSearchEnabled', this.googleSearchEnabled.toString()); - - if (window.require) { - try { - const { ipcRenderer } = window.require('electron'); - await ipcRenderer.invoke('update-google-search-setting', this.googleSearchEnabled); - } catch (error) { - console.error('Failed to notify main process:', error); - } - } - - this.requestUpdate(); - } - - loadLayoutMode() { - const savedLayoutMode = localStorage.getItem('layoutMode'); - if (savedLayoutMode) { - this.layoutMode = savedLayoutMode; - } - } - - loadBackgroundTransparency() { - const backgroundTransparency = localStorage.getItem('backgroundTransparency'); - if (backgroundTransparency !== null) { - this.backgroundTransparency = parseFloat(backgroundTransparency) || 0.5; - } - this.updateBackgroundTransparency(); - } - - handleBackgroundTransparencyChange(e) { - this.backgroundTransparency = parseFloat(e.target.value); - localStorage.setItem('backgroundTransparency', this.backgroundTransparency.toString()); - this.updateBackgroundTransparency(); - this.requestUpdate(); - } - - updateBackgroundTransparency() { - const root = document.documentElement; - root.style.setProperty('--header-background', `rgba(0, 0, 0, ${this.backgroundTransparency})`); - root.style.setProperty('--main-content-background', `rgba(0, 0, 0, ${this.backgroundTransparency})`); - root.style.setProperty('--card-background', `rgba(255, 255, 255, ${this.backgroundTransparency * 0.05})`); - root.style.setProperty('--input-background', `rgba(0, 0, 0, ${this.backgroundTransparency * 0.375})`); - root.style.setProperty('--input-focus-background', `rgba(0, 0, 0, ${this.backgroundTransparency * 0.625})`); - root.style.setProperty('--button-background', `rgba(0, 0, 0, ${this.backgroundTransparency * 0.625})`); - root.style.setProperty('--preview-video-background', `rgba(0, 0, 0, ${this.backgroundTransparency * 1.125})`); - root.style.setProperty('--screen-option-background', `rgba(0, 0, 0, ${this.backgroundTransparency * 0.5})`); - root.style.setProperty('--screen-option-hover-background', `rgba(0, 0, 0, ${this.backgroundTransparency * 0.75})`); - root.style.setProperty('--scrollbar-background', `rgba(0, 0, 0, ${this.backgroundTransparency * 0.5})`); - } - - loadFontSize() { - const fontSize = localStorage.getItem('fontSize'); - if (fontSize !== null) { - this.fontSize = parseInt(fontSize, 10) || 14; - } - this.updateFontSize(); - } - - handleFontSizeChange(e) { - this.fontSize = parseInt(e.target.value, 10); - localStorage.setItem('fontSize', this.fontSize.toString()); - this.updateFontSize(); - this.requestUpdate(); - } - - updateFontSize() { - const root = document.documentElement; - root.style.setProperty('--response-font-size', `${this.fontSize}px`); - } - - loadContentProtectionSettings() { - const contentProtection = localStorage.getItem('contentProtection'); - if (contentProtection !== null) { - this.contentProtection = contentProtection === 'true'; - } - } - - async handleContentProtectionChange(e) { - this.contentProtection = e.target.checked; - localStorage.setItem('contentProtection', this.contentProtection.toString()); - - if (window.require) { - try { - const { ipcRenderer } = window.require('electron'); - await ipcRenderer.invoke('update-content-protection', this.contentProtection); - } catch (error) { - console.error('Failed to notify main process about content protection change:', error); - } - } - - this.requestUpdate(); - } - - render() { - const loggedIn = !!this.firebaseUser; - console.log('[CustomizeView] render: Rendering component template.'); - return html` -
-
-
-

Pickle Glass

- -
-
- - - -
-
- -
- - -
- -
- ${this.getMainShortcuts().map(shortcut => html` -
- ${shortcut.name} -
- - ${shortcut.key} -
-
- `)} -
- -
- - -
- - -
- - - -
- ${this.firebaseUser - ? html` - - ` - : html` - - ` - } - -
-
-
- `; - } - - getMainShortcuts() { - return [ - { name: 'Show / Hide', key: '\\' }, - { name: 'Ask Anything', key: '↵' }, - { name: 'Scroll AI Response', key: '↕' } - ]; - } - - handleMoveLeft() { - console.log('Move Left clicked'); - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.invoke('move-window-step', 'left'); - } - } - - handleMoveRight() { - console.log('Move Right clicked'); - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.invoke('move-window-step', 'right'); - } - } - - async handlePersonalize() { - console.log('Personalize clicked'); - if (window.require) { - const { ipcRenderer, shell } = window.require('electron'); - try { - const webUrl = await ipcRenderer.invoke('get-web-url'); - shell.openExternal(`${webUrl}/personalize`); - } catch (error) { - console.error('Failed to get web URL or open external link:', error); - shell.openExternal('http://localhost:3000/personalize'); - } - } - } - - 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(); - } - } - - async handleSaveApiKey() { - const input = this.shadowRoot.getElementById('api-key-input'); - if (!input || !input.value) return; - - const newApiKey = input.value; - if (window.require) { - const { ipcRenderer } = window.require('electron'); - try { - const result = await ipcRenderer.invoke('save-api-key', 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('remove-api-key'); - this.requestUpdate(); - } - } - - handleQuit() { - console.log('Quit clicked'); - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.invoke('quit-application'); - } - } - - handleFirebaseLogout() { - console.log('Firebase Logout clicked'); - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.invoke('firebase-logout'); - } - } - - async loadInitialUser() { - if (!window.require) return; - const { ipcRenderer } = window.require('electron'); - try { - console.log('[CustomizeView] Loading initial user state...'); - const userState = await ipcRenderer.invoke('get-current-user'); - if (userState && userState.isLoggedIn) { - this.firebaseUser = userState; - } else { - this.firebaseUser = null; - } - this.requestUpdate(); - } catch (error) { - console.error('[CustomizeView] Failed to load initial user:', error); - this.firebaseUser = null; - this.requestUpdate(); - } - } - - getApiKeyFromStorage() { - if (window.require) { - const { ipcRenderer } = window.require('electron'); - ipcRenderer.invoke('get-stored-api-key').then(key => { - this.apiKey = key; - this.requestUpdate(); - }).catch(error => { - console.log('[CustomizeView] Failed to get API key:', error); - this.apiKey = null; - }); - } - return null; - } -} - -customElements.define('customize-view', CustomizeView); diff --git a/src/features/settings/SettingsView.js b/src/features/settings/SettingsView.js new file mode 100644 index 0000000..ba21dbb --- /dev/null +++ b/src/features/settings/SettingsView.js @@ -0,0 +1,809 @@ +import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js'; + +export class SettingsView extends LitElement { + static styles = css` + * { + font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + cursor: default; + user-select: none; + } + + :host { + display: block; + width: 180px; + min-height: 180px; + color: white; + } + + .settings-container { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + background: rgba(20, 20, 20, 0.8); + border-radius: 12px; + outline: 0.5px rgba(255, 255, 255, 0.2) solid; + outline-offset: -1px; + box-sizing: border-box; + position: relative; + overflow-y: auto; + padding: 12px 12px; + z-index: 1000; + } + + .settings-container::-webkit-scrollbar { + width: 6px; + } + + .settings-container::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 3px; + } + + .settings-container::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; + } + + .settings-container::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); + } + + .settings-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.15); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + border-radius: 12px; + filter: blur(10px); + z-index: -1; + } + + .settings-button[disabled], + .api-key-section input[disabled] { + opacity: 0.4; + cursor: not-allowed; + pointer-events: none; + } + + .header-section { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding-bottom: 6px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + position: relative; + z-index: 1; + } + + .title-line { + display: flex; + justify-content: space-between; + align-items: center; + } + + .app-title { + font-size: 13px; + font-weight: 500; + color: white; + margin: 0 0 4px 0; + } + + .account-info { + font-size: 11px; + color: rgba(255, 255, 255, 0.7); + margin: 0; + } + + .invisibility-icon { + padding-top: 2px; + opacity: 0; + transition: opacity 0.3s ease; + } + + .invisibility-icon.visible { + opacity: 1; + } + + .invisibility-icon svg { + width: 16px; + height: 16px; + } + + .shortcuts-section { + display: flex; + flex-direction: column; + gap: 2px; + padding: 4px 0; + position: relative; + z-index: 1; + } + + .shortcut-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 0; + color: white; + font-size: 11px; + } + + .shortcut-name { + font-weight: 300; + } + + .shortcut-keys { + display: flex; + align-items: center; + gap: 3px; + } + + .cmd-key, .shortcut-key { + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 500; + color: rgba(255, 255, 255, 0.9); + } + + /* Buttons Section */ + .buttons-section { + display: flex; + flex-direction: column; + gap: 4px; + padding-top: 6px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + position: relative; + z-index: 1; + flex: 1; + } + + .settings-button { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 4px; + color: white; + padding: 5px 10px; + font-size: 11px; + font-weight: 400; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + align-items: center; + justify-content: center; + white-space: nowrap; + } + + .settings-button:hover { + background: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.3); + } + + .settings-button:active { + transform: translateY(1px); + } + + .settings-button.full-width { + width: 100%; + } + + .settings-button.half-width { + flex: 1; + } + + .settings-button.danger { + background: rgba(255, 59, 48, 0.1); + border-color: rgba(255, 59, 48, 0.3); + color: rgba(255, 59, 48, 0.9); + } + + .settings-button.danger:hover { + background: rgba(255, 59, 48, 0.15); + border-color: rgba(255, 59, 48, 0.4); + } + + .move-buttons, .bottom-buttons { + display: flex; + gap: 4px; + } + + .api-key-section { + padding: 6px 0; + border-top: 1px solid rgba(255, 255, 255, 0.1); + } + + .api-key-section input { + width: 100%; + background: rgba(0,0,0,0.2); + border: 1px solid rgba(255,255,255,0.2); + color: white; + border-radius: 4px; + padding: 4px; + font-size: 11px; + margin-bottom: 4px; + box-sizing: border-box; + } + + .api-key-section input::placeholder { + color: rgba(255, 255, 255, 0.4); + } + + /* Preset Management Section */ + .preset-section { + padding: 6px 0; + border-top: 1px solid rgba(255, 255, 255, 0.1); + } + + .preset-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; + } + + .preset-title { + font-size: 11px; + font-weight: 500; + color: white; + } + + .preset-count { + font-size: 9px; + color: rgba(255, 255, 255, 0.5); + margin-left: 4px; + } + + .preset-toggle { + font-size: 10px; + color: rgba(255, 255, 255, 0.6); + cursor: pointer; + padding: 2px 4px; + border-radius: 2px; + transition: background-color 0.15s ease; + } + + .preset-toggle:hover { + background: rgba(255, 255, 255, 0.1); + } + + .preset-list { + display: flex; + flex-direction: column; + gap: 2px; + max-height: 120px; + overflow-y: auto; + } + + .preset-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 6px; + background: rgba(255, 255, 255, 0.05); + border-radius: 3px; + cursor: pointer; + transition: all 0.15s ease; + font-size: 11px; + border: 1px solid transparent; + } + + .preset-item:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.1); + } + + .preset-item.selected { + background: rgba(0, 122, 255, 0.25); + border-color: rgba(0, 122, 255, 0.6); + box-shadow: 0 0 0 1px rgba(0, 122, 255, 0.3); + } + + .preset-name { + color: white; + flex: 1; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + font-weight: 300; + } + + .preset-item.selected .preset-name { + font-weight: 500; + } + + .preset-status { + font-size: 9px; + color: rgba(0, 122, 255, 0.8); + font-weight: 500; + margin-left: 6px; + } + + .no-presets-message { + padding: 12px 8px; + text-align: center; + color: rgba(255, 255, 255, 0.5); + font-size: 10px; + line-height: 1.4; + } + + .no-presets-message .web-link { + color: rgba(0, 122, 255, 0.8); + text-decoration: underline; + cursor: pointer; + } + + .no-presets-message .web-link:hover { + color: rgba(0, 122, 255, 1); + } + + .loading-state { + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + color: rgba(255, 255, 255, 0.7); + font-size: 11px; + } + + .loading-spinner { + width: 12px; + height: 12px; + border: 1px solid rgba(255, 255, 255, 0.2); + border-top: 1px solid rgba(255, 255, 255, 0.8); + border-radius: 50%; + animation: spin 1s linear infinite; + margin-right: 6px; + } + + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + .hidden { + display: none; + } + `; + + static properties = { + firebaseUser: { type: Object, state: true }, + apiKey: { type: String, state: true }, + isLoading: { type: Boolean, state: true }, + isContentProtectionOn: { type: Boolean, state: true }, + settings: { type: Object, state: true }, + presets: { type: Array, state: true }, + selectedPreset: { type: Object, state: true }, + showPresets: { type: Boolean, state: true }, + saving: { type: Boolean, state: true }, + }; + + constructor() { + super(); + this.firebaseUser = null; + this.apiKey = null; + this.isLoading = false; + this.isContentProtectionOn = true; + this.settings = null; + this.presets = []; + this.selectedPreset = null; + this.showPresets = false; + this.saving = false; + this.loadInitialData(); + } + + async loadInitialData() { + if (!window.require) return; + + try { + this.isLoading = true; + const { ipcRenderer } = window.require('electron'); + + // Load all data in parallel + const [settings, presets, apiKey, contentProtection, userState] = await Promise.all([ + ipcRenderer.invoke('settings:getSettings'), + ipcRenderer.invoke('settings:getPresets'), + ipcRenderer.invoke('get-stored-api-key'), + ipcRenderer.invoke('get-content-protection-status'), + ipcRenderer.invoke('get-current-user') + ]); + + this.settings = settings; + this.presets = presets || []; + this.apiKey = apiKey; + this.isContentProtectionOn = contentProtection; + + // Set first user preset as selected + if (this.presets.length > 0) { + const firstUserPreset = this.presets.find(p => p.is_default === 0); + if (firstUserPreset) { + this.selectedPreset = firstUserPreset; + } + } + + if (userState && userState.isLoggedIn) { + this.firebaseUser = userState.user; + } + } catch (error) { + console.error('Error loading initial data:', error); + } finally { + this.isLoading = false; + } + } + + connectedCallback() { + super.connectedCallback(); + + this.setupEventListeners(); + this.setupIpcListeners(); + this.setupWindowResize(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.cleanupEventListeners(); + this.cleanupIpcListeners(); + this.cleanupWindowResize(); + } + + setupEventListeners() { + this.addEventListener('mouseenter', this.handleMouseEnter); + this.addEventListener('mouseleave', this.handleMouseLeave); + } + + cleanupEventListeners() { + this.removeEventListener('mouseenter', this.handleMouseEnter); + this.removeEventListener('mouseleave', this.handleMouseLeave); + } + + 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) { + this.firebaseUser = userState; + } else { + this.firebaseUser = null; + } + this.requestUpdate(); + }; + + this._settingsUpdatedListener = (event, settings) => { + console.log('[SettingsView] Received settings-updated'); + this.settings = settings; + this.requestUpdate(); + }; + + // 프리셋 업데이트 리스너 추가 + this._presetsUpdatedListener = async (event) => { + console.log('[SettingsView] Received presets-updated, refreshing presets'); + try { + const presets = await ipcRenderer.invoke('settings:getPresets'); + this.presets = presets || []; + + // 현재 선택된 프리셋이 삭제되었는지 확인 (사용자 프리셋만 고려) + const userPresets = this.presets.filter(p => p.is_default === 0); + if (this.selectedPreset && !userPresets.find(p => p.id === this.selectedPreset.id)) { + this.selectedPreset = userPresets.length > 0 ? userPresets[0] : null; + } + + this.requestUpdate(); + } catch (error) { + console.error('[SettingsView] Failed to refresh presets:', error); + } + }; + + ipcRenderer.on('user-state-changed', this._userStateListener); + ipcRenderer.on('settings-updated', this._settingsUpdatedListener); + ipcRenderer.on('presets-updated', this._presetsUpdatedListener); + } + + cleanupIpcListeners() { + if (!window.require) return; + + const { ipcRenderer } = window.require('electron'); + + if (this._userStateListener) { + ipcRenderer.removeListener('user-state-changed', this._userStateListener); + } + if (this._settingsUpdatedListener) { + ipcRenderer.removeListener('settings-updated', this._settingsUpdatedListener); + } + if (this._presetsUpdatedListener) { + ipcRenderer.removeListener('presets-updated', this._presetsUpdatedListener); + } + } + + setupWindowResize() { + this.resizeHandler = () => { + this.requestUpdate(); + this.updateScrollHeight(); + }; + window.addEventListener('resize', this.resizeHandler); + + // Initial setup + setTimeout(() => this.updateScrollHeight(), 100); + } + + cleanupWindowResize() { + if (this.resizeHandler) { + window.removeEventListener('resize', this.resizeHandler); + } + } + + updateScrollHeight() { + const windowHeight = window.innerHeight; + const headerHeight = 40; + const padding = 20; + const maxHeight = windowHeight - headerHeight - padding; + + this.style.maxHeight = `${maxHeight}px`; + + const container = this.shadowRoot?.querySelector('.settings-container'); + if (container) { + container.style.maxHeight = `${maxHeight - 20}px`; + } + } + + handleMouseEnter = () => { + if (window.require) { + const { ipcRenderer } = window.require('electron'); + ipcRenderer.send('cancel-hide-window', 'settings'); + } + } + + handleMouseLeave = () => { + if (window.require) { + const { ipcRenderer } = window.require('electron'); + ipcRenderer.send('hide-window', 'settings'); + } + } + + getMainShortcuts() { + return [ + { name: 'Show / Hide', key: '\\' }, + { name: 'Ask Anything', key: '↵' }, + { name: 'Scroll AI Response', key: '↕' } + ]; + } + + togglePresets() { + this.showPresets = !this.showPresets; + } + + async handlePresetSelect(preset) { + this.selectedPreset = preset; + // Here you could implement preset application logic + console.log('Selected preset:', preset); + } + + handleMoveLeft() { + console.log('Move Left clicked'); + if (window.require) { + const { ipcRenderer } = window.require('electron'); + ipcRenderer.invoke('move-window-step', 'left'); + } + } + + handleMoveRight() { + console.log('Move Right clicked'); + if (window.require) { + const { ipcRenderer } = window.require('electron'); + ipcRenderer.invoke('move-window-step', '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); + } + } + } + + 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(); + } + } + + async handleSaveApiKey() { + const input = this.shadowRoot.getElementById('api-key-input'); + 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); + } + } + } + + 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(); + } + } + + handleQuit() { + console.log('Quit clicked'); + if (window.require) { + const { ipcRenderer } = window.require('electron'); + ipcRenderer.invoke('quit-application'); + } + } + + handleFirebaseLogout() { + console.log('Firebase Logout clicked'); + if (window.require) { + const { ipcRenderer } = window.require('electron'); + ipcRenderer.invoke('firebase-logout'); + } + } + + render() { + if (this.isLoading) { + return html` +
+
+
+ Loading... +
+
+ `; + } + + const loggedIn = !!this.firebaseUser; + + return html` +
+
+
+

Pickle Glass

+ +
+
+ + + +
+
+ +
+ + +
+ +
+ ${this.getMainShortcuts().map(shortcut => html` +
+ ${shortcut.name} +
+ + ${shortcut.key} +
+
+ `)} +
+ + +
+
+ + My Presets + (${this.presets.filter(p => p.is_default === 0).length}) + + + ${this.showPresets ? '▼' : '▶'} + +
+ +
+ ${this.presets.filter(p => p.is_default === 0).length === 0 ? html` +
+ No custom presets yet.
+ + Create your first preset + +
+ ` : this.presets.filter(p => p.is_default === 0).map(preset => html` +
this.handlePresetSelect(preset)}> + ${preset.title} + ${this.selectedPreset?.id === preset.id ? html`Selected` : ''} +
+ `)} +
+
+ +
+ + +
+ + +
+ + + +
+ ${this.firebaseUser + ? html` + + ` + : html` + + ` + } + +
+
+
+ `; + } +} + +customElements.define('settings-view', SettingsView); \ No newline at end of file diff --git a/src/features/settings/repositories/index.js b/src/features/settings/repositories/index.js new file mode 100644 index 0000000..899e5df --- /dev/null +++ b/src/features/settings/repositories/index.js @@ -0,0 +1,23 @@ +const sqliteRepository = require('./sqlite.repository'); +// const firebaseRepository = require('./firebase.repository'); // Future implementation +const authService = require('../../../common/services/authService'); + +function getRepository() { + // In the future, we can check the user's login status from authService + // const user = authService.getCurrentUser(); + // if (user.isLoggedIn) { + // return firebaseRepository; + // } + return sqliteRepository; +} + +// Directly export functions for ease of use, decided by the strategy +module.exports = { + getSettings: (...args) => getRepository().getSettings(...args), + saveSettings: (...args) => getRepository().saveSettings(...args), + getPresets: (...args) => getRepository().getPresets(...args), + getPresetTemplates: (...args) => getRepository().getPresetTemplates(...args), + createPreset: (...args) => getRepository().createPreset(...args), + updatePreset: (...args) => getRepository().updatePreset(...args), + deletePreset: (...args) => getRepository().deletePreset(...args), +}; \ No newline at end of file diff --git a/src/features/settings/repositories/sqlite.repository.js b/src/features/settings/repositories/sqlite.repository.js new file mode 100644 index 0000000..def54cb --- /dev/null +++ b/src/features/settings/repositories/sqlite.repository.js @@ -0,0 +1,225 @@ +const sqliteClient = require('../../../common/services/sqliteClient'); + +function getSettings(uid) { + const db = sqliteClient.getDb(); + return new Promise((resolve, reject) => { + const query = ` + SELECT * FROM user_settings + WHERE uid = ? + `; + db.get(query, [uid], (err, row) => { + if (err) { + console.error('SQLite: Failed to get settings:', err); + reject(err); + } else if (row) { + // Parse JSON fields + try { + if (row.keybinds) row.keybinds = JSON.parse(row.keybinds); + } catch (parseError) { + console.warn('SQLite: Failed to parse keybinds JSON:', parseError); + row.keybinds = {}; + } + resolve(row); + } else { + // Return default settings if none exist + resolve({ + uid: uid, + profile: 'school', + language: 'en', + screenshot_interval: '5000', + image_quality: '0.8', + layout_mode: 'stacked', + keybinds: {}, + throttle_tokens: 500, + max_tokens: 2000, + throttle_percent: 80, + google_search_enabled: 0, + background_transparency: 0.5, + font_size: 14, + content_protection: 1, + created_at: Math.floor(Date.now() / 1000), + updated_at: Math.floor(Date.now() / 1000) + }); + } + }); + }); +} + +function saveSettings(uid, settings) { + const db = sqliteClient.getDb(); + const now = Math.floor(Date.now() / 1000); + + return new Promise((resolve, reject) => { + // Prepare settings object + const settingsToSave = { + uid: uid, + profile: settings.profile || 'school', + language: settings.language || 'en', + screenshot_interval: settings.screenshot_interval || '5000', + image_quality: settings.image_quality || '0.8', + layout_mode: settings.layout_mode || 'stacked', + keybinds: JSON.stringify(settings.keybinds || {}), + throttle_tokens: settings.throttle_tokens || 500, + max_tokens: settings.max_tokens || 2000, + throttle_percent: settings.throttle_percent || 80, + google_search_enabled: settings.google_search_enabled ? 1 : 0, + background_transparency: settings.background_transparency || 0.5, + font_size: settings.font_size || 14, + content_protection: settings.content_protection ? 1 : 0, + updated_at: now + }; + + const query = ` + INSERT INTO user_settings ( + uid, profile, language, screenshot_interval, image_quality, layout_mode, + keybinds, throttle_tokens, max_tokens, throttle_percent, google_search_enabled, + background_transparency, font_size, content_protection, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(uid) DO UPDATE SET + profile=excluded.profile, + language=excluded.language, + screenshot_interval=excluded.screenshot_interval, + image_quality=excluded.image_quality, + layout_mode=excluded.layout_mode, + keybinds=excluded.keybinds, + throttle_tokens=excluded.throttle_tokens, + max_tokens=excluded.max_tokens, + throttle_percent=excluded.throttle_percent, + google_search_enabled=excluded.google_search_enabled, + background_transparency=excluded.background_transparency, + font_size=excluded.font_size, + content_protection=excluded.content_protection, + updated_at=excluded.updated_at + `; + + const values = [ + settingsToSave.uid, + settingsToSave.profile, + settingsToSave.language, + settingsToSave.screenshot_interval, + settingsToSave.image_quality, + settingsToSave.layout_mode, + settingsToSave.keybinds, + settingsToSave.throttle_tokens, + settingsToSave.max_tokens, + settingsToSave.throttle_percent, + settingsToSave.google_search_enabled, + settingsToSave.background_transparency, + settingsToSave.font_size, + settingsToSave.content_protection, + now, // created_at + settingsToSave.updated_at + ]; + + db.run(query, values, function(err) { + if (err) { + console.error('SQLite: Failed to save settings:', err); + reject(err); + } else { + resolve({ changes: this.changes }); + } + }); + }); +} + +function getPresets(uid) { + const db = sqliteClient.getDb(); + return new Promise((resolve, reject) => { + const query = ` + SELECT * FROM prompt_presets + WHERE uid = ? OR is_default = 1 + ORDER BY is_default DESC, title ASC + `; + db.all(query, [uid], (err, rows) => { + if (err) { + console.error('SQLite: Failed to get presets:', err); + reject(err); + } else { + resolve(rows || []); + } + }); + }); +} + +function getPresetTemplates() { + const db = sqliteClient.getDb(); + return new Promise((resolve, reject) => { + const query = ` + SELECT * FROM prompt_presets + WHERE is_default = 1 + ORDER BY title ASC + `; + db.all(query, [], (err, rows) => { + if (err) { + console.error('SQLite: Failed to get preset templates:', err); + reject(err); + } else { + resolve(rows || []); + } + }); + }); +} + +function createPreset({ uid, title, prompt }) { + const db = sqliteClient.getDb(); + const presetId = require('crypto').randomUUID(); + const now = Math.floor(Date.now() / 1000); + const query = `INSERT INTO prompt_presets (id, uid, title, prompt, is_default, created_at, sync_state) VALUES (?, ?, ?, ?, 0, ?, 'dirty')`; + + return new Promise((resolve, reject) => { + db.run(query, [presetId, uid, title, prompt, now], function(err) { + if (err) { + console.error('SQLite: Failed to create preset:', err); + reject(err); + } else { + resolve({ id: presetId }); + } + }); + }); +} + +function updatePreset(id, { title, prompt }, uid) { + const db = sqliteClient.getDb(); + const query = `UPDATE prompt_presets SET title = ?, prompt = ?, sync_state = 'dirty' WHERE id = ? AND uid = ? AND is_default = 0`; + + return new Promise((resolve, reject) => { + db.run(query, [title, prompt, id, uid], function(err) { + if (err) { + console.error('SQLite: Failed to update preset:', err); + reject(err); + } else if (this.changes === 0) { + reject(new Error("Preset not found or permission denied.")); + } else { + resolve({ changes: this.changes }); + } + }); + }); +} + +function deletePreset(id, uid) { + const db = sqliteClient.getDb(); + const query = `DELETE FROM prompt_presets WHERE id = ? AND uid = ? AND is_default = 0`; + + return new Promise((resolve, reject) => { + db.run(query, [id, uid], function(err) { + if (err) { + console.error('SQLite: Failed to delete preset:', err); + reject(err); + } else if (this.changes === 0) { + reject(new Error("Preset not found or permission denied.")); + } else { + resolve({ changes: this.changes }); + } + }); + }); +} + +module.exports = { + getSettings, + saveSettings, + getPresets, + getPresetTemplates, + createPreset, + updatePreset, + deletePreset, +}; \ No newline at end of file diff --git a/src/features/settings/settingsService.js b/src/features/settings/settingsService.js new file mode 100644 index 0000000..9c6ef27 --- /dev/null +++ b/src/features/settings/settingsService.js @@ -0,0 +1,331 @@ +const { ipcMain, BrowserWindow } = require('electron'); +const authService = require('../../common/services/authService'); +const userRepository = require('../../common/repositories/user'); +const settingsRepository = require('./repositories'); +const { getStoredApiKey, getStoredProvider, windowPool } = require('../../electron/windowManager'); + +// Default keybinds configuration +const DEFAULT_KEYBINDS = { + mac: { + moveUp: 'Cmd+Up', + moveDown: 'Cmd+Down', + moveLeft: 'Cmd+Left', + moveRight: 'Cmd+Right', + toggleVisibility: 'Cmd+\\', + toggleClickThrough: 'Cmd+M', + nextStep: 'Cmd+Enter', + manualScreenshot: 'Cmd+Shift+S', + previousResponse: 'Cmd+[', + nextResponse: 'Cmd+]', + scrollUp: 'Cmd+Shift+Up', + scrollDown: 'Cmd+Shift+Down', + }, + windows: { + moveUp: 'Ctrl+Up', + moveDown: 'Ctrl+Down', + moveLeft: 'Ctrl+Left', + moveRight: 'Ctrl+Right', + toggleVisibility: 'Ctrl+\\', + toggleClickThrough: 'Ctrl+M', + nextStep: 'Ctrl+Enter', + manualScreenshot: 'Ctrl+Shift+S', + previousResponse: 'Ctrl+[', + nextResponse: 'Ctrl+]', + scrollUp: 'Ctrl+Shift+Up', + scrollDown: 'Ctrl+Shift+Down', + } +}; + +// Service state +let currentSettings = null; + +async function getSettings() { + try { + const uid = authService.getCurrentUserId(); + if (!uid) { + throw new Error("User not logged in, cannot get settings."); + } + + const settings = await settingsRepository.getSettings(uid); + currentSettings = settings; + return settings; + } catch (error) { + console.error('[SettingsService] Error getting settings:', error); + return null; + } +} + +function getDefaultSettings() { + const isMac = process.platform === 'darwin'; + return { + profile: 'school', + language: 'en', + screenshotInterval: '5000', + imageQuality: '0.8', + layoutMode: 'stacked', + keybinds: isMac ? DEFAULT_KEYBINDS.mac : DEFAULT_KEYBINDS.windows, + throttleTokens: 500, + maxTokens: 2000, + throttlePercent: 80, + googleSearchEnabled: false, + backgroundTransparency: 0.5, + fontSize: 14, + contentProtection: true + }; +} + +async function saveSettings(settings) { + try { + const uid = authService.getCurrentUserId(); + if (!uid) { + throw new Error("User not logged in, cannot save settings."); + } + + await settingsRepository.saveSettings(uid, settings); + currentSettings = settings; + return { success: true }; + } catch (error) { + console.error('[SettingsService] Error saving settings:', error); + return { success: false, error: error.message }; + } +} + +async function getPresets() { + try { + const uid = authService.getCurrentUserId(); + if (!uid) { + throw new Error("User not logged in, cannot get presets."); + } + + const presets = await settingsRepository.getPresets(uid); + return presets; + } catch (error) { + console.error('[SettingsService] Error getting presets:', error); + return []; + } +} + +async function getPresetTemplates() { + try { + const templates = await settingsRepository.getPresetTemplates(); + return templates; + } catch (error) { + console.error('[SettingsService] Error getting preset templates:', error); + return []; + } +} + +async function createPreset(title, prompt) { + try { + const uid = authService.getCurrentUserId(); + if (!uid) { + throw new Error("User not logged in, cannot create preset."); + } + + const result = await settingsRepository.createPreset({ uid, title, prompt }); + + // 모든 윈도우에 프리셋 업데이트 알림 + BrowserWindow.getAllWindows().forEach(win => { + if (!win.isDestroyed()) { + win.webContents.send('presets-updated'); + } + }); + + return { success: true, id: result.id }; + } catch (error) { + console.error('[SettingsService] Error creating preset:', error); + return { success: false, error: error.message }; + } +} + +async function updatePreset(id, title, prompt) { + try { + const uid = authService.getCurrentUserId(); + if (!uid) { + throw new Error("User not logged in, cannot update preset."); + } + + await settingsRepository.updatePreset(id, { title, prompt }, uid); + + // 모든 윈도우에 프리셋 업데이트 알림 + BrowserWindow.getAllWindows().forEach(win => { + if (!win.isDestroyed()) { + win.webContents.send('presets-updated'); + } + }); + + return { success: true }; + } catch (error) { + console.error('[SettingsService] Error updating preset:', error); + return { success: false, error: error.message }; + } +} + +async function deletePreset(id) { + try { + const uid = authService.getCurrentUserId(); + if (!uid) { + throw new Error("User not logged in, cannot delete preset."); + } + + await settingsRepository.deletePreset(id, uid); + + // 모든 윈도우에 프리셋 업데이트 알림 + BrowserWindow.getAllWindows().forEach(win => { + if (!win.isDestroyed()) { + win.webContents.send('presets-updated'); + } + }); + + return { success: true }; + } catch (error) { + console.error('[SettingsService] Error deleting preset:', error); + return { success: false, error: error.message }; + } +} + +async function saveApiKey(apiKey, provider = 'openai') { + try { + const uid = authService.getCurrentUserId(); + if (!uid) { + // For non-logged-in users, save to local storage + const { app } = require('electron'); + const Store = require('electron-store'); + const store = new Store(); + store.set('apiKey', apiKey); + store.set('provider', provider); + + // Notify windows + BrowserWindow.getAllWindows().forEach(win => { + if (!win.isDestroyed()) { + win.webContents.send('api-key-validated', apiKey); + } + }); + + return { success: true }; + } + + // For logged-in users, save to database + await userRepository.saveApiKey(apiKey, uid, provider); + + // Notify windows + BrowserWindow.getAllWindows().forEach(win => { + if (!win.isDestroyed()) { + win.webContents.send('api-key-validated', apiKey); + } + }); + + return { success: true }; + } catch (error) { + console.error('[SettingsService] Error saving API key:', error); + return { success: false, error: error.message }; + } +} + +async function removeApiKey() { + try { + const uid = authService.getCurrentUserId(); + if (!uid) { + // For non-logged-in users, remove from local storage + const { app } = require('electron'); + const Store = require('electron-store'); + const store = new Store(); + store.delete('apiKey'); + store.delete('provider'); + } else { + // For logged-in users, remove from database + await userRepository.saveApiKey(null, uid, null); + } + + // Notify windows + BrowserWindow.getAllWindows().forEach(win => { + if (!win.isDestroyed()) { + win.webContents.send('api-key-removed'); + } + }); + + return { success: true }; + } catch (error) { + console.error('[SettingsService] Error removing API key:', error); + return { success: false, error: error.message }; + } +} + +async function updateContentProtection(enabled) { + try { + const settings = await getSettings(); + settings.contentProtection = enabled; + + // Update content protection in main window + const { app } = require('electron'); + const mainWindow = windowPool.get('main'); + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.setContentProtection(enabled); + } + + return await saveSettings(settings); + } catch (error) { + console.error('[SettingsService] Error updating content protection:', error); + return { success: false, error: error.message }; + } +} + +function initialize() { + // 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); + }); + + console.log('[SettingsService] Initialized and ready.'); +} + +module.exports = { + initialize, + getSettings, + saveSettings, + getPresets, + getPresetTemplates, + createPreset, + updatePreset, + deletePreset, + saveApiKey, + removeApiKey, + updateContentProtection, +}; \ No newline at end of file diff --git a/src/index.js b/src/index.js index ea8224b..8f68365 100644 --- a/src/index.js +++ b/src/index.js @@ -24,6 +24,7 @@ const fetch = require('node-fetch'); const { autoUpdater } = require('electron-updater'); const { EventEmitter } = require('events'); const askService = require('./features/ask/askService'); +const settingsService = require('./features/settings/settingsService'); const sessionRepository = require('./common/repositories/session'); const eventBridge = new EventEmitter(); @@ -108,6 +109,7 @@ app.whenReady().then(async () => { authService.initialize(); setupLiveSummaryIpcHandlers(openaiSessionRef); askService.initialize(); + settingsService.initialize(); setupGeneralIpcHandlers(); }) .catch(err => { @@ -273,12 +275,30 @@ function setupWebDataHandlers() { break; case 'create-preset': result = await presetRepository.create({ ...payload, uid: currentUserId }); + // 모든 윈도우에 프리셋 업데이트 알림 + BrowserWindow.getAllWindows().forEach(win => { + if (!win.isDestroyed()) { + win.webContents.send('presets-updated'); + } + }); break; case 'update-preset': result = await presetRepository.update(payload.id, payload.data, currentUserId); + // 모든 윈도우에 프리셋 업데이트 알림 + BrowserWindow.getAllWindows().forEach(win => { + if (!win.isDestroyed()) { + win.webContents.send('presets-updated'); + } + }); break; case 'delete-preset': result = await presetRepository.delete(payload, currentUserId); + // 모든 윈도우에 프리셋 업데이트 알림 + BrowserWindow.getAllWindows().forEach(win => { + if (!win.isDestroyed()) { + win.webContents.send('presets-updated'); + } + }); break; // BATCH From 401e83de6fcdb4d5a41f48d3da9daeaa5fab2508 Mon Sep 17 00:00:00 2001 From: jhyang0 Date: Mon, 7 Jul 2025 05:56:09 +0900 Subject: [PATCH 2/3] settings big update --- src/features/settings/settingsService.js | 147 ++++++++++++++++++++--- src/index.js | 21 +--- 2 files changed, 135 insertions(+), 33 deletions(-) diff --git a/src/features/settings/settingsService.js b/src/features/settings/settingsService.js index 9c6ef27..e8df973 100644 --- a/src/features/settings/settingsService.js +++ b/src/features/settings/settingsService.js @@ -4,6 +4,109 @@ const userRepository = require('../../common/repositories/user'); const settingsRepository = require('./repositories'); const { getStoredApiKey, getStoredProvider, windowPool } = require('../../electron/windowManager'); +// Configuration constants +const NOTIFICATION_CONFIG = { + RELEVANT_WINDOW_TYPES: ['settings', 'main'], + DEBOUNCE_DELAY: 300, // prevent spam during bulk operations (ms) + MAX_RETRY_ATTEMPTS: 3, + RETRY_BASE_DELAY: 1000, // exponential backoff base (ms) +}; + +// window targeting system +class WindowNotificationManager { + constructor() { + this.pendingNotifications = new Map(); + } + + /** + * Send notifications only to relevant windows + * @param {string} event - Event name + * @param {*} data - Event data + * @param {object} options - Notification options + */ + notifyRelevantWindows(event, data = null, options = {}) { + const { + windowTypes = NOTIFICATION_CONFIG.RELEVANT_WINDOW_TYPES, + debounce = NOTIFICATION_CONFIG.DEBOUNCE_DELAY + } = options; + + if (debounce > 0) { + this.debounceNotification(event, () => { + this.sendToTargetWindows(event, data, windowTypes); + }, debounce); + } else { + this.sendToTargetWindows(event, data, windowTypes); + } + } + + sendToTargetWindows(event, data, windowTypes) { + const relevantWindows = this.getRelevantWindows(windowTypes); + + if (relevantWindows.length === 0) { + console.log(`[WindowNotificationManager] No relevant windows found for event: ${event}`); + return; + } + + console.log(`[WindowNotificationManager] Sending ${event} to ${relevantWindows.length} relevant windows`); + + relevantWindows.forEach(win => { + try { + if (data) { + win.webContents.send(event, data); + } else { + win.webContents.send(event); + } + } catch (error) { + console.warn(`[WindowNotificationManager] Failed to send ${event} to window:`, error.message); + } + }); + } + + getRelevantWindows(windowTypes) { + const allWindows = BrowserWindow.getAllWindows(); + const relevantWindows = []; + + allWindows.forEach(win => { + if (win.isDestroyed()) return; + + for (const [windowName, poolWindow] of windowPool || []) { + if (poolWindow === win && windowTypes.includes(windowName)) { + if (windowName === 'settings' || win.isVisible()) { + relevantWindows.push(win); + } + break; + } + } + }); + + return relevantWindows; + } + + debounceNotification(key, fn, delay) { + // Clear existing timeout + if (this.pendingNotifications.has(key)) { + clearTimeout(this.pendingNotifications.get(key)); + } + + // Set new timeout + const timeoutId = setTimeout(() => { + fn(); + this.pendingNotifications.delete(key); + }, delay); + + this.pendingNotifications.set(key, timeoutId); + } + + cleanup() { + // Clear all pending notifications + this.pendingNotifications.forEach(timeoutId => clearTimeout(timeoutId)); + this.pendingNotifications.clear(); + } +} + +// Global instance +const windowNotificationManager = new WindowNotificationManager(); + // Default keybinds configuration const DEFAULT_KEYBINDS = { mac: { @@ -124,11 +227,10 @@ async function createPreset(title, prompt) { const result = await settingsRepository.createPreset({ uid, title, prompt }); - // 모든 윈도우에 프리셋 업데이트 알림 - BrowserWindow.getAllWindows().forEach(win => { - if (!win.isDestroyed()) { - win.webContents.send('presets-updated'); - } + windowNotificationManager.notifyRelevantWindows('presets-updated', { + action: 'created', + presetId: result.id, + title }); return { success: true, id: result.id }; @@ -147,11 +249,10 @@ async function updatePreset(id, title, prompt) { await settingsRepository.updatePreset(id, { title, prompt }, uid); - // 모든 윈도우에 프리셋 업데이트 알림 - BrowserWindow.getAllWindows().forEach(win => { - if (!win.isDestroyed()) { - win.webContents.send('presets-updated'); - } + windowNotificationManager.notifyRelevantWindows('presets-updated', { + action: 'updated', + presetId: id, + title }); return { success: true }; @@ -170,11 +271,9 @@ async function deletePreset(id) { await settingsRepository.deletePreset(id, uid); - // 모든 윈도우에 프리셋 업데이트 알림 - BrowserWindow.getAllWindows().forEach(win => { - if (!win.isDestroyed()) { - win.webContents.send('presets-updated'); - } + windowNotificationManager.notifyRelevantWindows('presets-updated', { + action: 'deleted', + presetId: id }); return { success: true }; @@ -271,6 +370,9 @@ async function updateContentProtection(enabled) { } function initialize() { + // cleanup + windowNotificationManager.cleanup(); + // IPC handlers for settings ipcMain.handle('settings:getSettings', async () => { return await getSettings(); @@ -316,8 +418,23 @@ function initialize() { console.log('[SettingsService] Initialized and ready.'); } +// Cleanup function +function cleanup() { + windowNotificationManager.cleanup(); + console.log('[SettingsService] Cleaned up resources.'); +} + +function notifyPresetUpdate(action, presetId, title = null) { + const data = { action, presetId }; + if (title) data.title = title; + + windowNotificationManager.notifyRelevantWindows('presets-updated', data); +} + module.exports = { initialize, + cleanup, + notifyPresetUpdate, getSettings, saveSettings, getPresets, diff --git a/src/index.js b/src/index.js index 8f68365..5c4a3ff 100644 --- a/src/index.js +++ b/src/index.js @@ -275,30 +275,15 @@ function setupWebDataHandlers() { break; case 'create-preset': result = await presetRepository.create({ ...payload, uid: currentUserId }); - // 모든 윈도우에 프리셋 업데이트 알림 - BrowserWindow.getAllWindows().forEach(win => { - if (!win.isDestroyed()) { - win.webContents.send('presets-updated'); - } - }); + settingsService.notifyPresetUpdate('created', result.id, payload.title); break; case 'update-preset': result = await presetRepository.update(payload.id, payload.data, currentUserId); - // 모든 윈도우에 프리셋 업데이트 알림 - BrowserWindow.getAllWindows().forEach(win => { - if (!win.isDestroyed()) { - win.webContents.send('presets-updated'); - } - }); + settingsService.notifyPresetUpdate('updated', payload.id, payload.data.title); break; case 'delete-preset': result = await presetRepository.delete(payload, currentUserId); - // 모든 윈도우에 프리셋 업데이트 알림 - BrowserWindow.getAllWindows().forEach(win => { - if (!win.isDestroyed()) { - win.webContents.send('presets-updated'); - } - }); + settingsService.notifyPresetUpdate('deleted', payload); break; // BATCH From 549a3cdeb9f45c7721f577a81ef83e08208bc5e9 Mon Sep 17 00:00:00 2001 From: jhyang0 Date: Mon, 7 Jul 2025 06:17:42 +0900 Subject: [PATCH 3/3] settings big update --- src/features/settings/repositories/index.js | 2 - .../repositories/sqlite.repository.js | 160 +++--------------- src/features/settings/settingsService.js | 60 ++++--- 3 files changed, 59 insertions(+), 163 deletions(-) diff --git a/src/features/settings/repositories/index.js b/src/features/settings/repositories/index.js index 899e5df..508ebe5 100644 --- a/src/features/settings/repositories/index.js +++ b/src/features/settings/repositories/index.js @@ -13,8 +13,6 @@ function getRepository() { // Directly export functions for ease of use, decided by the strategy module.exports = { - getSettings: (...args) => getRepository().getSettings(...args), - saveSettings: (...args) => getRepository().saveSettings(...args), getPresets: (...args) => getRepository().getPresets(...args), getPresetTemplates: (...args) => getRepository().getPresetTemplates(...args), createPreset: (...args) => getRepository().createPreset(...args), diff --git a/src/features/settings/repositories/sqlite.repository.js b/src/features/settings/repositories/sqlite.repository.js index def54cb..82d0c01 100644 --- a/src/features/settings/repositories/sqlite.repository.js +++ b/src/features/settings/repositories/sqlite.repository.js @@ -1,127 +1,5 @@ const sqliteClient = require('../../../common/services/sqliteClient'); -function getSettings(uid) { - const db = sqliteClient.getDb(); - return new Promise((resolve, reject) => { - const query = ` - SELECT * FROM user_settings - WHERE uid = ? - `; - db.get(query, [uid], (err, row) => { - if (err) { - console.error('SQLite: Failed to get settings:', err); - reject(err); - } else if (row) { - // Parse JSON fields - try { - if (row.keybinds) row.keybinds = JSON.parse(row.keybinds); - } catch (parseError) { - console.warn('SQLite: Failed to parse keybinds JSON:', parseError); - row.keybinds = {}; - } - resolve(row); - } else { - // Return default settings if none exist - resolve({ - uid: uid, - profile: 'school', - language: 'en', - screenshot_interval: '5000', - image_quality: '0.8', - layout_mode: 'stacked', - keybinds: {}, - throttle_tokens: 500, - max_tokens: 2000, - throttle_percent: 80, - google_search_enabled: 0, - background_transparency: 0.5, - font_size: 14, - content_protection: 1, - created_at: Math.floor(Date.now() / 1000), - updated_at: Math.floor(Date.now() / 1000) - }); - } - }); - }); -} - -function saveSettings(uid, settings) { - const db = sqliteClient.getDb(); - const now = Math.floor(Date.now() / 1000); - - return new Promise((resolve, reject) => { - // Prepare settings object - const settingsToSave = { - uid: uid, - profile: settings.profile || 'school', - language: settings.language || 'en', - screenshot_interval: settings.screenshot_interval || '5000', - image_quality: settings.image_quality || '0.8', - layout_mode: settings.layout_mode || 'stacked', - keybinds: JSON.stringify(settings.keybinds || {}), - throttle_tokens: settings.throttle_tokens || 500, - max_tokens: settings.max_tokens || 2000, - throttle_percent: settings.throttle_percent || 80, - google_search_enabled: settings.google_search_enabled ? 1 : 0, - background_transparency: settings.background_transparency || 0.5, - font_size: settings.font_size || 14, - content_protection: settings.content_protection ? 1 : 0, - updated_at: now - }; - - const query = ` - INSERT INTO user_settings ( - uid, profile, language, screenshot_interval, image_quality, layout_mode, - keybinds, throttle_tokens, max_tokens, throttle_percent, google_search_enabled, - background_transparency, font_size, content_protection, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(uid) DO UPDATE SET - profile=excluded.profile, - language=excluded.language, - screenshot_interval=excluded.screenshot_interval, - image_quality=excluded.image_quality, - layout_mode=excluded.layout_mode, - keybinds=excluded.keybinds, - throttle_tokens=excluded.throttle_tokens, - max_tokens=excluded.max_tokens, - throttle_percent=excluded.throttle_percent, - google_search_enabled=excluded.google_search_enabled, - background_transparency=excluded.background_transparency, - font_size=excluded.font_size, - content_protection=excluded.content_protection, - updated_at=excluded.updated_at - `; - - const values = [ - settingsToSave.uid, - settingsToSave.profile, - settingsToSave.language, - settingsToSave.screenshot_interval, - settingsToSave.image_quality, - settingsToSave.layout_mode, - settingsToSave.keybinds, - settingsToSave.throttle_tokens, - settingsToSave.max_tokens, - settingsToSave.throttle_percent, - settingsToSave.google_search_enabled, - settingsToSave.background_transparency, - settingsToSave.font_size, - settingsToSave.content_protection, - now, // created_at - settingsToSave.updated_at - ]; - - db.run(query, values, function(err) { - if (err) { - console.error('SQLite: Failed to save settings:', err); - reject(err); - } else { - resolve({ changes: this.changes }); - } - }); - }); -} - function getPresets(uid) { const db = sqliteClient.getDb(); return new Promise((resolve, reject) => { @@ -162,17 +40,19 @@ function getPresetTemplates() { function createPreset({ uid, title, prompt }) { const db = sqliteClient.getDb(); - const presetId = require('crypto').randomUUID(); - const now = Math.floor(Date.now() / 1000); - const query = `INSERT INTO prompt_presets (id, uid, title, prompt, is_default, created_at, sync_state) VALUES (?, ?, ?, ?, 0, ?, 'dirty')`; - return new Promise((resolve, reject) => { - db.run(query, [presetId, uid, title, prompt, now], function(err) { + const id = require('crypto').randomUUID(); + const now = Math.floor(Date.now() / 1000); + const query = ` + INSERT INTO prompt_presets (id, uid, title, prompt, is_default, created_at, sync_state) + VALUES (?, ?, ?, ?, 0, ?, 'dirty') + `; + db.run(query, [id, uid, title, prompt, now], function(err) { if (err) { console.error('SQLite: Failed to create preset:', err); reject(err); } else { - resolve({ id: presetId }); + resolve({ id }); } }); }); @@ -180,15 +60,19 @@ function createPreset({ uid, title, prompt }) { function updatePreset(id, { title, prompt }, uid) { const db = sqliteClient.getDb(); - const query = `UPDATE prompt_presets SET title = ?, prompt = ?, sync_state = 'dirty' WHERE id = ? AND uid = ? AND is_default = 0`; - return new Promise((resolve, reject) => { - db.run(query, [title, prompt, id, uid], function(err) { + const now = Math.floor(Date.now() / 1000); + const query = ` + UPDATE prompt_presets + SET title = ?, prompt = ?, sync_state = 'dirty', updated_at = ? + WHERE id = ? AND uid = ? AND is_default = 0 + `; + db.run(query, [title, prompt, now, id, uid], function(err) { if (err) { console.error('SQLite: Failed to update preset:', err); reject(err); } else if (this.changes === 0) { - reject(new Error("Preset not found or permission denied.")); + reject(new Error('Preset not found, is default, or permission denied')); } else { resolve({ changes: this.changes }); } @@ -198,15 +82,17 @@ function updatePreset(id, { title, prompt }, uid) { function deletePreset(id, uid) { const db = sqliteClient.getDb(); - const query = `DELETE FROM prompt_presets WHERE id = ? AND uid = ? AND is_default = 0`; - return new Promise((resolve, reject) => { + const query = ` + DELETE FROM prompt_presets + WHERE id = ? AND uid = ? AND is_default = 0 + `; db.run(query, [id, uid], function(err) { if (err) { console.error('SQLite: Failed to delete preset:', err); reject(err); } else if (this.changes === 0) { - reject(new Error("Preset not found or permission denied.")); + reject(new Error('Preset not found, is default, or permission denied')); } else { resolve({ changes: this.changes }); } @@ -215,11 +101,9 @@ function deletePreset(id, uid) { } module.exports = { - getSettings, - saveSettings, getPresets, getPresetTemplates, createPreset, updatePreset, - deletePreset, + deletePreset }; \ No newline at end of file diff --git a/src/features/settings/settingsService.js b/src/features/settings/settingsService.js index e8df973..6360f36 100644 --- a/src/features/settings/settingsService.js +++ b/src/features/settings/settingsService.js @@ -1,9 +1,17 @@ const { ipcMain, BrowserWindow } = require('electron'); +const Store = require('electron-store'); const authService = require('../../common/services/authService'); const userRepository = require('../../common/repositories/user'); const settingsRepository = require('./repositories'); const { getStoredApiKey, getStoredProvider, windowPool } = require('../../electron/windowManager'); +const store = new Store({ + name: 'pickle-glass-settings', + defaults: { + users: {} + } +}); + // Configuration constants const NOTIFICATION_CONFIG = { RELEVANT_WINDOW_TYPES: ['settings', 'main'], @@ -142,22 +150,6 @@ const DEFAULT_KEYBINDS = { // Service state let currentSettings = null; -async function getSettings() { - try { - const uid = authService.getCurrentUserId(); - if (!uid) { - throw new Error("User not logged in, cannot get settings."); - } - - const settings = await settingsRepository.getSettings(uid); - currentSettings = settings; - return settings; - } catch (error) { - console.error('[SettingsService] Error getting settings:', error); - return null; - } -} - function getDefaultSettings() { const isMac = process.platform === 'darwin'; return { @@ -177,18 +169,39 @@ function getDefaultSettings() { }; } +async function getSettings() { + try { + const uid = authService.getCurrentUserId(); + const userSettingsKey = uid ? `users.${uid}` : 'users.default'; + + const defaultSettings = getDefaultSettings(); + const savedSettings = store.get(userSettingsKey, {}); + + currentSettings = { ...defaultSettings, ...savedSettings }; + return currentSettings; + } catch (error) { + console.error('[SettingsService] Error getting settings from store:', error); + return getDefaultSettings(); + } +} + async function saveSettings(settings) { try { const uid = authService.getCurrentUserId(); - if (!uid) { - throw new Error("User not logged in, cannot save settings."); - } + const userSettingsKey = uid ? `users.${uid}` : 'users.default'; - await settingsRepository.saveSettings(uid, settings); - currentSettings = settings; + const currentSaved = store.get(userSettingsKey, {}); + const newSettings = { ...currentSaved, ...settings }; + + store.set(userSettingsKey, newSettings); + currentSettings = newSettings; + + // Use smart notification system + windowNotificationManager.notifyRelevantWindows('settings-updated', currentSettings); + return { success: true }; } catch (error) { - console.error('[SettingsService] Error saving settings:', error); + console.error('[SettingsService] Error saving settings to store:', error); return { success: false, error: error.message }; } } @@ -197,7 +210,8 @@ async function getPresets() { try { const uid = authService.getCurrentUserId(); if (!uid) { - throw new Error("User not logged in, cannot get presets."); + // Logged out users only see default presets + return await settingsRepository.getPresetTemplates(); } const presets = await settingsRepository.getPresets(uid);