Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions electron/appMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { app, Menu, shell } from 'electron';
import type { BrowserWindow } from 'electron';

interface ApplicationMenuOptions {
getMainWindow: () => BrowserWindow | null;
}

function focusMainWindow(getMainWindow: () => BrowserWindow | null): BrowserWindow | null {
const win = getMainWindow();
if (!win) return null;

if (win.isMinimized()) {
win.restore();
}
if (!win.isVisible()) {
win.show();
}
win.focus();
return win;
}

function openNetworkSettings(getMainWindow: () => BrowserWindow | null): void {
const win = focusMainWindow(getMainWindow);
win?.webContents.send('network-settings:open');
}

/**
* Standard app menu with a discoverable Network Settings entry.
* Keeps File/Edit/View/Window/Help (and Quit on all platforms) so this is not
* a regression vs Electron's default menu for users who never open proxy settings.
*/
export function buildApplicationMenu({ getMainWindow }: ApplicationMenuOptions): void {
const isMac = process.platform === 'darwin';

const networkSettingsItem: Electron.MenuItemConstructorOptions = {
label: 'Network Settings…',
accelerator: 'CmdOrCtrl+,',
click: () => openNetworkSettings(getMainWindow)
};

const template: Electron.MenuItemConstructorOptions[] = [
...(isMac
? [{
label: app.name,
submenu: [
{ role: 'about' as const },
{ type: 'separator' as const },
networkSettingsItem,
{ type: 'separator' as const },
{ role: 'services' as const },
{ type: 'separator' as const },
{ role: 'hide' as const },
{ role: 'hideOthers' as const },
{ role: 'unhide' as const },
{ type: 'separator' as const },
{ role: 'quit' as const }
]
}]
: []),
{
label: 'File',
submenu: [
...(isMac ? [] : [networkSettingsItem, { type: 'separator' as const }]),
isMac ? { role: 'close' as const } : { role: 'quit' as const }
]
},
{
label: 'Edit',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The Network menu action only does getMainWindow()?.webContents.send('network-settings:open'). If the window is minimized/hidden, or React has not yet registered the listener, the event is easy to miss and no window is brought forward.

Suggestion: Restore/show/focus the main window before send; optionally buffer a “pending open network settings” flag in main until the renderer acknowledges readiness.

submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
...(isMac
? [
{ role: 'pasteAndMatchStyle' as const },
{ role: 'delete' as const },
{ role: 'selectAll' as const },
{ type: 'separator' as const },
{
label: 'Speech',
submenu: [
{ role: 'startSpeaking' as const },
{ role: 'stopSpeaking' as const }
]
}
]
: [
{ role: 'delete' as const },
{ type: 'separator' as const },
{ role: 'selectAll' as const }
])
]
},
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
},
{
label: 'Window',
submenu: [
{ role: 'minimize' },
{ role: 'zoom' },
...(isMac
? [
{ type: 'separator' as const },
{ role: 'front' as const },
{ type: 'separator' as const },
{ role: 'window' as const }
]
: [
{ role: 'close' as const }
])
]
},
{
label: 'Help',
submenu: [
{
label: 'BSV Desktop on GitHub',
click: async () => {
await shell.openExternal('https://github.com/bsv-blockchain/bsv-desktop');
}
}
]
}
];

Menu.setApplicationMenu(Menu.buildFromTemplate(template));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Menu.setApplicationMenu replaces Electron’s default menu. On Windows/Linux the new template has no Quit/Exit item and no standard File menu, so users lose the usual menu path to quit (they must close the window). macOS still has Quit under the app menu.

Suggestion: Add a File (or app) menu on non-darwin with { role: 'quit' }, or append a Quit item under Settings/Window for Windows/Linux.

}
81 changes: 61 additions & 20 deletions electron/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron';
import { app, BrowserWindow, ipcMain, dialog, shell, session } from 'electron';
import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
import fs from 'fs';
import { startHttpServer } from './httpServer.js';
import { buildApplicationMenu } from './appMenu.js';
import { applyPersistedProxySettings, registerNetworkIpc } from './networkSettings.js';

const require = createRequire(import.meta.url);

Expand Down Expand Up @@ -49,6 +51,33 @@ const __dirname = path.dirname(__filename);

let mainWindow: BrowserWindow | null = null;
let httpServerCleanup: (() => Promise<void>) | null = null;
let cleanupStarted = false;

async function cleanupBeforeExit(): Promise<void> {
if (cleanupStarted) return;
cleanupStarted = true;

if (storageManager) {
try {
await storageManager.cleanup();
} catch (error) {
console.error('Failed to clean up storage manager before exit:', error);
} finally {
storageManager = null;
}
}

if (httpServerCleanup) {
const cleanup = httpServerCleanup;
httpServerCleanup = null;

try {
await cleanup();
} catch (error) {
console.error('Failed to clean up HTTP server before exit:', error);
}
}
}

// Store previous focused app on macOS
let prevBundleId: string | null = null;
Expand Down Expand Up @@ -171,6 +200,13 @@ function isSafeExternalUrl(url: string): boolean {

// ===== IPC Handlers =====

registerNetworkIpc({
hasActiveMonitorWorkers: () => {
// storageManager is only set after first storage use; no workers if never loaded
return Boolean(storageManager?.hasActiveMonitorWorkers?.());
}
});

// Check if window is focused
ipcMain.handle('is-focused', () => {
return mainWindow?.isFocused() ?? false;
Expand Down Expand Up @@ -406,8 +442,9 @@ ipcMain.handle('proxy-fetch-manifest', async (_event, url: string) => {
throw new Error('Only manifest.json files are allowed');
}

const fetch = (await import('node-fetch')).default;
const response = await fetch(url, {
// Use the Chromium session so manifest fetches honor session proxy settings
// (node-fetch would bypass session.setProxy).
const response = await session.defaultSession.fetch(url, {
headers: {
'User-Agent': 'bsv-desktop-electron/1.0',
'Accept': 'application/json, */*;q=0.8'
Expand Down Expand Up @@ -444,6 +481,17 @@ ipcMain.handle('proxy-fetch-manifest', async (_event, url: string) => {
}
});

// Process exits in this handler; the invoke Promise is not observed by the renderer.
ipcMain.handle('app:restart', async () => {
try {
await cleanupBeforeExit();
} catch (error) {
console.error('App restart cleanup failed:', error);
}
app.relaunch();
app.exit(0);
});

// Forward HTTP requests to renderer
ipcMain.on('http-response', (_event, response) => {
if (mainWindow) {
Expand Down Expand Up @@ -651,6 +699,13 @@ app.whenReady().then(async () => {
app.commandLine.appendSwitch('--disable-web-security');
}

try {
await applyPersistedProxySettings();
} catch (error) {
console.error('[Startup] Failed to apply persisted proxy settings, continuing startup without them:', error);
}

buildApplicationMenu({ getMainWindow: () => mainWindow });
createWindow();

// Start HTTPS server on port 2121
Expand All @@ -670,31 +725,17 @@ app.whenReady().then(async () => {
});

app.on('window-all-closed', async () => {
// Cleanup storage connections
if (storageManager) {
await storageManager.cleanup();
}

if (httpServerCleanup) {
await httpServerCleanup();
}
await cleanupBeforeExit();

app.quit();
app.quit();
});

app.on('before-quit', async (event) => {
// Prevent default quit to perform async cleanup
if (storageManager || httpServerCleanup) {
event.preventDefault();

// Cleanup storage connections
if (storageManager) {
await storageManager.cleanup();
}

if (httpServerCleanup) {
await httpServerCleanup();
}
await cleanupBeforeExit();

// Now actually quit
app.exit(0);
Expand Down
Loading
Loading