Skip to content

Commit c418683

Browse files
committed
Function: Implement auto startup
1 parent eca85ab commit c418683

File tree

7 files changed

+303
-21
lines changed

7 files changed

+303
-21
lines changed

app/main.ts

Lines changed: 218 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-unused-vars */
2-
import {app, BrowserWindow, ipcMain, shell, dialog} from 'electron';
2+
import {app, BrowserWindow, ipcMain, shell, dialog, Tray, Menu, nativeImage} from 'electron';
33
import * as path from 'path';
44
import * as fs from 'fs';
55
import * as os from 'os';
@@ -8,11 +8,147 @@ import { execSync } from 'child_process';
88

99
// Main window reference
1010
let win: BrowserWindow | null = null;
11+
// Tray reference
12+
let tray: Tray | null = null;
13+
14+
// Settings
15+
let closeToTray = true;
1116

1217
// Check if app is running in development mode
1318
const args = process.argv.slice(1);
1419
const serve = args.some(val => val === '--serve');
1520

21+
/**
22+
* Create the system tray
23+
*/
24+
function createTray() {
25+
if (tray) {
26+
return;
27+
}
28+
29+
// Get appropriate icon based on platform
30+
const iconPath = path.join(__dirname, '..', 'dist', 'logos', 'favicon.256x256.png');
31+
const icon = nativeImage.createFromPath(iconPath);
32+
33+
tray = new Tray(icon);
34+
tray.setToolTip('TensorBlock Desktop');
35+
36+
const contextMenu = Menu.buildFromTemplate([
37+
{ label: 'Open TensorBlock', click: () => {
38+
win?.show();
39+
win?.setSkipTaskbar(false); // Show in taskbar
40+
}},
41+
{ type: 'separator' },
42+
{ label: 'Quit', click: () => { app.quit(); } }
43+
]);
44+
45+
tray.setContextMenu(contextMenu);
46+
47+
tray.on('click', () => {
48+
if (win) {
49+
if (win.isVisible()) {
50+
win.hide();
51+
win.setSkipTaskbar(true); // Hide from taskbar
52+
} else {
53+
win.show();
54+
win.setSkipTaskbar(false); // Show in taskbar
55+
}
56+
}
57+
});
58+
}
59+
60+
/**
61+
* Set or remove auto launch on system startup
62+
*/
63+
function setAutoLaunch(enable: boolean): boolean {
64+
try {
65+
if (process.platform === 'win32') {
66+
const appPath = app.getPath('exe');
67+
const regKey = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run';
68+
const appName = app.getName();
69+
70+
if (enable) {
71+
// Add to registry to enable auto launch
72+
execSync(`reg add ${regKey} /v ${appName} /t REG_SZ /d "${appPath}" /f`);
73+
} else {
74+
// Remove from registry to disable auto launch
75+
execSync(`reg delete ${regKey} /v ${appName} /f`);
76+
}
77+
return true;
78+
} else if (process.platform === 'darwin') {
79+
const appPath = app.getPath('exe');
80+
const loginItemSettings = app.getLoginItemSettings();
81+
82+
// Set login item settings for macOS
83+
app.setLoginItemSettings({
84+
openAtLogin: enable,
85+
path: appPath
86+
});
87+
88+
return app.getLoginItemSettings().openAtLogin === enable;
89+
} else if (process.platform === 'linux') {
90+
// For Linux, create or remove a .desktop file in autostart directory
91+
const desktopFilePath = path.join(os.homedir(), '.config', 'autostart', `${app.getName()}.desktop`);
92+
93+
if (enable) {
94+
// Create directory if it doesn't exist
95+
const autoStartDir = path.dirname(desktopFilePath);
96+
if (!fs.existsSync(autoStartDir)) {
97+
fs.mkdirSync(autoStartDir, { recursive: true });
98+
}
99+
100+
// Create .desktop file
101+
const desktopFileContent = `
102+
[Desktop Entry]
103+
Type=Application
104+
Exec=${app.getPath('exe')}
105+
Hidden=false
106+
NoDisplay=false
107+
X-GNOME-Autostart-enabled=true
108+
Name=${app.getName()}
109+
Comment=${app.getName()} startup script
110+
`;
111+
fs.writeFileSync(desktopFilePath, desktopFileContent);
112+
} else if (fs.existsSync(desktopFilePath)) {
113+
// Remove .desktop file
114+
fs.unlinkSync(desktopFilePath);
115+
}
116+
117+
return true;
118+
}
119+
120+
return false;
121+
} catch (error) {
122+
console.error('Error setting auto launch:', error);
123+
return false;
124+
}
125+
}
126+
127+
/**
128+
* Check if app is set to auto launch on system startup
129+
*/
130+
function getAutoLaunchEnabled(): boolean {
131+
try {
132+
if (process.platform === 'win32') {
133+
const regKey = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run';
134+
const appName = app.getName();
135+
136+
const output = execSync(`reg query ${regKey} /v ${appName} 2>nul`).toString();
137+
return output.includes(appName);
138+
} else if (process.platform === 'darwin') {
139+
return app.getLoginItemSettings().openAtLogin;
140+
} else if (process.platform === 'linux') {
141+
const desktopFilePath = path.join(os.homedir(), '.config', 'autostart', `${app.getName()}.desktop`);
142+
return fs.existsSync(desktopFilePath);
143+
}
144+
145+
return false;
146+
} catch (error) {
147+
// If command fails (e.g., key doesn't exist), auto launch is not enabled
148+
return false;
149+
}
150+
}
151+
16152
/**
17153
* Creates the main application window
18154
*/
@@ -29,8 +165,8 @@ function createWindow(): BrowserWindow {
29165
frame: false,
30166
fullscreenable: false,
31167
autoHideMenuBar: true,
32-
minWidth: 700,
33-
minHeight: 1000,
168+
minWidth: 800,
169+
minHeight: 700,
34170
webPreferences: {
35171
preload: path.join(__dirname, 'preload.js'),
36172
contextIsolation: true,
@@ -180,14 +316,66 @@ function createWindow(): BrowserWindow {
180316

181317
// Close application
182318
ipcMain.on('close-app', () => {
183-
app.quit();
319+
if (closeToTray) {
320+
win?.hide();
321+
win?.setSkipTaskbar(true);
322+
} else {
323+
app.quit();
324+
}
184325
});
185326

186327
// Open URL in default browser
187328
ipcMain.on('open-url', (event, url) => {
188329
shell.openExternal(url);
189330
});
190331

332+
// Auto-startup handlers
333+
ipcMain.handle('get-auto-launch', () => {
334+
return getAutoLaunchEnabled();
335+
});
336+
337+
ipcMain.handle('set-auto-launch', (event, enable) => {
338+
return setAutoLaunch(enable);
339+
});
340+
341+
// Tray handlers
342+
ipcMain.handle('set-close-to-tray', (event, enable) => {
343+
closeToTray = enable;
344+
return true;
345+
});
346+
347+
ipcMain.handle('get-close-to-tray', () => {
348+
return closeToTray;
349+
});
350+
351+
ipcMain.handle('set-startup-to-tray', (event, enable) => {
352+
// Store this preference for the next app start
353+
try {
354+
const configPath = path.join(app.getPath('userData'), 'config.json');
355+
let config = {};
356+
357+
if (fs.existsSync(configPath)) {
358+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
359+
}
360+
361+
config = { ...config, startupToTray: enable };
362+
fs.writeFileSync(configPath, JSON.stringify(config));
363+
return true;
364+
} catch (error) {
365+
console.error('Error saving startup to tray setting:', error);
366+
return false;
367+
}
368+
});
369+
370+
// Listen for window close event
371+
win.on('close', (e) => {
372+
if (closeToTray) {
373+
e.preventDefault();
374+
win?.hide();
375+
win?.setSkipTaskbar(true);
376+
}
377+
});
378+
191379
// Disable page refresh in production
192380
if (process.env.NODE_ENV === 'production') {
193381
win.webContents.on('before-input-event', (event, input) => {
@@ -293,7 +481,28 @@ try {
293481
// Initialize app when Electron is ready
294482
// Added delay to fix black background issue with transparent windows
295483
// See: https://github.com/electron/electron/issues/15947
296-
app.on('ready', () => setTimeout(createWindow, 400));
484+
app.on('ready', () => {
485+
setTimeout(() => {
486+
// Create window
487+
win = createWindow();
488+
489+
// Create tray
490+
createTray();
491+
492+
// Check if we should start minimized to tray
493+
try {
494+
const configPath = path.join(app.getPath('userData'), 'config.json');
495+
if (fs.existsSync(configPath)) {
496+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
497+
if (config.startupToTray) {
498+
win.hide();
499+
}
500+
}
501+
} catch (error) {
502+
console.error('Error reading config file:', error);
503+
}
504+
}, 400);
505+
});
297506

298507
// Quit when all windows are closed
299508
app.on('window-all-closed', () => {
@@ -303,7 +512,10 @@ try {
303512
// Re-create window if activated and no windows exist
304513
app.on('activate', () => {
305514
if (win === null) {
306-
createWindow();
515+
win = createWindow();
516+
} else {
517+
win.show();
518+
win.setSkipTaskbar(false); // Show in taskbar
307519
}
308520
});
309521
} catch (_e) {

app/preload.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,11 @@ contextBridge.exposeInMainWorld('electron', {
2929
saveFile: (fileBuffer: ArrayBuffer | string, fileName: string, fileType: string) =>
3030
ipcRenderer.invoke('save-file', { fileBuffer, fileName, fileType }),
3131
openFile: (filePath: string) => ipcRenderer.invoke('open-file', filePath),
32+
33+
// Auto-startup and tray functions
34+
getAutoLaunch: () => ipcRenderer.invoke('get-auto-launch'),
35+
setAutoLaunch: (enable: boolean) => ipcRenderer.invoke('set-auto-launch', enable),
36+
setCloseToTray: (enable: boolean) => ipcRenderer.invoke('set-close-to-tray', enable),
37+
getCloseToTray: () => ipcRenderer.invoke('get-close-to-tray'),
38+
setStartupToTray: (enable: boolean) => ipcRenderer.invoke('set-startup-to-tray', enable),
3239
});

src/components/pages/SettingsPage.tsx

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,31 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
6565
setHasApiKeyChanged(false);
6666
lastOpenedSettings.current = true;
6767

68-
// In a real implementation, we would load these from settings service
69-
// This is just for the UI prototype
70-
// setStartWithSystem(settings.startWithSystem || false);
71-
// setStartupToTray(settings.startupToTray || false);
72-
// setCloseToTray(settings.closeToTray || true);
73-
// setProxyMode(settings.proxyMode || 'system');
74-
// setSendErrorReports(settings.sendErrorReports || true);
68+
// Load general settings from settings service
69+
setStartWithSystem(settings.startWithSystem || false);
70+
setStartupToTray(settings.startupToTray || false);
71+
setCloseToTray(settings.closeToTray || true);
72+
setProxyMode(settings.proxyMode || 'system');
73+
setSendErrorReports(settings.sendErrorReports || true);
74+
75+
// Load current auto-start setting from system
76+
const checkAutoLaunch = async () => {
77+
if (window.electron && window.electron.getAutoLaunch) {
78+
const autoLaunchEnabled = await window.electron.getAutoLaunch();
79+
setStartWithSystem(autoLaunchEnabled);
80+
}
81+
};
82+
83+
// Load current close-to-tray setting
84+
const checkCloseToTray = async () => {
85+
if (window.electron && window.electron.getCloseToTray) {
86+
const closeToTrayEnabled = await window.electron.getCloseToTray();
87+
setCloseToTray(closeToTrayEnabled);
88+
}
89+
};
90+
91+
checkAutoLaunch();
92+
checkCloseToTray();
7593
}
7694

7795
if(!isOpen && lastOpenedSettings.current){
@@ -117,12 +135,24 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
117135
switch(key) {
118136
case 'startWithSystem':
119137
setStartWithSystem(value as boolean);
138+
// Apply auto-launch setting to system
139+
if (window.electron && window.electron.setAutoLaunch) {
140+
window.electron.setAutoLaunch(value as boolean);
141+
}
120142
break;
121143
case 'startupToTray':
122144
setStartupToTray(value as boolean);
145+
// Store startup-to-tray setting
146+
if (window.electron && window.electron.setStartupToTray) {
147+
window.electron.setStartupToTray(value as boolean);
148+
}
123149
break;
124150
case 'closeToTray':
125151
setCloseToTray(value as boolean);
152+
// Apply close-to-tray setting
153+
if (window.electron && window.electron.setCloseToTray) {
154+
window.electron.setCloseToTray(value as boolean);
155+
}
126156
break;
127157
case 'proxyMode':
128158
setProxyMode(value as 'system' | 'custom' | 'none');
@@ -150,13 +180,13 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
150180
// Update all settings in one go
151181
await settingsService.updateSettings({
152182
providers: providerSettings,
153-
enableWebSearch_Preview: useWebSearch
154-
// In a real implementation, we would save general settings here
155-
// startWithSystem,
156-
// startupToTray,
157-
// closeToTray,
158-
// proxyMode,
159-
// sendErrorReports
183+
enableWebSearch_Preview: useWebSearch,
184+
// Save general settings
185+
startWithSystem,
186+
startupToTray,
187+
closeToTray,
188+
proxyMode,
189+
sendErrorReports
160190
});
161191

162192
// Refresh models if API key has changed

src/components/settings/GeneralSettings.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useEffect } from 'react';
22
import { useTranslation } from 'react-i18next';
33

44
interface GeneralSettingsProps {
@@ -22,6 +22,19 @@ export const GeneralSettings: React.FC<GeneralSettingsProps> = ({
2222
}) => {
2323
const { t } = useTranslation();
2424
const [customProxyUrl, setCustomProxyUrl] = useState<string>('');
25+
const [isWindows, setIsWindows] = useState(false);
26+
27+
// Check if running on Windows platform
28+
useEffect(() => {
29+
const checkPlatform = async () => {
30+
if (window.electron && window.electron.getPlatform) {
31+
const platform = await window.electron.getPlatform();
32+
setIsWindows(platform === 'win32');
33+
}
34+
};
35+
36+
checkPlatform();
37+
}, []);
2538

2639
const handleProxyModeChange = (mode: 'system' | 'custom' | 'none') => {
2740
onSettingChange('proxyMode', mode);

0 commit comments

Comments
 (0)