483 lines
13 KiB
TypeScript
483 lines
13 KiB
TypeScript
import { app, Menu, ipcMain, BrowserWindow, shell } from 'electron';
|
|
import * as fse from 'fs-extra';
|
|
import * as path from 'path';
|
|
import * as ChildProcess from 'child_process';
|
|
|
|
import { shellNeedsPatching, updateEnvironmentForProcess } from '@overflow/core/shell';
|
|
import { parseAppURL } from '@overflow/core/parse-app-url';
|
|
// import {
|
|
// enableSourceMaps,
|
|
// withSourceMappedStack,
|
|
// } from '@overflow/core/source-map-support';
|
|
import { now } from '@overflow/core/now';
|
|
import { IMenuItem } from '@overflow/core/menu-item';
|
|
|
|
import { AppWindow } from './app-window';
|
|
|
|
import { handleSquirrelEvent } from './squirrel-updater';
|
|
|
|
import { openDirectorySafe } from './shell';
|
|
import { buildDefaultMenu } from './menu/build-default';
|
|
import { MenuEvent } from '../commons/type';
|
|
import { findMenuItemByID } from './menu/find-menu-item';
|
|
import { IMenuItemState } from '../commons/model/menu-update';
|
|
|
|
import * as sqlite3 from 'sqlite3';
|
|
|
|
// enableSourceMaps();
|
|
|
|
let mainWindow: AppWindow | null = null;
|
|
|
|
const launchTime = now();
|
|
|
|
let preventQuit = false;
|
|
let readyTime: number | null = null;
|
|
|
|
type OnDidLoadFn = (window: AppWindow) => void;
|
|
/** See the `onDidLoad` function. */
|
|
let onDidLoadFns: Array<OnDidLoadFn> | null = [];
|
|
|
|
function handleUncaughtException(error: Error) {
|
|
preventQuit = true;
|
|
|
|
if (mainWindow) {
|
|
mainWindow.destroy();
|
|
mainWindow = null;
|
|
}
|
|
|
|
const isLaunchError = !mainWindow;
|
|
// showUncaughtException(isLaunchError, error);
|
|
}
|
|
|
|
// process.on('uncaughtException', (error: Error) => {
|
|
// error = withSourceMappedStack(error);
|
|
|
|
// // reportError(error);
|
|
// handleUncaughtException(error);
|
|
// });
|
|
|
|
const handlingSquirrelEvent = false;
|
|
// if (__WIN32__ && process.argv.length > 1) {
|
|
// const arg = process.argv[1];
|
|
|
|
// const promise = handleSquirrelEvent(arg);
|
|
// if (promise) {
|
|
// handlingSquirrelEvent = true;
|
|
// promise
|
|
// .catch(e => {
|
|
// log.error(`Failed handling Squirrel event: ${arg}`, e);
|
|
// })
|
|
// .then(() => {
|
|
// app.quit();
|
|
// });
|
|
// } else {
|
|
// handlePossibleProtocolLauncherArgs(process.argv);
|
|
// }
|
|
// }
|
|
|
|
// function handleAppURL(url: string) {
|
|
// log.info('Processing protocol url');
|
|
// const action = parseAppURL(url);
|
|
// onDidLoad(window => {
|
|
// // This manual focus call _shouldn't_ be necessary, but is for Chrome on
|
|
// // macOS. See https://github.com/desktop/desktop/issues/973.
|
|
// window.focus();
|
|
// window.sendURLAction(action);
|
|
// });
|
|
// }
|
|
|
|
const isDuplicateInstance = false;
|
|
// If we're handling a Squirrel event we don't want to enforce single instance.
|
|
// We want to let the updated instance launch and do its work. It will then quit
|
|
// once it's done.
|
|
// if (!handlingSquirrelEvent) {
|
|
// isDuplicateInstance = app.makeSingleInstance((args, workingDirectory) => {
|
|
// // Someone tried to run a second instance, we should focus our window.
|
|
// if (mainWindow) {
|
|
// if (mainWindow.isMinimized()) {
|
|
// mainWindow.restore();
|
|
// }
|
|
|
|
// if (!mainWindow.isVisible()) {
|
|
// mainWindow.show();
|
|
// }
|
|
|
|
// mainWindow.focus();
|
|
// }
|
|
|
|
// handlePossibleProtocolLauncherArgs(args);
|
|
// });
|
|
|
|
// if (isDuplicateInstance) {
|
|
// app.quit();
|
|
// }
|
|
// }
|
|
|
|
if (shellNeedsPatching(process)) {
|
|
updateEnvironmentForProcess();
|
|
}
|
|
|
|
app.on('will-finish-launching', () => {
|
|
// macOS only
|
|
app.on('open-url', (event, url) => {
|
|
event.preventDefault();
|
|
// handleAppURL(url);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Attempt to detect and handle any protocol handler arguments passed
|
|
* either via the command line directly to the current process or through
|
|
* IPC from a duplicate instance (see makeSingleInstance)
|
|
*
|
|
* @param args Essentially process.argv, i.e. the first element is the exec
|
|
* path
|
|
*/
|
|
// function handlePossibleProtocolLauncherArgs(args: ReadonlyArray<string>) {
|
|
// log.info(`Received possible protocol arguments: ${args.length}`);
|
|
|
|
// if (__WIN32__) {
|
|
// // We register our protocol handler callback on Windows as
|
|
// // [executable path] --protocol-launcher -- "%1" meaning that any
|
|
// // url data comes after we've stopped processing arguments. We check
|
|
// // for that exact scenario here before doing any processing. If there's
|
|
// // more than 4 args because of a malformed url then we bail out.
|
|
// if (
|
|
// args.length === 4 &&
|
|
// args[1] === '--protocol-launcher' &&
|
|
// args[2] === '--'
|
|
// ) {
|
|
// handleAppURL(args[3]);
|
|
// }
|
|
// } else if (args.length > 1) {
|
|
// handleAppURL(args[1]);
|
|
// }
|
|
// }
|
|
|
|
/**
|
|
* Wrapper around app.setAsDefaultProtocolClient that adds our
|
|
* custom prefix command line switches on Windows that prevents
|
|
* command line argument parsing after the `--`.
|
|
*/
|
|
function setAsDefaultProtocolClient(protocol: string) {
|
|
if (__WIN32__) {
|
|
app.setAsDefaultProtocolClient(protocol, process.execPath, [
|
|
'--protocol-launcher',
|
|
'--',
|
|
]);
|
|
} else {
|
|
app.setAsDefaultProtocolClient(protocol);
|
|
}
|
|
}
|
|
|
|
// if (process.env.GITHUB_DESKTOP_DISABLE_HARDWARE_ACCELERATION) {
|
|
// log.info(
|
|
// `GITHUB_DESKTOP_DISABLE_HARDWARE_ACCELERATION environment variable set, disabling hardware acceleration`
|
|
// );
|
|
// app.disableHardwareAcceleration();
|
|
// }
|
|
|
|
app.on('ready', () => {
|
|
if (isDuplicateInstance || handlingSquirrelEvent) {
|
|
return;
|
|
}
|
|
|
|
const dbPath = __DEV__ ?
|
|
path.join(__dirname, '..', '..', 'config', '_database.sqlite')
|
|
: path.join(__dirname, '..', '..', 'bin', 'database.sqlite');
|
|
|
|
const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err: Error) => {
|
|
if (err) {
|
|
console.error(err.message);
|
|
}
|
|
console.log('Connected to the database.');
|
|
});
|
|
|
|
if (!__DEV__) {
|
|
const probePath = path.join(__dirname, '..', '..', 'bin', 'probe');
|
|
ChildProcess.spawn(probePath);
|
|
}
|
|
|
|
readyTime = now() - launchTime;
|
|
|
|
// setAsDefaultProtocolClient('x-github-client');
|
|
|
|
// if (__DEV__) {
|
|
// setAsDefaultProtocolClient('x-github-desktop-dev-auth');
|
|
// } else {
|
|
// setAsDefaultProtocolClient('x-github-desktop-auth');
|
|
// }
|
|
|
|
// // Also support Desktop Classic's protocols.
|
|
// if (__DARWIN__) {
|
|
// setAsDefaultProtocolClient('github-mac');
|
|
// } else if (__WIN32__) {
|
|
// setAsDefaultProtocolClient('github-windows');
|
|
// }
|
|
|
|
createWindow();
|
|
|
|
let menu = buildDefaultMenu();
|
|
Menu.setApplicationMenu(menu);
|
|
|
|
ipcMain.on(
|
|
'update-preferred-app-menu-item-labels',
|
|
(
|
|
event: Electron.IpcMessageEvent,
|
|
) => {
|
|
menu = buildDefaultMenu();
|
|
Menu.setApplicationMenu(menu);
|
|
if (mainWindow) {
|
|
mainWindow.sendAppMenu();
|
|
}
|
|
}
|
|
);
|
|
|
|
ipcMain.on('menu-event', (event: Electron.IpcMessageEvent, args: any[]) => {
|
|
const { name }: { name: MenuEvent } = event as any;
|
|
if (mainWindow) {
|
|
mainWindow.sendMenuEvent(name);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* An event sent by the renderer asking that the menu item with the given id
|
|
* is executed (ie clicked).
|
|
*/
|
|
ipcMain.on(
|
|
'execute-menu-item',
|
|
(event: Electron.IpcMessageEvent, { id }: { id: string }) => {
|
|
const menuItem = findMenuItemByID(menu, id);
|
|
if (menuItem) {
|
|
const window = BrowserWindow.fromWebContents(event.sender);
|
|
const fakeEvent = { preventDefault: () => { }, sender: event.sender };
|
|
menuItem.click(fakeEvent, window, event.sender);
|
|
}
|
|
}
|
|
);
|
|
|
|
ipcMain.on(
|
|
'update-menu-state',
|
|
(
|
|
event: Electron.IpcMessageEvent,
|
|
items: Array<{ id: string; state: IMenuItemState }>
|
|
) => {
|
|
let sendMenuChangedEvent = false;
|
|
|
|
for (const item of items) {
|
|
const { id, state } = item;
|
|
const menuItem = findMenuItemByID(menu, id);
|
|
|
|
if (menuItem) {
|
|
// Only send the updated app menu when the state actually changes
|
|
// or we might end up introducing a never ending loop between
|
|
// the renderer and the main process
|
|
if (
|
|
state.enabled !== undefined &&
|
|
menuItem.enabled !== state.enabled
|
|
) {
|
|
menuItem.enabled = state.enabled;
|
|
sendMenuChangedEvent = true;
|
|
}
|
|
} else {
|
|
// fatalError(`Unknown menu id: ${id}`);
|
|
}
|
|
}
|
|
|
|
if (sendMenuChangedEvent && mainWindow) {
|
|
mainWindow.sendAppMenu();
|
|
}
|
|
}
|
|
);
|
|
|
|
// ipcMain.on(
|
|
// 'show-contextual-menu',
|
|
// (event: Electron.IpcMessageEvent, items: ReadonlyArray<IMenuItem>) => {
|
|
// const menu = buildContextMenu(items, ix =>
|
|
// event.sender.send('contextual-menu-action', ix)
|
|
// );
|
|
|
|
// const window = BrowserWindow.fromWebContents(event.sender);
|
|
// menu.popup({ window });
|
|
// }
|
|
// );
|
|
|
|
/**
|
|
* An event sent by the renderer asking for a copy of the current
|
|
* application menu.
|
|
*/
|
|
ipcMain.on('get-app-menu', () => {
|
|
if (mainWindow) {
|
|
mainWindow.sendAppMenu();
|
|
}
|
|
});
|
|
|
|
// ipcMain.on(
|
|
// 'show-certificate-trust-dialog',
|
|
// (
|
|
// event: Electron.IpcMessageEvent,
|
|
// {
|
|
// certificate,
|
|
// message,
|
|
// }: { certificate: Electron.Certificate; message: string }
|
|
// ) => {
|
|
// // This API is only implemented for macOS and Windows right now.
|
|
// if (__DARWIN__ || __WIN32__) {
|
|
// onDidLoad(window => {
|
|
// window.showCertificateTrustDialog(certificate, message);
|
|
// });
|
|
// }
|
|
// }
|
|
// );
|
|
|
|
// ipcMain.on(
|
|
// 'log',
|
|
// (event: Electron.IpcMessageEvent, level: LogLevel, message: string) => {
|
|
// writeLog(level, message);
|
|
// }
|
|
// );
|
|
|
|
// ipcMain.on(
|
|
// 'uncaught-exception',
|
|
// (event: Electron.IpcMessageEvent, error: Error) => {
|
|
// handleUncaughtException(error);
|
|
// }
|
|
// );
|
|
|
|
// ipcMain.on(
|
|
// 'send-error-report',
|
|
// (
|
|
// event: Electron.IpcMessageEvent,
|
|
// { error, extra }: { error: Error; extra: { [key: string]: string } }
|
|
// ) => {
|
|
// reportError(error, extra);
|
|
// }
|
|
// );
|
|
|
|
// ipcMain.on(
|
|
// 'open-external',
|
|
// (event: Electron.IpcMessageEvent, { path }: { path: string }) => {
|
|
// const pathLowerCase = path.toLowerCase();
|
|
// if (
|
|
// pathLowerCase.startsWith('http://') ||
|
|
// pathLowerCase.startsWith('https://')
|
|
// ) {
|
|
// log.info(`opening in browser: ${path}`);
|
|
// }
|
|
|
|
// const result = shell.openExternal(path);
|
|
// event.sender.send('open-external-result', { result });
|
|
// }
|
|
// );
|
|
|
|
// ipcMain.on(
|
|
// 'show-item-in-folder',
|
|
// (event: Electron.IpcMessageEvent, { path }: { path: string }) => {
|
|
// Fs.stat(path, (err, stats) => {
|
|
// if (err) {
|
|
// log.error(`Unable to find file at '${path}'`, err);
|
|
// return;
|
|
// }
|
|
|
|
// if (stats.isDirectory()) {
|
|
// openDirectorySafe(path);
|
|
// } else {
|
|
// shell.showItemInFolder(path);
|
|
// }
|
|
// });
|
|
// }
|
|
// );
|
|
});
|
|
|
|
app.on('activate', () => {
|
|
onDidLoad(window => {
|
|
window.show();
|
|
});
|
|
});
|
|
|
|
app.on('web-contents-created', (event, contents) => {
|
|
contents.on('new-window', (_event, url) => {
|
|
// Prevent links or window.open from opening new windows
|
|
_event.preventDefault();
|
|
log.warn(`Prevented new window to: ${url}`);
|
|
});
|
|
});
|
|
|
|
// app.on(
|
|
// 'certificate-error',
|
|
// (event, webContents, url, error, certificate, callback) => {
|
|
// callback(false);
|
|
|
|
// onDidLoad(window => {
|
|
// window.sendCertificateError(certificate, error, url);
|
|
// });
|
|
// }
|
|
// );
|
|
|
|
function createWindow() {
|
|
const window = new AppWindow();
|
|
|
|
if (__DEV__) {
|
|
const {
|
|
default: installExtension,
|
|
} = require('electron-devtools-installer');
|
|
|
|
require('electron-debug')({ showDevTools: true });
|
|
|
|
const ChromeLens = {
|
|
id: 'idikgljglpfilbhaboonnpnnincjhjkd',
|
|
electron: '>=1.2.1',
|
|
};
|
|
|
|
const extensions = [ChromeLens];
|
|
|
|
for (const extension of extensions) {
|
|
try {
|
|
installExtension(extension);
|
|
} catch (e) { }
|
|
}
|
|
}
|
|
|
|
window.onClose(() => {
|
|
mainWindow = null;
|
|
if (!__DARWIN__ && !preventQuit) {
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
window.onDidLoad((data: any) => {
|
|
window.show();
|
|
window.sendLaunchTimingStats({
|
|
mainReadyTime: readyTime ? readyTime : 0,
|
|
loadTime: window.loadTime ? window.loadTime : 0,
|
|
rendererReadyTime: window.rendererReadyTime ? window.rendererReadyTime : 0,
|
|
});
|
|
|
|
const fns = onDidLoadFns ? onDidLoadFns : null;
|
|
onDidLoadFns = null;
|
|
if (fns) {
|
|
for (const fn of fns) {
|
|
fn(window);
|
|
}
|
|
}
|
|
});
|
|
|
|
window.load();
|
|
|
|
mainWindow = window;
|
|
}
|
|
|
|
/**
|
|
* Register a function to be called once the window has been loaded. If the
|
|
* window has already been loaded, the function will be called immediately.
|
|
*/
|
|
function onDidLoad(fn: OnDidLoadFn) {
|
|
if (onDidLoadFns) {
|
|
onDidLoadFns.push(fn);
|
|
} else {
|
|
if (mainWindow) {
|
|
fn(mainWindow);
|
|
}
|
|
}
|
|
}
|