Skip to content

Commit 678c6ad

Browse files
macclaude
andcommitted
feat: Settings UI and profile management system
- Add comprehensive Settings UI with macOS-style design - Implement settings modal and dialog system - Add hotkey management infrastructure - Include user authentication hooks (Privy integration) - Add online status monitoring - Implement dialog manager for native settings window - Add user profile store hooks for settings integration This is the settings UI layer (3/6) from PR #45. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent a75f9fd commit 678c6ad

File tree

14 files changed

+6383
-0
lines changed

14 files changed

+6383
-0
lines changed

apps/electron-app/src/main/browser/dialog-manager.ts

Lines changed: 1064 additions & 0 deletions
Large diffs are not rendered by default.

apps/electron-app/src/main/browser/templates/settings-dialog.html

Lines changed: 1724 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { globalShortcut } from "electron";
2+
import { createLogger } from "@vibe/shared-types";
3+
import { useUserProfileStore } from "@/store/user-profile-store";
4+
5+
const logger = createLogger("hotkey-manager");
6+
7+
// Default hotkey for password paste
8+
const DEFAULT_PASSWORD_PASTE_HOTKEY = "CommandOrControl+Shift+P";
9+
10+
// Currently registered hotkeys
11+
const registeredHotkeys = new Map<string, string>();
12+
13+
/**
14+
* Register a global hotkey
15+
*/
16+
export function registerHotkey(hotkey: string, action: () => void): boolean {
17+
try {
18+
// Unregister existing hotkey if it exists
19+
if (registeredHotkeys.has(hotkey)) {
20+
globalShortcut.unregister(hotkey);
21+
registeredHotkeys.delete(hotkey);
22+
}
23+
24+
// Register new hotkey
25+
const success = globalShortcut.register(hotkey, action);
26+
if (success) {
27+
registeredHotkeys.set(hotkey, action.name);
28+
logger.info(`Registered hotkey: ${hotkey}`);
29+
} else {
30+
logger.error(`Failed to register hotkey: ${hotkey}`);
31+
}
32+
return success;
33+
} catch (error) {
34+
logger.error(`Error registering hotkey ${hotkey}:`, error);
35+
return false;
36+
}
37+
}
38+
39+
/**
40+
* Unregister a global hotkey
41+
*/
42+
export function unregisterHotkey(hotkey: string): boolean {
43+
try {
44+
globalShortcut.unregister(hotkey);
45+
registeredHotkeys.delete(hotkey);
46+
logger.info(`Unregistered hotkey: ${hotkey}`);
47+
return true;
48+
} catch (error) {
49+
logger.error(`Error unregistering hotkey ${hotkey}:`, error);
50+
return false;
51+
}
52+
}
53+
54+
/**
55+
* Get the current password paste hotkey from settings
56+
*/
57+
export function getPasswordPasteHotkey(): string {
58+
try {
59+
const userProfileStore = useUserProfileStore.getState();
60+
const activeProfile = userProfileStore.getActiveProfile();
61+
return (
62+
activeProfile?.settings?.hotkeys?.passwordPaste ||
63+
DEFAULT_PASSWORD_PASTE_HOTKEY
64+
);
65+
} catch (error) {
66+
logger.error("Failed to get password paste hotkey from settings:", error);
67+
return DEFAULT_PASSWORD_PASTE_HOTKEY;
68+
}
69+
}
70+
71+
/**
72+
* Set the password paste hotkey in settings
73+
*/
74+
export function setPasswordPasteHotkey(hotkey: string): boolean {
75+
try {
76+
const userProfileStore = useUserProfileStore.getState();
77+
const activeProfile = userProfileStore.getActiveProfile();
78+
79+
if (!activeProfile) {
80+
logger.error("No active profile found");
81+
return false;
82+
}
83+
84+
// Update profile settings
85+
const updatedSettings = {
86+
...activeProfile.settings,
87+
hotkeys: {
88+
...activeProfile.settings?.hotkeys,
89+
passwordPaste: hotkey,
90+
},
91+
};
92+
93+
userProfileStore.updateProfile(activeProfile.id, {
94+
settings: updatedSettings,
95+
});
96+
97+
logger.info(`Password paste hotkey updated to: ${hotkey}`);
98+
return true;
99+
} catch (error) {
100+
logger.error("Failed to set password paste hotkey:", error);
101+
return false;
102+
}
103+
}
104+
105+
/**
106+
* Initialize password paste hotkey
107+
*/
108+
export function initializePasswordPasteHotkey(): boolean {
109+
try {
110+
const hotkey = getPasswordPasteHotkey();
111+
112+
const action = async () => {
113+
try {
114+
// Import the password paste function directly
115+
const { pastePasswordForActiveTab } = await import(
116+
"./password-paste-handler"
117+
);
118+
const result = await pastePasswordForActiveTab();
119+
if (result.success) {
120+
logger.info("Password pasted successfully via hotkey");
121+
} else {
122+
logger.warn("Failed to paste password via hotkey:", result.error);
123+
}
124+
} catch (error) {
125+
logger.error("Error in password paste hotkey action:", error);
126+
}
127+
};
128+
129+
return registerHotkey(hotkey, action);
130+
} catch (error) {
131+
logger.error("Failed to initialize password paste hotkey:", error);
132+
return false;
133+
}
134+
}
135+
136+
/**
137+
* Update password paste hotkey
138+
*/
139+
export function updatePasswordPasteHotkey(newHotkey: string): boolean {
140+
try {
141+
const oldHotkey = getPasswordPasteHotkey();
142+
143+
// Unregister old hotkey
144+
unregisterHotkey(oldHotkey);
145+
146+
// Set new hotkey in settings
147+
const success = setPasswordPasteHotkey(newHotkey);
148+
if (!success) {
149+
return false;
150+
}
151+
152+
// Register new hotkey
153+
return initializePasswordPasteHotkey();
154+
} catch (error) {
155+
logger.error("Failed to update password paste hotkey:", error);
156+
return false;
157+
}
158+
}
159+
160+
/**
161+
* Get all registered hotkeys
162+
*/
163+
export function getRegisteredHotkeys(): Map<string, string> {
164+
return new Map(registeredHotkeys);
165+
}
166+
167+
/**
168+
* Cleanup all registered hotkeys
169+
*/
170+
export function cleanupHotkeys(): void {
171+
try {
172+
registeredHotkeys.forEach((_actionName, hotkey) => {
173+
globalShortcut.unregister(hotkey);
174+
logger.info(`Cleaned up hotkey: ${hotkey}`);
175+
});
176+
registeredHotkeys.clear();
177+
} catch (error) {
178+
logger.error("Error cleaning up hotkeys:", error);
179+
}
180+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { ipcMain } from "electron";
2+
import { createLogger } from "@vibe/shared-types";
3+
import {
4+
getPasswordPasteHotkey,
5+
updatePasswordPasteHotkey,
6+
getRegisteredHotkeys,
7+
} from "@/hotkey-manager";
8+
9+
const logger = createLogger("hotkey-control");
10+
11+
/**
12+
* Hotkey management IPC handlers
13+
*/
14+
15+
ipcMain.handle("hotkeys:get-password-paste", async () => {
16+
try {
17+
const hotkey = getPasswordPasteHotkey();
18+
return { success: true, hotkey };
19+
} catch (error) {
20+
logger.error("Failed to get password paste hotkey:", error);
21+
return { success: false, error: "Failed to get hotkey" };
22+
}
23+
});
24+
25+
ipcMain.handle("hotkeys:set-password-paste", async (_event, hotkey: string) => {
26+
try {
27+
const success = updatePasswordPasteHotkey(hotkey);
28+
return { success };
29+
} catch (error) {
30+
logger.error("Failed to set password paste hotkey:", error);
31+
return { success: false, error: "Failed to set hotkey" };
32+
}
33+
});
34+
35+
ipcMain.handle("hotkeys:get-registered", async () => {
36+
try {
37+
const hotkeys = getRegisteredHotkeys();
38+
return { success: true, hotkeys: Object.fromEntries(hotkeys) };
39+
} catch (error) {
40+
logger.error("Failed to get registered hotkeys:", error);
41+
return { success: false, error: "Failed to get hotkeys" };
42+
}
43+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Modal IPC handlers
3+
* Handles modal-related IPC events that are not handled by DialogManager
4+
*/
5+
6+
import { ipcMain } from "electron";
7+
import { createLogger } from "@vibe/shared-types";
8+
9+
const logger = createLogger("ipc-modals");
10+
11+
// Note: dialog:show-settings is handled by DialogManager directly
12+
// We only handle the modal closed events here
13+
14+
// Handle settings modal closed event
15+
ipcMain.on("app:settings-modal-closed", () => {
16+
logger.debug("Settings modal closed");
17+
18+
// Optional: You could add any cleanup logic here
19+
// For now, just acknowledge the event
20+
});
21+
22+
// Handle downloads modal closed event
23+
ipcMain.on("app:downloads-modal-closed", () => {
24+
logger.debug("Downloads modal closed");
25+
26+
// Optional: You could add any cleanup logic here
27+
// For now, just acknowledge the event
28+
});
29+
30+
logger.info("Modal IPC handlers registered");
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Settings</title>
6+
<!-- Security headers - relaxed for development -->
7+
<meta
8+
http-equiv="Content-Security-Policy"
9+
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' ws: wss: http: https:; font-src 'self' data:; object-src 'none'; frame-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'"
10+
/>
11+
<meta http-equiv="X-Content-Type-Options" content="nosniff" />
12+
<meta http-equiv="X-XSS-Protection" content="1; mode=block" />
13+
<meta
14+
http-equiv="Referrer-Policy"
15+
content="strict-origin-when-cross-origin"
16+
/>
17+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
18+
<meta name="description" content="Application Settings" />
19+
</head>
20+
21+
<body>
22+
<div id="root"></div>
23+
<script type="module" src="/src/settings-entry.tsx"></script>
24+
</body>
25+
</html>

0 commit comments

Comments
 (0)