settings big update

This commit is contained in:
jhyang0 2025-07-07 05:42:05 +09:00
parent a18e93583f
commit 64111891eb
8 changed files with 1419 additions and 1115 deletions

View File

@ -1,7 +1,6 @@
import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js'; 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 { AssistantView } from '../features/listen/AssistantView.js';
import { OnboardingView } from '../features/onboarding/OnboardingView.js';
import { AskView } from '../features/ask/AskView.js'; import { AskView } from '../features/ask/AskView.js';
import '../features/listen/renderer.js'; import '../features/listen/renderer.js';
@ -21,7 +20,7 @@ export class PickleGlassApp extends LitElement {
width: 100%; 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; display: block;
width: 100%; width: 100%;
} }
@ -180,8 +179,8 @@ export class PickleGlassApp extends LitElement {
this.isMainViewVisible = !this.isMainViewVisible; this.isMainViewVisible = !this.isMainViewVisible;
} }
handleCustomizeClick() { handleSettingsClick() {
this.currentView = 'customize'; this.currentView = 'settings';
this.isMainViewVisible = true; this.isMainViewVisible = true;
} }
@ -247,10 +246,6 @@ export class PickleGlassApp extends LitElement {
this.currentResponseIndex = e.detail.index; this.currentResponseIndex = e.detail.index;
} }
handleOnboardingComplete() {
this.currentView = 'main';
}
render() { render() {
switch (this.currentView) { switch (this.currentView) {
case 'listen': case 'listen':
@ -263,19 +258,17 @@ export class PickleGlassApp extends LitElement {
></assistant-view>`; ></assistant-view>`;
case 'ask': case 'ask':
return html`<ask-view></ask-view>`; return html`<ask-view></ask-view>`;
case 'customize': case 'settings':
return html`<customize-view return html`<settings-view
.selectedProfile=${this.selectedProfile} .selectedProfile=${this.selectedProfile}
.selectedLanguage=${this.selectedLanguage} .selectedLanguage=${this.selectedLanguage}
.onProfileChange=${profile => (this.selectedProfile = profile)} .onProfileChange=${profile => (this.selectedProfile = profile)}
.onLanguageChange=${lang => (this.selectedLanguage = lang)} .onLanguageChange=${lang => (this.selectedLanguage = lang)}
></customize-view>`; ></settings-view>`;
case 'history': case 'history':
return html`<history-view></history-view>`; return html`<history-view></history-view>`;
case 'help': case 'help':
return html`<help-view></help-view>`; return html`<help-view></help-view>`;
case 'onboarding':
return html`<onboarding-view></onboarding-view>`;
case 'setup': case 'setup':
return html`<setup-view></setup-view>`; return html`<setup-view></setup-view>`;
default: default:

View File

@ -102,8 +102,8 @@ function createFeatureWindows(header) {
const settings = new BrowserWindow({ ...commonChildOptions, width:240, height:450, parent:undefined }); const settings = new BrowserWindow({ ...commonChildOptions, width:240, height:450, parent:undefined });
settings.setContentProtection(isContentProtectionOn); settings.setContentProtection(isContentProtectionOn);
settings.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); settings.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
settings.loadFile(path.join(__dirname,'../app/content.html'),{query:{view:'customize'}}) settings.loadFile(path.join(__dirname,'../app/content.html'),{query:{view:'settings'}})
.catch(console.error); .catch(console.error);
windowPool.set('settings', settings); windowPool.set('settings', settings);
} }
@ -1950,7 +1950,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, openaiSessi
globalShortcut.unregisterAll(); globalShortcut.unregisterAll();
if (movementManager) { if (movementManager) {
movementManager.destroy(); movementManager.destroy();nd
} }
movementManager = new SmoothMovementManager(); movementManager = new SmoothMovementManager();
@ -2162,7 +2162,7 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, openaiSessionRef) {
if (isMainViewVisible) { if (isMainViewVisible) {
const viewHeights = { const viewHeights = {
listen: 400, listen: 400,
customize: 600, settings: 600,
help: 550, help: 550,
history: 550, history: 550,
setup: 200, setup: 200,

File diff suppressed because it is too large Load Diff

View File

@ -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`
<div class="settings-container">
<div class="loading-state">
<div class="loading-spinner"></div>
<span>Loading...</span>
</div>
</div>
`;
}
const loggedIn = !!this.firebaseUser;
return html`
<div class="settings-container">
<div class="header-section">
<div>
<h1 class="app-title">Pickle Glass</h1>
<div class="account-info">
${this.firebaseUser
? html`Account: ${this.firebaseUser.email || 'Logged In'}`
: this.apiKey && this.apiKey.length > 10
? html`API Key: ${this.apiKey.substring(0, 6)}...${this.apiKey.substring(this.apiKey.length - 6)}`
: `Account: Not Logged In`
}
</div>
</div>
<div class="invisibility-icon ${this.isContentProtectionOn ? 'visible' : ''}" title="Invisibility is On">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.785 7.41787C8.7 7.41787 7.79 8.19371 7.55667 9.22621C7.0025 8.98704 6.495 9.05121 6.11 9.22037C5.87083 8.18204 4.96083 7.41787 3.88167 7.41787C2.61583 7.41787 1.58333 8.46204 1.58333 9.75121C1.58333 11.0404 2.61583 12.0845 3.88167 12.0845C5.08333 12.0845 6.06333 11.1395 6.15667 9.93787C6.355 9.79787 6.87417 9.53537 7.51 9.94954C7.615 11.1454 8.58333 12.0845 9.785 12.0845C11.0508 12.0845 12.0833 11.0404 12.0833 9.75121C12.0833 8.46204 11.0508 7.41787 9.785 7.41787ZM3.88167 11.4195C2.97167 11.4195 2.2425 10.6729 2.2425 9.75121C2.2425 8.82954 2.9775 8.08287 3.88167 8.08287C4.79167 8.08287 5.52083 8.82954 5.52083 9.75121C5.52083 10.6729 4.79167 11.4195 3.88167 11.4195ZM9.785 11.4195C8.875 11.4195 8.14583 10.6729 8.14583 9.75121C8.14583 8.82954 8.875 8.08287 9.785 8.08287C10.695 8.08287 11.43 8.82954 11.43 9.75121C11.43 10.6729 10.6892 11.4195 9.785 11.4195ZM12.6667 5.95954H1V6.83454H12.6667V5.95954ZM8.8925 1.36871C8.76417 1.08287 8.4375 0.931207 8.12833 1.03037L6.83333 1.46204L5.5325 1.03037L5.50333 1.02454C5.19417 0.93704 4.8675 1.10037 4.75083 1.39787L3.33333 5.08454H10.3333L8.91 1.39787L8.8925 1.36871Z" fill="white"/>
</svg>
</div>
</div>
<div class="api-key-section">
<input
type="password"
id="api-key-input"
placeholder="Enter API Key"
.value=${this.apiKey || ''}
?disabled=${loggedIn}
>
<button class="settings-button full-width" @click=${this.handleSaveApiKey} ?disabled=${loggedIn}>
Save API Key
</button>
</div>
<div class="shortcuts-section">
${this.getMainShortcuts().map(shortcut => html`
<div class="shortcut-item">
<span class="shortcut-name">${shortcut.name}</span>
<div class="shortcut-keys">
<span class="cmd-key"></span>
<span class="shortcut-key">${shortcut.key}</span>
</div>
</div>
`)}
</div>
<!-- Preset Management Section -->
<div class="preset-section">
<div class="preset-header">
<span class="preset-title">
My Presets
<span class="preset-count">(${this.presets.filter(p => p.is_default === 0).length})</span>
</span>
<span class="preset-toggle" @click=${this.togglePresets}>
${this.showPresets ? '▼' : '▶'}
</span>
</div>
<div class="preset-list ${this.showPresets ? '' : 'hidden'}">
${this.presets.filter(p => p.is_default === 0).length === 0 ? html`
<div class="no-presets-message">
No custom presets yet.<br>
<span class="web-link" @click=${this.handlePersonalize}>
Create your first preset
</span>
</div>
` : this.presets.filter(p => p.is_default === 0).map(preset => html`
<div class="preset-item ${this.selectedPreset?.id === preset.id ? 'selected' : ''}"
@click=${() => this.handlePresetSelect(preset)}>
<span class="preset-name">${preset.title}</span>
${this.selectedPreset?.id === preset.id ? html`<span class="preset-status">Selected</span>` : ''}
</div>
`)}
</div>
</div>
<div class="buttons-section">
<button class="settings-button full-width" @click=${this.handlePersonalize}>
<span>Personalize / Meeting Notes</span>
</button>
<div class="move-buttons">
<button class="settings-button half-width" @click=${this.handleMoveLeft}>
<span> Move</span>
</button>
<button class="settings-button half-width" @click=${this.handleMoveRight}>
<span>Move </span>
</button>
</div>
<button class="settings-button full-width" @click=${this.handleToggleInvisibility}>
<span>${this.isContentProtectionOn ? 'Disable Invisibility' : 'Enable Invisibility'}</span>
</button>
<div class="bottom-buttons">
${this.firebaseUser
? html`
<button class="settings-button half-width danger" @click=${this.handleFirebaseLogout}>
<span>Logout</span>
</button>
`
: html`
<button class="settings-button half-width danger" @click=${this.handleClearApiKey}>
<span>Clear API Key</span>
</button>
`
}
<button class="settings-button half-width danger" @click=${this.handleQuit}>
<span>Quit</span>
</button>
</div>
</div>
</div>
`;
}
}
customElements.define('settings-view', SettingsView);

View File

@ -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),
};

View File

@ -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,
};

View File

@ -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,
};

View File

@ -24,6 +24,7 @@ const fetch = require('node-fetch');
const { autoUpdater } = require('electron-updater'); const { autoUpdater } = require('electron-updater');
const { EventEmitter } = require('events'); const { EventEmitter } = require('events');
const askService = require('./features/ask/askService'); const askService = require('./features/ask/askService');
const settingsService = require('./features/settings/settingsService');
const sessionRepository = require('./common/repositories/session'); const sessionRepository = require('./common/repositories/session');
const eventBridge = new EventEmitter(); const eventBridge = new EventEmitter();
@ -108,6 +109,7 @@ app.whenReady().then(async () => {
authService.initialize(); authService.initialize();
setupLiveSummaryIpcHandlers(openaiSessionRef); setupLiveSummaryIpcHandlers(openaiSessionRef);
askService.initialize(); askService.initialize();
settingsService.initialize();
setupGeneralIpcHandlers(); setupGeneralIpcHandlers();
}) })
.catch(err => { .catch(err => {
@ -273,12 +275,30 @@ function setupWebDataHandlers() {
break; break;
case 'create-preset': case 'create-preset':
result = await presetRepository.create({ ...payload, uid: currentUserId }); result = await presetRepository.create({ ...payload, uid: currentUserId });
// 모든 윈도우에 프리셋 업데이트 알림
BrowserWindow.getAllWindows().forEach(win => {
if (!win.isDestroyed()) {
win.webContents.send('presets-updated');
}
});
break; break;
case 'update-preset': case 'update-preset':
result = await presetRepository.update(payload.id, payload.data, currentUserId); result = await presetRepository.update(payload.id, payload.data, currentUserId);
// 모든 윈도우에 프리셋 업데이트 알림
BrowserWindow.getAllWindows().forEach(win => {
if (!win.isDestroyed()) {
win.webContents.send('presets-updated');
}
});
break; break;
case 'delete-preset': case 'delete-preset':
result = await presetRepository.delete(payload, currentUserId); result = await presetRepository.delete(payload, currentUserId);
// 모든 윈도우에 프리셋 업데이트 알림
BrowserWindow.getAllWindows().forEach(win => {
if (!win.isDestroyed()) {
win.webContents.send('presets-updated');
}
});
break; break;
// BATCH // BATCH