Merge branch 'main' of https://github.com/pickle-com/glass-private
This commit is contained in:
commit
1c1da37f4a
@ -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 {
|
||||
></assistant-view>`;
|
||||
case 'ask':
|
||||
return html`<ask-view></ask-view>`;
|
||||
case 'customize':
|
||||
return html`<customize-view
|
||||
case 'settings':
|
||||
return html`<settings-view
|
||||
.selectedProfile=${this.selectedProfile}
|
||||
.selectedLanguage=${this.selectedLanguage}
|
||||
.onProfileChange=${profile => (this.selectedProfile = profile)}
|
||||
.onLanguageChange=${lang => (this.selectedLanguage = lang)}
|
||||
></customize-view>`;
|
||||
></settings-view>`;
|
||||
case 'history':
|
||||
return html`<history-view></history-view>`;
|
||||
case 'help':
|
||||
return html`<help-view></help-view>`;
|
||||
case 'onboarding':
|
||||
return html`<onboarding-view></onboarding-view>`;
|
||||
case 'setup':
|
||||
return html`<setup-view></setup-view>`;
|
||||
default:
|
||||
|
@ -102,7 +102,7 @@ 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'}})
|
||||
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,
|
||||
|
File diff suppressed because it is too large
Load Diff
809
src/features/settings/SettingsView.js
Normal file
809
src/features/settings/SettingsView.js
Normal 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);
|
21
src/features/settings/repositories/index.js
Normal file
21
src/features/settings/repositories/index.js
Normal file
@ -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),
|
||||
};
|
109
src/features/settings/repositories/sqlite.repository.js
Normal file
109
src/features/settings/repositories/sqlite.repository.js
Normal file
@ -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
|
||||
};
|
462
src/features/settings/settingsService.js
Normal file
462
src/features/settings/settingsService.js
Normal file
@ -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,
|
||||
};
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user