mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
* fix(electron): show the main window once its document has loaded; enforce J1 IPC and mutation counters Re-lands #1788, which merged into #1782's branch after #1782 had already reached master, so none of it is on master. The hidden main window was shown on ready-to-show only. On Linux under X11, when the startup scripts run before the window's first frame, the next frame comes about a second later: nothing is on screen and the splash's requestAnimationFrame waits, so J1's first card came ~940 ms after load instead of ~480 ms in most runner launches (18 bridge calls / 1,031-1,033 DOM mutations instead of 15 / 558). The window is now shown at ready-to-show or the main frame's did-finish-load, whichever comes first, with the splash colour as its background so showing before the first paint does not flash. The journey gate keeps the app's did-finish-load listeners away from its about:blank detour, as it already does for ready-to-show. Three dispatched runs on this branch (37192092882, 37192097790, 37192103151) read 15 calls and 558 mutations in all 18 iterations, stable: true. Both become baselines (slack 0), and the Performance journeys job checks them with check-journey-ratchet.mjs --only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(perf): record the evidence PR of the J1 runtime baselines Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: 4gray <fourgray@proton.me>
727 lines
26 KiB
TypeScript
727 lines
26 KiB
TypeScript
import { app, BrowserWindow, Menu, screen, session, shell } from 'electron';
|
|
import {
|
|
ElectronBridgeWindowState,
|
|
WINDOW_STATE_CHANGED,
|
|
} from '@iptvnator/shared/interfaces';
|
|
import { join, resolve } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import { rendererAppName, rendererAppPort } from './constants';
|
|
import {
|
|
isPerformanceCaptureEnabled,
|
|
isRendererConsoleTraceEnabled,
|
|
isSqlStatementCountEnabled,
|
|
isWindowTraceEnabled,
|
|
performanceCounters,
|
|
trace,
|
|
traceStartupPhase,
|
|
} from './services/debug-trace';
|
|
import {
|
|
MAIN_WINDOW_BACKGROUND_COLOR,
|
|
showMainWindowWhenLoaded,
|
|
} from './services/main-window-first-show';
|
|
import { attachMainWindowPerformanceCounters } from './services/performance-counters';
|
|
import {
|
|
STARTUP_WINDOW_MODE,
|
|
store,
|
|
WINDOW_BOUNDS,
|
|
} from './services/store.service';
|
|
import { isFrameCopyRuntimeUsable } from './services/embedded-mpv-frame-copy-platform.util';
|
|
import {
|
|
attachZoomLevelPersistence,
|
|
persistZoomLevel,
|
|
} from './services/window-zoom-level';
|
|
import {
|
|
attachRendererReloadFallback,
|
|
resolveRoutedRendererUrl,
|
|
restoreRendererRoute,
|
|
} from './services/renderer-reload-fallback';
|
|
import { isEmbeddedMpvFeatureEnabled } from './services/embedded-mpv-runtime-policy.util';
|
|
import {
|
|
FULLSCREEN_LAUNCH_SWITCH,
|
|
resolveStartupWindowMode,
|
|
} from './services/startup-window-mode';
|
|
import {
|
|
requestFullScreen,
|
|
trackNativeFullScreen,
|
|
} from './services/native-fullscreen-transitions';
|
|
|
|
const externalBrowserProtocols = new Set(['http:', 'https:']);
|
|
const trustedDevRendererHosts = new Set([
|
|
'localhost',
|
|
'127.0.0.1',
|
|
'[::1]',
|
|
'::1',
|
|
]);
|
|
|
|
function parseUrl(url: string): URL | null {
|
|
try {
|
|
return new URL(url);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getPackagedRendererIndexPath(): string {
|
|
return resolve(__dirname, '..', rendererAppName, 'index.html');
|
|
}
|
|
|
|
function getFilePathFromUrl(url: URL): string | null {
|
|
try {
|
|
return fileURLToPath(url);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function isExternalBrowserUrl(url: string): boolean {
|
|
const parsedUrl = parseUrl(url);
|
|
return Boolean(
|
|
parsedUrl && externalBrowserProtocols.has(parsedUrl.protocol)
|
|
);
|
|
}
|
|
|
|
export function isTrustedRendererNavigationUrl(
|
|
url: string,
|
|
isDevelopmentMode: boolean,
|
|
packagedRendererIndexPath = getPackagedRendererIndexPath()
|
|
): boolean {
|
|
const parsedUrl = parseUrl(url);
|
|
|
|
if (!parsedUrl) {
|
|
return false;
|
|
}
|
|
|
|
if (parsedUrl.protocol === 'file:') {
|
|
const filePath = getFilePathFromUrl(parsedUrl);
|
|
|
|
if (isDevelopmentMode || !filePath) {
|
|
return false;
|
|
}
|
|
|
|
return resolve(filePath) === resolve(packagedRendererIndexPath);
|
|
}
|
|
|
|
if (!isDevelopmentMode) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
parsedUrl.protocol === 'http:' &&
|
|
trustedDevRendererHosts.has(parsedUrl.hostname) &&
|
|
parsedUrl.port === String(rendererAppPort)
|
|
);
|
|
}
|
|
|
|
export function getMainWindowWebPreferences(): Electron.BrowserWindowConstructorOptions['webPreferences'] {
|
|
// The frame-copy embedded MPV experiment needs the preload script to
|
|
// load the shm frame-reader native addon, which the renderer sandbox
|
|
// forbids. Only that opt-in flag relaxes the sandbox; context isolation
|
|
// and nodeIntegration:false stay on either way, so page code never
|
|
// gains Node access. Revisit before the engine can become a default.
|
|
const frameCopyExperiment =
|
|
isEmbeddedMpvFeatureEnabled() &&
|
|
['1', 'true', 'yes', 'on'].includes(
|
|
(process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY ?? '')
|
|
.trim()
|
|
.toLowerCase()
|
|
) && isFrameCopyRuntimeUsable();
|
|
return {
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
sandbox: !frameCopyExperiment,
|
|
webSecurity: true,
|
|
// Chromium's default. A hidden or minimized window must report
|
|
// `document.hidden` so idle timers can pause and the playback
|
|
// keep-awake gate can release the display; Chromium itself keeps
|
|
// audible media and picture-in-picture running at full rate.
|
|
backgroundThrottling: true,
|
|
preload: join(__dirname, 'main.preload.js'),
|
|
};
|
|
}
|
|
|
|
export async function clearElectronServiceWorkerStorage(
|
|
electronSession: Pick<Electron.Session, 'clearStorageData'> = session.defaultSession
|
|
): Promise<void> {
|
|
try {
|
|
await electronSession.clearStorageData({
|
|
storages: ['serviceworkers', 'cachestorage'],
|
|
});
|
|
|
|
traceStartupPhase('electron-service-worker-storage:cleared');
|
|
} catch (error) {
|
|
console.warn('Failed to clear Electron service worker storage:', error);
|
|
|
|
traceStartupPhase(
|
|
'electron-service-worker-storage:clear-failed',
|
|
() => error
|
|
);
|
|
}
|
|
}
|
|
|
|
function attachWindowTrace(mainWindow: Electron.BrowserWindow): void {
|
|
if (!isWindowTraceEnabled()) {
|
|
return;
|
|
}
|
|
|
|
const webContents = mainWindow.webContents;
|
|
|
|
trace('window', 'created', {
|
|
id: mainWindow.id,
|
|
});
|
|
|
|
mainWindow.on('unresponsive', () => {
|
|
trace('window', 'unresponsive', {
|
|
id: mainWindow.id,
|
|
url: webContents.getURL(),
|
|
});
|
|
});
|
|
mainWindow.on('responsive', () => {
|
|
trace('window', 'responsive', {
|
|
id: mainWindow.id,
|
|
url: webContents.getURL(),
|
|
});
|
|
});
|
|
|
|
webContents.on('did-start-loading', () => {
|
|
trace('window', 'did-start-loading', {
|
|
id: mainWindow.id,
|
|
url: webContents.getURL(),
|
|
});
|
|
});
|
|
webContents.on('dom-ready', () => {
|
|
trace('window', 'dom-ready', {
|
|
id: mainWindow.id,
|
|
url: webContents.getURL(),
|
|
});
|
|
});
|
|
webContents.on('did-finish-load', () => {
|
|
trace('window', 'did-finish-load', {
|
|
id: mainWindow.id,
|
|
url: webContents.getURL(),
|
|
});
|
|
});
|
|
webContents.on(
|
|
'did-fail-load',
|
|
(_event, errorCode, errorDescription, validatedURL) => {
|
|
trace('window', 'did-fail-load', {
|
|
errorCode,
|
|
errorDescription,
|
|
id: mainWindow.id,
|
|
validatedURL,
|
|
});
|
|
}
|
|
);
|
|
webContents.on('did-navigate', (_event, url) => {
|
|
trace('window', 'did-navigate', {
|
|
id: mainWindow.id,
|
|
url,
|
|
});
|
|
});
|
|
webContents.on('render-process-gone', (_event, details) => {
|
|
trace('window', 'render-process-gone', {
|
|
details,
|
|
id: mainWindow.id,
|
|
url: webContents.getURL(),
|
|
});
|
|
});
|
|
|
|
if (isRendererConsoleTraceEnabled()) {
|
|
webContents.on(
|
|
'console-message',
|
|
(_event, level, message, line, sourceId) => {
|
|
trace('renderer-console', 'message', {
|
|
level,
|
|
line,
|
|
message,
|
|
sourceId,
|
|
});
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
export default class App {
|
|
// Keep a global reference of the window object, if you don't, the window will
|
|
// be closed automatically when the JavaScript object is garbage collected.
|
|
static mainWindow: Electron.BrowserWindow | null = null;
|
|
private static readonly mainWindowListeners: Array<
|
|
(mainWindow: Electron.BrowserWindow) => void
|
|
> = [];
|
|
static application: Electron.App;
|
|
static BrowserWindow;
|
|
private static loadedMainWindow: Electron.BrowserWindow | null = null;
|
|
private static mainWindowLoadPromise: Promise<void> | null = null;
|
|
/**
|
|
* Whether the `--fullscreen` launch switch has already shaped a window.
|
|
* See initMainWindow: the switch is consumed by the first window so a
|
|
* window re-created later in the same process (macOS Dock) follows the
|
|
* stored setting instead.
|
|
*/
|
|
private static launchFullscreenSwitchConsumed = false;
|
|
private static rendererLoadingEnabled = false;
|
|
|
|
private static shouldOpenDevTools() {
|
|
return process.env.ELECTRON_OPEN_DEVTOOLS === '1';
|
|
}
|
|
|
|
public static isDevelopmentMode() {
|
|
// First check ELECTRON_IS_DEV environment variable (used by E2E tests)
|
|
// This allows E2E tests to run in production mode without packaging
|
|
if ('ELECTRON_IS_DEV' in process.env) {
|
|
return parseInt(process.env.ELECTRON_IS_DEV, 10) === 1;
|
|
}
|
|
// Fall back to Electron's built-in app.isPackaged
|
|
// This is the most reliable way to detect if the app is packaged
|
|
return !app.isPackaged;
|
|
}
|
|
|
|
private static onWindowAllClosed() {
|
|
if (process.platform !== 'darwin') {
|
|
App.application.quit();
|
|
}
|
|
}
|
|
|
|
private static onClose() {
|
|
// Dereference the window object, usually you would store windows
|
|
// in an array if your app supports multi windows, this is the time
|
|
// when you should delete the corresponding element.
|
|
App.mainWindow = null;
|
|
}
|
|
|
|
private static startMainWindowLoad(): void {
|
|
void App.loadMainWindow().catch((error) => {
|
|
console.error('Failed to load main window:', error);
|
|
});
|
|
}
|
|
|
|
private static onReady() {
|
|
// This method will be called when Electron has finished
|
|
// initialization and is ready to create browser windows.
|
|
// Some APIs can only be used after this event occurs.
|
|
if (rendererAppName) {
|
|
App.initMainWindow();
|
|
if (App.rendererLoadingEnabled) {
|
|
App.startMainWindowLoad();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Registers a listener that needs the current main window, and re-runs it
|
|
* whenever a new one is created.
|
|
*
|
|
* The main window is not created once per process: on macOS the window can
|
|
* be closed and rebuilt (dock `activate`, or a second launch handed over by
|
|
* the single-instance guard) while the process lives on. Anything that
|
|
* caches the window — `download-broadcast`'s module-level reference, for
|
|
* one — would otherwise keep pointing at a destroyed window and silently
|
|
* stop delivering to the renderer. Fires immediately when a window already
|
|
* exists, so callers registering after startup do not miss the first one.
|
|
*/
|
|
static onMainWindowCreated(
|
|
listener: (mainWindow: Electron.BrowserWindow) => void
|
|
): void {
|
|
App.mainWindowListeners.push(listener);
|
|
|
|
if (App.mainWindow && !App.mainWindow.isDestroyed()) {
|
|
listener(App.mainWindow);
|
|
}
|
|
}
|
|
|
|
private static notifyMainWindowCreated(
|
|
mainWindow: Electron.BrowserWindow
|
|
): void {
|
|
for (const listener of App.mainWindowListeners) {
|
|
listener(mainWindow);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Brings the app back to a windowed state, re-creating the main window if
|
|
* it is gone.
|
|
*
|
|
* On macOS closing the last window deliberately keeps the process alive
|
|
* (`onWindowAllClosed`), so this is the recovery path for both the dock
|
|
* `activate` event and a second launch that the single-instance guard
|
|
* hands over to this process.
|
|
*/
|
|
static ensureMainWindow() {
|
|
if (App.mainWindow === null) {
|
|
App.onReady();
|
|
}
|
|
if (App.rendererLoadingEnabled) {
|
|
App.startMainWindowLoad();
|
|
}
|
|
}
|
|
|
|
private static onActivate() {
|
|
// On macOS it's common to re-create a window in the app when the
|
|
// dock icon is clicked and there are no other windows open.
|
|
App.ensureMainWindow();
|
|
}
|
|
|
|
private static handleRendererNavigation(
|
|
event: Electron.Event,
|
|
url: string
|
|
): void {
|
|
if (isTrustedRendererNavigationUrl(url, App.isDevelopmentMode())) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
// A renderer-initiated reload (`location.reload()`, e.g. the
|
|
// settings unsaved-changes guard) on an in-app route arrives here as
|
|
// a routed file:// URL with no file behind it. Send it straight to
|
|
// the packaged index with that route instead of cancelling it.
|
|
if (!App.isDevelopmentMode() && App.mainWindow) {
|
|
const rendererIndexPath = getPackagedRendererIndexPath();
|
|
const route = resolveRoutedRendererUrl(url, rendererIndexPath);
|
|
|
|
if (route !== null) {
|
|
restoreRendererRoute(App.mainWindow, rendererIndexPath, route);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (isExternalBrowserUrl(url)) {
|
|
shell.openExternal(url);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide the native title bar on every desktop platform. macOS keeps the
|
|
* system traffic lights (overlay), while Windows/Linux rely on the
|
|
* renderer-drawn window controls (`app-window-controls`) wired up via the
|
|
* WINDOW:* IPC channels. `frame` stays untouched so native resize borders
|
|
* and snapping keep working.
|
|
*/
|
|
private static getPlatformTitleBarOptions(): Electron.BrowserWindowConstructorOptions {
|
|
if (process.platform === 'darwin') {
|
|
return {
|
|
titleBarStyle: 'hidden',
|
|
titleBarOverlay: true,
|
|
trafficLightPosition: { x: 16, y: 20 },
|
|
};
|
|
}
|
|
|
|
return { titleBarStyle: 'hidden' };
|
|
}
|
|
|
|
private static attachWindowStateEvents(win: Electron.BrowserWindow): void {
|
|
// Only Windows/Linux render custom window controls that subscribe
|
|
// to these pushes; macOS keeps the native traffic lights, so
|
|
// sending state updates there would be dead IPC traffic.
|
|
if (process.platform === 'darwin') {
|
|
return;
|
|
}
|
|
|
|
// Window state is never re-read at event time: on Windows both
|
|
// isFullScreen() and isMaximized() can still report the
|
|
// pre-transition value while the matching event fires (notably for
|
|
// HTML-element fullscreen, i.e. the video player). Since the
|
|
// renderer replaces both flags on every push and no later event
|
|
// corrects a stale one, polling left the controls hidden forever
|
|
// after leaving fullscreen — and, for the companion flag, the
|
|
// maximize/restore glyph stuck on the wrong icon.
|
|
//
|
|
// Instead the state is seeded once here (window creation, so no
|
|
// transition is in flight) and each event patches only the flag it
|
|
// names.
|
|
const state: ElectronBridgeWindowState = {
|
|
isMaximized: win.isMaximized(),
|
|
isFullScreen: win.isFullScreen(),
|
|
};
|
|
|
|
// Native (OS-level) and HTML-element fullscreen are tracked apart
|
|
// and OR-ed into the pushed flag. Electron remembers when the window
|
|
// was already natively fullscreen before the player entered HTML
|
|
// fullscreen and then leaves ONLY the HTML state on exit — no
|
|
// 'leave-full-screen' fires and the window stays fullscreen. A single
|
|
// flag cleared by 'leave-html-full-screen' would un-hide the window
|
|
// controls over a window that is still fullscreen, which a fullscreen
|
|
// launch or F11 followed by the player's F → Esc makes routine.
|
|
const fullscreen = { native: state.isFullScreen, html: false };
|
|
|
|
const push = (patch: Partial<ElectronBridgeWindowState>) => {
|
|
Object.assign(state, patch);
|
|
|
|
if (win.isDestroyed()) {
|
|
return;
|
|
}
|
|
|
|
// A copy per push: the renderer must not observe later
|
|
// mutations of the tracked state.
|
|
win.webContents.send(WINDOW_STATE_CHANGED, { ...state });
|
|
};
|
|
const pushFullScreen = () =>
|
|
push({ isFullScreen: fullscreen.native || fullscreen.html });
|
|
|
|
win.on('maximize', () => push({ isMaximized: true }));
|
|
win.on('unmaximize', () => push({ isMaximized: false }));
|
|
// The html variants cover HTML-element fullscreen; not every
|
|
// platform/trigger emits both pairs, and duplicate pushes with the
|
|
// same payload are harmless.
|
|
win.on('enter-full-screen', () => {
|
|
fullscreen.native = true;
|
|
pushFullScreen();
|
|
});
|
|
win.on('leave-full-screen', () => {
|
|
fullscreen.native = false;
|
|
pushFullScreen();
|
|
});
|
|
win.on('enter-html-full-screen', () => {
|
|
fullscreen.html = true;
|
|
pushFullScreen();
|
|
});
|
|
win.on('leave-html-full-screen', () => {
|
|
fullscreen.html = false;
|
|
pushFullScreen();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Persist the state restored on the next launch: window bounds and the
|
|
* zoom level. Runs from both the window 'close' and app 'before-quit'
|
|
* handlers, since either can be the last to run before the process ends.
|
|
* The zoom half is a no-op until the window's preload took ownership of
|
|
* the level (`services/window-zoom-level.ts`).
|
|
*/
|
|
private static persistWindowState(win: Electron.BrowserWindow): void {
|
|
store.set(WINDOW_BOUNDS, win.getNormalBounds());
|
|
persistZoomLevel(win);
|
|
}
|
|
|
|
private static initMainWindow() {
|
|
const workAreaSize = screen.getPrimaryDisplay().workAreaSize;
|
|
const width = Math.min(1280, workAreaSize.width || 1280);
|
|
const height = Math.min(720, workAreaSize.height || 720);
|
|
|
|
const savedWindowBounds = store.get(WINDOW_BOUNDS);
|
|
const startupWindowMode = resolveStartupWindowMode({
|
|
// One-shot: the switch describes the launch, not every window
|
|
// this process ever opens. On macOS the process outlives its
|
|
// last window and the Dock re-creates it through this same
|
|
// path, which must then follow the stored setting only.
|
|
cliHasFullscreenSwitch:
|
|
!App.launchFullscreenSwitchConsumed &&
|
|
app.commandLine.hasSwitch(FULLSCREEN_LAUNCH_SWITCH),
|
|
storedMode: store.get(STARTUP_WINDOW_MODE),
|
|
});
|
|
App.launchFullscreenSwitchConsumed = true;
|
|
|
|
// Create the browser window.
|
|
App.mainWindow = new BrowserWindow({
|
|
title: 'IPTVnator',
|
|
width: width,
|
|
height: height,
|
|
show: false,
|
|
// The splash colour: the window can be shown before its first
|
|
// paint (main-window-first-show.ts).
|
|
backgroundColor: MAIN_WINDOW_BACKGROUND_COLOR,
|
|
webPreferences: getMainWindowWebPreferences(),
|
|
...savedWindowBounds,
|
|
// Fullscreen is a constructor option: the window is created
|
|
// hidden and enters fullscreen before its first paint. The saved
|
|
// bounds stay spread in above — they are the normal bounds the
|
|
// window returns to, and the close handler keeps persisting
|
|
// getNormalBounds(), so a fullscreen session never corrupts them.
|
|
...(startupWindowMode === 'fullscreen' ? { fullscreen: true } : {}),
|
|
minHeight: 600,
|
|
minWidth: 900,
|
|
...App.getPlatformTitleBarOptions(),
|
|
});
|
|
App.mainWindow.setMenu(null);
|
|
attachMainWindowPerformanceCounters(
|
|
App.mainWindow,
|
|
performanceCounters,
|
|
{
|
|
capture: isPerformanceCaptureEnabled(),
|
|
sqlStatements: isSqlStatementCountEnabled(),
|
|
}
|
|
);
|
|
attachWindowTrace(App.mainWindow);
|
|
App.attachWindowStateEvents(App.mainWindow);
|
|
// Seeds the F11 tracker's fullscreen state now, while no transition
|
|
// can be in flight; from here on it follows the window's events.
|
|
trackNativeFullScreen(App.mainWindow);
|
|
App.notifyMainWindowCreated(App.mainWindow);
|
|
if (!savedWindowBounds) {
|
|
App.mainWindow.center();
|
|
}
|
|
|
|
// Shown at ready-to-show or did-finish-load, whichever comes first
|
|
// (see main-window-first-show.ts).
|
|
const mainWindow = App.mainWindow;
|
|
showMainWindowWhenLoaded(mainWindow, () => {
|
|
// maximize() on a hidden window shows it (Electron docs), so it
|
|
// waits for the document like show() does — any earlier and a
|
|
// blank window flashes before the splash is there.
|
|
if (startupWindowMode === 'maximized') {
|
|
mainWindow.maximize();
|
|
}
|
|
mainWindow.show();
|
|
// macOS ignores the constructor's `fullscreen` while the window
|
|
// is hidden — an NSWindow can only toggle fullscreen once it is
|
|
// on screen — so the request is repeated after show() wherever
|
|
// it has not taken yet. Windows/Linux honoured it at creation
|
|
// (before the first paint) and are left alone. It goes through
|
|
// the transition tracker so an F11 pressed during the animation
|
|
// reads the pending target and leaves fullscreen instead of
|
|
// asking for it again.
|
|
if (
|
|
startupWindowMode === 'fullscreen' &&
|
|
!mainWindow.isFullScreen()
|
|
) {
|
|
requestFullScreen(mainWindow, true);
|
|
}
|
|
});
|
|
|
|
// Route target="_blank" / window.open() to the OS default browser
|
|
App.mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
if (isExternalBrowserUrl(url)) {
|
|
shell.openExternal(url);
|
|
}
|
|
return { action: 'deny' };
|
|
});
|
|
|
|
App.mainWindow.webContents.on(
|
|
'will-navigate',
|
|
App.handleRendererNavigation
|
|
);
|
|
App.mainWindow.webContents.on(
|
|
'will-redirect',
|
|
App.handleRendererNavigation
|
|
);
|
|
|
|
// The preload restores the zoom level on every document load; the
|
|
// main process only saves it back before a reload drops it.
|
|
attachZoomLevelPersistence(App.mainWindow);
|
|
|
|
// A reload on an in-app route asks file:// for a path that does not
|
|
// exist; re-load the packaged index with that route instead of
|
|
// leaving Chromium's error page (see renderer-reload-fallback.ts).
|
|
attachRendererReloadFallback(
|
|
App.mainWindow,
|
|
getPackagedRendererIndexPath()
|
|
);
|
|
|
|
// Emitted when the window is closed.
|
|
App.mainWindow.on('closed', () => {
|
|
// Dereference the window object, usually you would store windows
|
|
// in an array if your app supports multi windows, this is the time
|
|
// when you should delete the corresponding element.
|
|
App.mainWindow = null;
|
|
App.loadedMainWindow = null;
|
|
App.mainWindowLoadPromise = null;
|
|
});
|
|
|
|
App.mainWindow.on('close', () => {
|
|
if (App.mainWindow) {
|
|
App.persistWindowState(App.mainWindow);
|
|
}
|
|
});
|
|
|
|
// Enable context menu for input fields only
|
|
App.mainWindow.webContents.on('context-menu', (event, params) => {
|
|
const { isEditable, editFlags } = params;
|
|
|
|
// Check if this is an editable field (input, textarea, contenteditable)
|
|
// editFlags.canPaste is a good indicator of an input field
|
|
if (isEditable && editFlags.canPaste) {
|
|
const menu = Menu.buildFromTemplate([
|
|
{
|
|
label: 'Cut',
|
|
role: 'cut',
|
|
enabled: editFlags.canCut,
|
|
},
|
|
{
|
|
label: 'Copy',
|
|
role: 'copy',
|
|
enabled: editFlags.canCopy,
|
|
},
|
|
{
|
|
label: 'Paste',
|
|
role: 'paste',
|
|
enabled: editFlags.canPaste,
|
|
},
|
|
{
|
|
type: 'separator',
|
|
},
|
|
{
|
|
label: 'Select All',
|
|
role: 'selectAll',
|
|
enabled: editFlags.canSelectAll,
|
|
},
|
|
]);
|
|
|
|
menu.popup();
|
|
}
|
|
});
|
|
}
|
|
|
|
private static async loadMainWindowContent(
|
|
mainWindow: Electron.BrowserWindow
|
|
): Promise<void> {
|
|
// load the index.html of the app.
|
|
if (App.isDevelopmentMode()) {
|
|
const loadPromise = mainWindow.loadURL(
|
|
`http://localhost:${rendererAppPort}`
|
|
);
|
|
if (App.shouldOpenDevTools()) {
|
|
mainWindow.webContents.openDevTools();
|
|
}
|
|
await loadPromise;
|
|
} else {
|
|
await clearElectronServiceWorkerStorage();
|
|
await mainWindow.loadFile(getPackagedRendererIndexPath());
|
|
}
|
|
}
|
|
|
|
static async loadMainWindow(): Promise<void> {
|
|
App.rendererLoadingEnabled = true;
|
|
|
|
if (!rendererAppName || !App.mainWindow) {
|
|
return;
|
|
}
|
|
|
|
if (App.loadedMainWindow === App.mainWindow) {
|
|
return;
|
|
}
|
|
|
|
if (!App.mainWindowLoadPromise) {
|
|
const mainWindow = App.mainWindow;
|
|
App.mainWindowLoadPromise = App.loadMainWindowContent(mainWindow)
|
|
.then(() => {
|
|
App.loadedMainWindow = mainWindow;
|
|
})
|
|
.finally(() => {
|
|
App.mainWindowLoadPromise = null;
|
|
});
|
|
}
|
|
|
|
await App.mainWindowLoadPromise;
|
|
}
|
|
|
|
static main(app: Electron.App, browserWindow: typeof BrowserWindow) {
|
|
// we pass the Electron.App object and the
|
|
// Electron.BrowserWindow into this function
|
|
// so this class has no dependencies. This
|
|
// makes the code easier to write tests for
|
|
|
|
App.BrowserWindow = browserWindow;
|
|
App.application = app;
|
|
|
|
App.application.on('window-all-closed', App.onWindowAllClosed); // Quit when all windows are closed.
|
|
if (App.application.isReady()) {
|
|
App.onReady();
|
|
} else {
|
|
App.application.on('ready', App.onReady); // App is ready to load data
|
|
}
|
|
App.application.on('activate', App.onActivate); // App is activated
|
|
App.application.on('before-quit', () => {
|
|
if (App.mainWindow) App.persistWindowState(App.mainWindow);
|
|
});
|
|
}
|
|
}
|