diff --git a/src/app/PickleGlassApp.js b/src/app/PickleGlassApp.js
index 96d0516..b330600 100644
--- a/src/app/PickleGlassApp.js
+++ b/src/app/PickleGlassApp.js
@@ -1,5 +1,5 @@
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 { AskView } from '../features/ask/AskView.js';
@@ -20,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%;
}
@@ -179,8 +179,8 @@ export class PickleGlassApp extends LitElement {
this.isMainViewVisible = !this.isMainViewVisible;
}
- handleCustomizeClick() {
- this.currentView = 'customize';
+ handleSettingsClick() {
+ this.currentView = 'settings';
this.isMainViewVisible = true;
}
@@ -246,10 +246,6 @@ export class PickleGlassApp extends LitElement {
this.currentResponseIndex = e.detail.index;
}
- handleOnboardingComplete() {
- this.currentView = 'main';
- }
-
render() {
switch (this.currentView) {
case 'listen':
@@ -262,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 e4cc058..0cd9826 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`
-
-
-
-
-
-
-
-
-
- ${this.getMainShortcuts().map(shortcut => html`
-
-
${shortcut.name}
-
- ⌘
- ${shortcut.key}
-
-
- `)}
-
-
-
-
- `;
- }
-
- 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`
+
+ `;
+ }
+
+ const loggedIn = !!this.firebaseUser;
+
+ return html`
+
+
+
+
+
+
+
+
+
+ ${this.getMainShortcuts().map(shortcut => html`
+
+
${shortcut.name}
+
+ ⌘
+ ${shortcut.key}
+
+
+ `)}
+
+
+
+
+
+
+
+ ${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` : ''}
+
+ `)}
+
+
+
+
+
+ `;
+ }
+}
+
+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..508ebe5
--- /dev/null
+++ b/src/features/settings/repositories/index.js
@@ -0,0 +1,21 @@
+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 = {
+ 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..82d0c01
--- /dev/null
+++ b/src/features/settings/repositories/sqlite.repository.js
@@ -0,0 +1,109 @@
+const sqliteClient = require('../../../common/services/sqliteClient');
+
+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();
+ return new Promise((resolve, reject) => {
+ 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 });
+ }
+ });
+ });
+}
+
+function updatePreset(id, { title, prompt }, uid) {
+ const db = sqliteClient.getDb();
+ return new Promise((resolve, reject) => {
+ 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, is default, or permission denied'));
+ } else {
+ resolve({ changes: this.changes });
+ }
+ });
+ });
+}
+
+function deletePreset(id, uid) {
+ const db = sqliteClient.getDb();
+ 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, is default, or permission denied'));
+ } else {
+ resolve({ changes: this.changes });
+ }
+ });
+ });
+}
+
+module.exports = {
+ 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..6360f36
--- /dev/null
+++ b/src/features/settings/settingsService.js
@@ -0,0 +1,462 @@
+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'],
+ 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: {
+ 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;
+
+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 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();
+ const userSettingsKey = uid ? `users.${uid}` : 'users.default';
+
+ 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 to store:', error);
+ return { success: false, error: error.message };
+ }
+}
+
+async function getPresets() {
+ try {
+ const uid = authService.getCurrentUserId();
+ if (!uid) {
+ // Logged out users only see default presets
+ return await settingsRepository.getPresetTemplates();
+ }
+
+ 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 });
+
+ windowNotificationManager.notifyRelevantWindows('presets-updated', {
+ action: 'created',
+ presetId: result.id,
+ title
+ });
+
+ 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);
+
+ windowNotificationManager.notifyRelevantWindows('presets-updated', {
+ action: 'updated',
+ presetId: id,
+ title
+ });
+
+ 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);
+
+ windowNotificationManager.notifyRelevantWindows('presets-updated', {
+ action: 'deleted',
+ presetId: id
+ });
+
+ 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() {
+ // 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);
+ });
+
+ 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,
+ getPresetTemplates,
+ createPreset,
+ updatePreset,
+ deletePreset,
+ saveApiKey,
+ removeApiKey,
+ updateContentProtection,
+};
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index 1cca48f..eb6e89c 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();
@@ -110,6 +111,7 @@ app.whenReady().then(async () => {
authService.initialize();
listenService.setupIpcHandlers();
askService.initialize();
+ settingsService.initialize();
setupGeneralIpcHandlers();
})
.catch(err => {
@@ -276,12 +278,15 @@ function setupWebDataHandlers() {
break;
case 'create-preset':
result = await presetRepository.create({ ...payload, uid: currentUserId });
+ settingsService.notifyPresetUpdate('created', result.id, payload.title);
break;
case 'update-preset':
result = await presetRepository.update(payload.id, payload.data, currentUserId);
+ settingsService.notifyPresetUpdate('updated', payload.id, payload.data.title);
break;
case 'delete-preset':
result = await presetRepository.delete(payload, currentUserId);
+ settingsService.notifyPresetUpdate('deleted', payload);
break;
// BATCH