You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
244 lines
8.6 KiB
244 lines
8.6 KiB
import type { CapacitorElectronConfig } from '@capacitor-community/electron';
|
|
import {
|
|
CapElectronEventEmitter,
|
|
CapacitorSplashScreen,
|
|
setupCapacitorElectronPlugins,
|
|
} from '@capacitor-community/electron';
|
|
import chokidar from 'chokidar';
|
|
import type { MenuItemConstructorOptions } from 'electron';
|
|
import { app, BrowserWindow, Menu, MenuItem, nativeImage, Tray, session } from 'electron';
|
|
import electronIsDev from 'electron-is-dev';
|
|
import electronServe from 'electron-serve';
|
|
import windowStateKeeper from 'electron-window-state';
|
|
import { join } from 'path';
|
|
|
|
// Define components for a watcher to detect when the webapp is changed so we can reload in Dev mode.
|
|
const reloadWatcher = {
|
|
debouncer: null,
|
|
ready: false,
|
|
watcher: null,
|
|
};
|
|
export function setupReloadWatcher(electronCapacitorApp: ElectronCapacitorApp): void {
|
|
reloadWatcher.watcher = chokidar
|
|
.watch(join(app.getAppPath(), 'app'), {
|
|
ignored: /[/\\]\./,
|
|
persistent: true,
|
|
})
|
|
.on('ready', () => {
|
|
reloadWatcher.ready = true;
|
|
})
|
|
.on('all', (_event, _path) => {
|
|
if (reloadWatcher.ready) {
|
|
clearTimeout(reloadWatcher.debouncer);
|
|
reloadWatcher.debouncer = setTimeout(async () => {
|
|
electronCapacitorApp.getMainWindow().webContents.reload();
|
|
reloadWatcher.ready = false;
|
|
clearTimeout(reloadWatcher.debouncer);
|
|
reloadWatcher.debouncer = null;
|
|
reloadWatcher.watcher = null;
|
|
setupReloadWatcher(electronCapacitorApp);
|
|
}, 1500);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Define our class to manage our app.
|
|
export class ElectronCapacitorApp {
|
|
private MainWindow: BrowserWindow | null = null;
|
|
private SplashScreen: CapacitorSplashScreen | null = null;
|
|
private TrayIcon: Tray | null = null;
|
|
private CapacitorFileConfig: CapacitorElectronConfig;
|
|
private TrayMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [
|
|
new MenuItem({ label: 'Quit App', role: 'quit' }),
|
|
];
|
|
private AppMenuBarMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [
|
|
{ role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' },
|
|
{ role: 'viewMenu' },
|
|
];
|
|
private mainWindowState;
|
|
private loadWebApp;
|
|
private customScheme: string;
|
|
|
|
constructor(
|
|
capacitorFileConfig: CapacitorElectronConfig,
|
|
trayMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[],
|
|
appMenuBarMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[]
|
|
) {
|
|
this.CapacitorFileConfig = capacitorFileConfig;
|
|
|
|
this.customScheme = this.CapacitorFileConfig.electron?.customUrlScheme ?? 'capacitor-electron';
|
|
|
|
if (trayMenuTemplate) {
|
|
this.TrayMenuTemplate = trayMenuTemplate;
|
|
}
|
|
|
|
if (appMenuBarMenuTemplate) {
|
|
this.AppMenuBarMenuTemplate = appMenuBarMenuTemplate;
|
|
}
|
|
|
|
// Setup our web app loader, this lets us load apps like react, vue, and angular without changing their build chains.
|
|
this.loadWebApp = electronServe({
|
|
directory: join(app.getAppPath(), 'app'),
|
|
scheme: this.customScheme,
|
|
});
|
|
}
|
|
|
|
// Helper function to load in the app.
|
|
private async loadMainWindow(thisRef: any) {
|
|
await thisRef.loadWebApp(thisRef.MainWindow);
|
|
}
|
|
|
|
// Expose the mainWindow ref for use outside of the class.
|
|
getMainWindow(): BrowserWindow {
|
|
return this.MainWindow;
|
|
}
|
|
|
|
getCustomURLScheme(): string {
|
|
return this.customScheme;
|
|
}
|
|
|
|
async init(): Promise<void> {
|
|
const icon = nativeImage.createFromPath(
|
|
join(app.getAppPath(), 'assets', process.platform === 'win32' ? 'appIcon.ico' : 'appIcon.png')
|
|
);
|
|
this.mainWindowState = windowStateKeeper({
|
|
defaultWidth: 1000,
|
|
defaultHeight: 800,
|
|
});
|
|
// Setup preload script path and construct our main window.
|
|
const preloadPath = join(app.getAppPath(), 'build', 'src', 'preload.js');
|
|
this.MainWindow = new BrowserWindow({
|
|
icon,
|
|
show: false,
|
|
x: this.mainWindowState.x,
|
|
y: this.mainWindowState.y,
|
|
width: this.mainWindowState.width,
|
|
height: this.mainWindowState.height,
|
|
webPreferences: {
|
|
nodeIntegration: true,
|
|
contextIsolation: true,
|
|
// Use preload to inject the electron varriant overrides for capacitor plugins.
|
|
// preload: join(app.getAppPath(), "node_modules", "@capacitor-community", "electron", "dist", "runtime", "electron-rt.js"),
|
|
preload: preloadPath,
|
|
},
|
|
});
|
|
this.mainWindowState.manage(this.MainWindow);
|
|
|
|
if (this.CapacitorFileConfig.backgroundColor) {
|
|
this.MainWindow.setBackgroundColor(this.CapacitorFileConfig.electron.backgroundColor);
|
|
}
|
|
|
|
// If we close the main window with the splashscreen enabled we need to destory the ref.
|
|
this.MainWindow.on('closed', () => {
|
|
if (this.SplashScreen?.getSplashWindow() && !this.SplashScreen.getSplashWindow().isDestroyed()) {
|
|
this.SplashScreen.getSplashWindow().close();
|
|
}
|
|
});
|
|
|
|
// When the tray icon is enabled, setup the options.
|
|
if (this.CapacitorFileConfig.electron?.trayIconAndMenuEnabled) {
|
|
this.TrayIcon = new Tray(icon);
|
|
this.TrayIcon.on('double-click', () => {
|
|
if (this.MainWindow) {
|
|
if (this.MainWindow.isVisible()) {
|
|
this.MainWindow.hide();
|
|
} else {
|
|
this.MainWindow.show();
|
|
this.MainWindow.focus();
|
|
}
|
|
}
|
|
});
|
|
this.TrayIcon.on('click', () => {
|
|
if (this.MainWindow) {
|
|
if (this.MainWindow.isVisible()) {
|
|
this.MainWindow.hide();
|
|
} else {
|
|
this.MainWindow.show();
|
|
this.MainWindow.focus();
|
|
}
|
|
}
|
|
});
|
|
this.TrayIcon.setToolTip(app.getName());
|
|
this.TrayIcon.setContextMenu(Menu.buildFromTemplate(this.TrayMenuTemplate));
|
|
}
|
|
|
|
// Setup the main manu bar at the top of our window.
|
|
Menu.setApplicationMenu(Menu.buildFromTemplate(this.AppMenuBarMenuTemplate));
|
|
|
|
// If the splashscreen is enabled, show it first while the main window loads then switch it out for the main window, or just load the main window from the start.
|
|
if (this.CapacitorFileConfig.electron?.splashScreenEnabled) {
|
|
this.SplashScreen = new CapacitorSplashScreen({
|
|
imageFilePath: join(
|
|
app.getAppPath(),
|
|
'assets',
|
|
this.CapacitorFileConfig.electron?.splashScreenImageName ?? 'splash.png'
|
|
),
|
|
windowWidth: 400,
|
|
windowHeight: 400,
|
|
});
|
|
this.SplashScreen.init(this.loadMainWindow, this);
|
|
} else {
|
|
this.loadMainWindow(this);
|
|
}
|
|
|
|
// Security
|
|
this.MainWindow.webContents.setWindowOpenHandler((details) => {
|
|
if (!details.url.includes(this.customScheme)) {
|
|
return { action: 'deny' };
|
|
} else {
|
|
return { action: 'allow' };
|
|
}
|
|
});
|
|
this.MainWindow.webContents.on('will-navigate', (event, _newURL) => {
|
|
if (!this.MainWindow.webContents.getURL().includes(this.customScheme)) {
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
|
|
// Link electron plugins into the system.
|
|
setupCapacitorElectronPlugins();
|
|
|
|
// When the web app is loaded we hide the splashscreen if needed and show the mainwindow.
|
|
this.MainWindow.webContents.on('dom-ready', () => {
|
|
if (this.CapacitorFileConfig.electron?.splashScreenEnabled) {
|
|
this.SplashScreen.getSplashWindow().hide();
|
|
}
|
|
if (!this.CapacitorFileConfig.electron?.hideMainWindowOnLaunch) {
|
|
this.MainWindow.show();
|
|
}
|
|
setTimeout(() => {
|
|
if (electronIsDev) {
|
|
this.MainWindow.webContents.openDevTools();
|
|
}
|
|
CapElectronEventEmitter.emit('CAPELECTRON_DeeplinkListenerInitialized', '');
|
|
}, 400);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Set a CSP up for our application based on the custom scheme
|
|
export function setupContentSecurityPolicy(customScheme: string): void {
|
|
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
|
callback({
|
|
responseHeaders: {
|
|
...details.responseHeaders,
|
|
'Content-Security-Policy': [
|
|
// Base CSP for both dev and prod
|
|
`default-src ${customScheme}://* 'unsafe-inline' data:;`,
|
|
// Allow Google Fonts
|
|
`style-src ${customScheme}://* 'unsafe-inline' https://fonts.googleapis.com;`,
|
|
`font-src ${customScheme}://* https://fonts.gstatic.com;`,
|
|
// Allow images and media
|
|
`img-src ${customScheme}://* data: https:;`,
|
|
// Allow connections to HTTPS resources
|
|
`connect-src ${customScheme}://* https:;`,
|
|
// Add dev-specific policies
|
|
...(electronIsDev ? [
|
|
`script-src ${customScheme}://* 'unsafe-inline' 'unsafe-eval' devtools://*;`,
|
|
`default-src ${customScheme}://* 'unsafe-inline' devtools://* 'unsafe-eval' data:;`
|
|
] : [])
|
|
].join(' ')
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|