- Add 'editMenu' role to AppMenuBarMenuTemplate in setup.ts and index.ts - Enables standard keyboard shortcuts (Cmd+C, Cmd+V, etc.) in Electron app - Fixes issue where copy/paste shortcuts were not working in text inputs - Maintains existing clipboard service functionality for programmatic operations Resolves keyboard shortcut functionality for better user experience in desktop app.
182 lines
6.3 KiB
TypeScript
182 lines
6.3 KiB
TypeScript
import type { CapacitorElectronConfig } from '@capacitor-community/electron';
|
|
import { getCapacitorElectronConfig, setupElectronDeepLinking } from '@capacitor-community/electron';
|
|
import type { MenuItemConstructorOptions } from 'electron';
|
|
import { app, MenuItem, ipcMain, dialog } from 'electron';
|
|
import electronIsDev from 'electron-is-dev';
|
|
import unhandled from 'electron-unhandled';
|
|
// import { autoUpdater } from 'electron-updater';
|
|
import { promises as fs } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } from './setup';
|
|
|
|
// Use Electron's built-in single instance lock
|
|
const gotTheLock = app.requestSingleInstanceLock();
|
|
|
|
if (!gotTheLock) {
|
|
console.log('[Electron] Another instance is already running. Exiting immediately...');
|
|
process.exit(0);
|
|
}
|
|
|
|
// Graceful handling of unhandled errors.
|
|
unhandled({
|
|
logger: (error) => {
|
|
// Suppress EPIPE errors which are common in AppImages due to console output issues
|
|
if (error.message && error.message.includes('EPIPE')) {
|
|
return; // Don't log EPIPE errors
|
|
}
|
|
console.error('Unhandled error:', error);
|
|
}
|
|
});
|
|
|
|
// Handle EPIPE errors on stdout/stderr to prevent crashes
|
|
process.stdout.on('error', (err) => {
|
|
if (err.code === 'EPIPE') {
|
|
// Ignore EPIPE errors on stdout
|
|
return;
|
|
}
|
|
console.error('stdout error:', err);
|
|
});
|
|
|
|
process.stderr.on('error', (err) => {
|
|
if (err.code === 'EPIPE') {
|
|
// Ignore EPIPE errors on stderr
|
|
return;
|
|
}
|
|
console.error('stderr error:', err);
|
|
});
|
|
|
|
// Define our menu templates (these are optional)
|
|
const trayMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [new MenuItem({ label: 'Quit App', role: 'quit' })];
|
|
const appMenuBarMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [
|
|
{ role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' },
|
|
{ role: 'editMenu' },
|
|
{ role: 'viewMenu' },
|
|
];
|
|
|
|
// Get Config options from capacitor.config
|
|
const capacitorFileConfig: CapacitorElectronConfig = getCapacitorElectronConfig();
|
|
|
|
// Initialize our app. You can pass menu templates into the app here.
|
|
// const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig);
|
|
const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig, trayMenuTemplate, appMenuBarMenuTemplate);
|
|
|
|
// If deeplinking is enabled then we will set it up here.
|
|
if (capacitorFileConfig.electron?.deepLinkingEnabled) {
|
|
setupElectronDeepLinking(myCapacitorApp, {
|
|
customProtocol: capacitorFileConfig.electron.deepLinkingCustomProtocol ?? 'mycapacitorapp',
|
|
});
|
|
}
|
|
|
|
// If we are in Dev mode, use the file watcher components.
|
|
if (electronIsDev) {
|
|
setupReloadWatcher(myCapacitorApp);
|
|
}
|
|
|
|
// Configure auto-updater
|
|
// DISABLED: Auto-updates not supported on Gitea hosting
|
|
// autoUpdater.on('error', (error) => {
|
|
// console.log('Auto-updater error (suppressed):', error.message);
|
|
// // Don't show error dialogs for update check failures
|
|
// });
|
|
|
|
// Run Application
|
|
(async () => {
|
|
// Wait for electron app to be ready.
|
|
await app.whenReady();
|
|
// Security - Set Content-Security-Policy based on whether or not we are in dev mode.
|
|
setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme());
|
|
// Initialize our app, build windows, and load content.
|
|
await myCapacitorApp.init();
|
|
|
|
// Handle second instance launch (focus existing window and show dialog)
|
|
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
|
console.log('[Electron] Second instance attempted to launch');
|
|
|
|
// Someone tried to run a second instance, we should focus our window instead
|
|
const mainWindow = myCapacitorApp.getMainWindow();
|
|
if (mainWindow) {
|
|
if (mainWindow.isMinimized()) {
|
|
mainWindow.restore();
|
|
}
|
|
mainWindow.focus();
|
|
|
|
// Show user-friendly dialog to inform about single instance
|
|
dialog.showMessageBox(mainWindow, {
|
|
type: 'info',
|
|
title: 'TimeSafari Already Running',
|
|
message: 'TimeSafari is already running in another window.',
|
|
detail: 'Please use this existing TimeSafari window instead of opening a new one.',
|
|
buttons: ['OK']
|
|
}).catch((error) => {
|
|
console.error('[Electron] Error showing dialog:', error);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Auto-updates disabled - not supported on Gitea hosting
|
|
// Only check for updates in production builds, not in development or AppImage
|
|
// if (!electronIsDev && !process.env.APPIMAGE) {
|
|
// try {
|
|
// autoUpdater.checkForUpdatesAndNotify();
|
|
// } catch (error) {
|
|
// console.log('Update check failed (suppressed):', error);
|
|
// }
|
|
// }
|
|
})();
|
|
|
|
// Handle when all of our windows are close (platforms have their own expectations).
|
|
app.on('window-all-closed', function () {
|
|
// On OS X it is common for applications and their menu bar
|
|
// to stay active until the user quits explicitly with Cmd + Q
|
|
if (process.platform !== 'darwin') {
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
// When the dock icon is clicked.
|
|
app.on('activate', async function () {
|
|
// On OS X it's common to re-create a window in the app when the
|
|
// dock icon is clicked and there are no other windows open.
|
|
if (myCapacitorApp.getMainWindow().isDestroyed()) {
|
|
await myCapacitorApp.init();
|
|
}
|
|
});
|
|
|
|
// Place all ipc or other electron api calls and custom functionality under this line
|
|
|
|
/**
|
|
* IPC Handler for exporting data to the user's Downloads folder.
|
|
*
|
|
* This provides a secure, native way to save files directly to the Downloads
|
|
* directory using the main process's file system access.
|
|
*
|
|
* @param fileName - The name of the file to save (including extension)
|
|
* @param data - The data to write to the file (string or buffer)
|
|
* @returns Promise<{success: boolean, path?: string, error?: string}>
|
|
*/
|
|
ipcMain.handle('export-data-to-downloads', async (_event, fileName: string, data: string) => {
|
|
try {
|
|
// Get the user's Downloads directory path
|
|
const downloadsDir = app.getPath('downloads');
|
|
const filePath = join(downloadsDir, fileName);
|
|
|
|
// Write the file to the Downloads directory
|
|
await fs.writeFile(filePath, data, 'utf-8');
|
|
|
|
console.log(`[Electron Main] File exported successfully: ${filePath}`);
|
|
|
|
return {
|
|
success: true,
|
|
path: filePath
|
|
};
|
|
} catch (error) {
|
|
console.error(`[Electron Main] File export failed:`, error);
|
|
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
};
|
|
}
|
|
});
|