mirror of
https://github.com/Llewellynvdm/nativefier.git
synced 2024-06-15 18:32:21 +00:00
79009e87cd
I'm picking up @RickStanley's abandoned PR #1321 again to add screensharing support (fixes #927), with the following additional changes: - In newer Electron versions, `desktopCapturer.getSources` must be called from the main process, so I solved this with an IPC call. - Importing from `./helpers/helpers` in 'preload.ts' does not work, as was mentioned by @DimICE in https://github.com/nativefier/nativefier/pull/1321#issuecomment-1001518035. I'm not very familiar with TypeScript or Electron, so not sure why that is and how it could be solved - for now I just copied the referenced `isWayland` function to `preload.ts`. - Add a screensharing test to the manual test script, as requested by @ronjouch in https://github.com/nativefier/nativefier/pull/1321#issuecomment-1006725818 As far as I understood from the discussion in #1321, the last point was basically the only thing that was missing to get this merged, correct? --------- Co-authored-by: Rick Stanley <rick-stanley@outlook.com> Co-authored-by: Rick Stanley <rick.stanley@lambda3.com.br> Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
457 lines
13 KiB
TypeScript
457 lines
13 KiB
TypeScript
import 'source-map-support/register';
|
|
|
|
import fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
import electron, {
|
|
app,
|
|
dialog,
|
|
globalShortcut,
|
|
systemPreferences,
|
|
BrowserWindow,
|
|
Event,
|
|
} from 'electron';
|
|
import electronDownload from 'electron-dl';
|
|
|
|
import { createLoginWindow } from './components/loginWindow';
|
|
import {
|
|
createMainWindow,
|
|
saveAppArgs,
|
|
APP_ARGS_FILE_PATH,
|
|
} from './components/mainWindow';
|
|
import { createTrayIcon } from './components/trayIcon';
|
|
import {
|
|
isOSX,
|
|
isWayland,
|
|
isWindows,
|
|
removeUserAgentSpecifics,
|
|
} from './helpers/helpers';
|
|
import { inferFlashPath } from './helpers/inferFlash';
|
|
import * as log from './helpers/loggingHelper';
|
|
import {
|
|
IS_PLAYWRIGHT,
|
|
PLAYWRIGHT_CONFIG,
|
|
safeGetEnv,
|
|
} from './helpers/playwrightHelpers';
|
|
import { OutputOptions } from '../../shared/src/options/model';
|
|
|
|
// Entrypoint for Squirrel, a windows update framework. See https://github.com/nativefier/nativefier/pull/744
|
|
if (require('electron-squirrel-startup')) {
|
|
app.exit();
|
|
}
|
|
|
|
if (process.argv.indexOf('--verbose') > -1 || safeGetEnv('VERBOSE') === '1') {
|
|
log.setLevel('DEBUG');
|
|
process.traceDeprecation = true;
|
|
process.traceProcessWarnings = true;
|
|
process.argv.slice(1);
|
|
}
|
|
|
|
let mainWindow: BrowserWindow;
|
|
|
|
const appArgs =
|
|
IS_PLAYWRIGHT && PLAYWRIGHT_CONFIG
|
|
? (JSON.parse(PLAYWRIGHT_CONFIG) as OutputOptions)
|
|
: (JSON.parse(
|
|
fs.readFileSync(APP_ARGS_FILE_PATH, 'utf8'),
|
|
) as OutputOptions);
|
|
|
|
log.debug('appArgs', appArgs);
|
|
// Do this relatively early so that we can start storing appData with the app
|
|
if (appArgs.portable) {
|
|
log.debug(
|
|
'App was built as portable; setting appData and userData to the app folder: ',
|
|
path.resolve(path.join(__dirname, '..', 'appData')),
|
|
);
|
|
app.setPath('appData', path.join(__dirname, '..', 'appData'));
|
|
app.setPath('userData', path.join(__dirname, '..', 'appData'));
|
|
}
|
|
|
|
if (!appArgs.userAgentHonest) {
|
|
if (appArgs.userAgent) {
|
|
app.userAgentFallback = appArgs.userAgent;
|
|
} else {
|
|
app.userAgentFallback = removeUserAgentSpecifics(
|
|
app.userAgentFallback,
|
|
app.getName(),
|
|
app.getVersion(),
|
|
);
|
|
}
|
|
}
|
|
|
|
// this step is required to allow app names to be displayed correctly in notifications on windows
|
|
// https://www.electronjs.org/docs/latest/api/app#appsetappusermodelidid-windows
|
|
// https://www.electronjs.org/docs/latest/tutorial/notifications#windows
|
|
if (isWindows()) {
|
|
app.setAppUserModelId(app.getName());
|
|
}
|
|
|
|
const urlArgv = process.argv.filter((a) => a.startsWith('http'));
|
|
|
|
// Take in a URL on the command line as an override
|
|
if (urlArgv.length > 0) {
|
|
const maybeUrl = urlArgv[0];
|
|
try {
|
|
new URL(maybeUrl);
|
|
appArgs.targetUrl = maybeUrl;
|
|
log.info('Loading override URL passed as argument:', maybeUrl);
|
|
} catch (err: unknown) {
|
|
log.error(
|
|
'Not loading override URL passed as argument, because failed to parse:',
|
|
maybeUrl,
|
|
err,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Nativefier is a browser, and an old browser is an insecure / badly-performant one.
|
|
// Given our builder/app design, we currently don't have an easy way to offer
|
|
// upgrades from the app themselves (like browsers do).
|
|
// As a workaround, we ask for a manual upgrade & re-build if the build is old.
|
|
// The period in days is chosen to be not too small to be exceedingly annoying,
|
|
// but not too large to be exceedingly insecure.
|
|
const OLD_BUILD_WARNING_THRESHOLD_DAYS = 90;
|
|
const OLD_BUILD_WARNING_THRESHOLD_MS =
|
|
OLD_BUILD_WARNING_THRESHOLD_DAYS * 24 * 60 * 60 * 1000;
|
|
|
|
const fileDownloadOptions = { ...appArgs.fileDownloadOptions };
|
|
electronDownload(fileDownloadOptions);
|
|
|
|
if (appArgs.processEnvs) {
|
|
let processEnvs: Record<string, string> =
|
|
appArgs.processEnvs as unknown as Record<string, string>;
|
|
// This is compatibility if just a string was passed.
|
|
if (typeof appArgs.processEnvs === 'string') {
|
|
try {
|
|
processEnvs = JSON.parse(appArgs.processEnvs) as Record<string, string>;
|
|
} catch {
|
|
// This wasn't JSON. Fall back to the old code
|
|
processEnvs = {};
|
|
process.env.processEnvs = appArgs.processEnvs;
|
|
}
|
|
}
|
|
Object.keys(processEnvs)
|
|
.filter((key) => key !== undefined)
|
|
.forEach((key) => {
|
|
process.env[key] = processEnvs[key];
|
|
});
|
|
}
|
|
|
|
if (typeof appArgs.flashPluginDir === 'string') {
|
|
app.commandLine.appendSwitch('ppapi-flash-path', appArgs.flashPluginDir);
|
|
} else if (appArgs.flashPluginDir) {
|
|
const flashPath = inferFlashPath();
|
|
app.commandLine.appendSwitch('ppapi-flash-path', flashPath);
|
|
}
|
|
|
|
if (appArgs.ignoreCertificate) {
|
|
app.commandLine.appendSwitch('ignore-certificate-errors');
|
|
}
|
|
|
|
if (appArgs.disableGpu) {
|
|
app.disableHardwareAcceleration();
|
|
}
|
|
|
|
if (appArgs.ignoreGpuBlacklist) {
|
|
app.commandLine.appendSwitch('ignore-gpu-blacklist');
|
|
}
|
|
|
|
if (appArgs.enableEs3Apis) {
|
|
app.commandLine.appendSwitch('enable-es3-apis');
|
|
}
|
|
|
|
if (appArgs.diskCacheSize) {
|
|
app.commandLine.appendSwitch(
|
|
'disk-cache-size',
|
|
appArgs.diskCacheSize.toString(),
|
|
);
|
|
}
|
|
|
|
if (appArgs.basicAuthUsername) {
|
|
app.commandLine.appendSwitch(
|
|
'basic-auth-username',
|
|
appArgs.basicAuthUsername,
|
|
);
|
|
}
|
|
|
|
if (appArgs.basicAuthPassword) {
|
|
app.commandLine.appendSwitch(
|
|
'basic-auth-password',
|
|
appArgs.basicAuthPassword,
|
|
);
|
|
}
|
|
|
|
if (isWayland()) {
|
|
app.commandLine.appendSwitch('enable-features', 'WebRTCPipeWireCapturer');
|
|
}
|
|
|
|
if (appArgs.lang) {
|
|
const langParts = appArgs.lang.split(',');
|
|
// Convert locales to languages, because for some reason locales don't work. Stupid Chromium
|
|
const langPartsParsed = Array.from(
|
|
// Convert to set to dedupe in case something like "en-GB,en-US" was passed
|
|
new Set(langParts.map((l) => l.split('-')[0])),
|
|
);
|
|
const langFlag = langPartsParsed.join(',');
|
|
log.debug('Setting --lang flag to', langFlag);
|
|
app.commandLine.appendSwitch('--lang', langFlag);
|
|
}
|
|
|
|
let currentBadgeCount = 0;
|
|
const setDockBadge = isOSX()
|
|
? (count?: number | string, bounce = false): void => {
|
|
if (count !== undefined) {
|
|
app.dock.setBadge(count.toString());
|
|
if (bounce && count > currentBadgeCount) app.dock.bounce();
|
|
currentBadgeCount = typeof count === 'number' ? count : 0;
|
|
}
|
|
}
|
|
: (): void => undefined;
|
|
|
|
app.on('window-all-closed', () => {
|
|
log.debug('app.window-all-closed');
|
|
if (!isOSX() || appArgs.fastQuit || IS_PLAYWRIGHT) {
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
app.on('before-quit', () => {
|
|
log.debug('app.before-quit');
|
|
// not fired when the close button on the window is clicked
|
|
if (isOSX()) {
|
|
// need to force a quit as a workaround here to simulate the osx app hiding behaviour
|
|
// Somehow sokution at https://github.com/atom/electron/issues/444#issuecomment-76492576 does not work,
|
|
// e.prevent default appears to persist
|
|
|
|
// might cause issues in the future as before-quit and will-quit events are not called
|
|
app.exit(0);
|
|
}
|
|
});
|
|
|
|
app.on('will-quit', (event) => {
|
|
log.debug('app.will-quit', event);
|
|
});
|
|
|
|
app.on('quit', (event, exitCode) => {
|
|
log.debug('app.quit', { event, exitCode });
|
|
});
|
|
|
|
app.on('will-finish-launching', () => {
|
|
log.debug('app.will-finish-launching');
|
|
});
|
|
|
|
app.on('open-url', (event, url) => {
|
|
log.debug('app.open-url', { event, url });
|
|
|
|
event.preventDefault();
|
|
if (mainWindow) {
|
|
mainWindow.webContents.send('open-url', url);
|
|
}
|
|
});
|
|
|
|
if (appArgs.widevine) {
|
|
// @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime
|
|
app.on('widevine-ready', (version: string, lastVersion: string) => {
|
|
log.debug('app.widevine-ready', { version, lastVersion });
|
|
onReady().catch((err) => log.error('onReady ERROR', err));
|
|
});
|
|
|
|
app.on(
|
|
// @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime
|
|
'widevine-update-pending',
|
|
(currentVersion: string, pendingVersion: string) => {
|
|
log.debug('app.widevine-update-pending', {
|
|
currentVersion,
|
|
pendingVersion,
|
|
});
|
|
},
|
|
);
|
|
|
|
// @ts-expect-error This event only appears on the widevine version of electron, which we'd see at runtime
|
|
app.on('widevine-error', (error: Error) => {
|
|
log.error('app.widevine-error', error);
|
|
});
|
|
} else {
|
|
app.on('ready', () => {
|
|
log.debug('ready');
|
|
onReady().catch((err) => log.error('onReady ERROR', err));
|
|
});
|
|
}
|
|
|
|
app.on('activate', (event: electron.Event, hasVisibleWindows: boolean) => {
|
|
log.debug('app.activate', { event, hasVisibleWindows });
|
|
if (isOSX() && !IS_PLAYWRIGHT) {
|
|
// this is called when the dock is clicked
|
|
if (!hasVisibleWindows) {
|
|
mainWindow.show();
|
|
}
|
|
}
|
|
});
|
|
|
|
// quit if singleInstance mode and there's already another instance running
|
|
const shouldQuit = appArgs.singleInstance && !app.requestSingleInstanceLock();
|
|
if (shouldQuit) {
|
|
app.quit();
|
|
} else {
|
|
app.on('second-instance', () => {
|
|
log.debug('app.second-instance');
|
|
if (mainWindow) {
|
|
if (!mainWindow.isVisible()) {
|
|
// try
|
|
mainWindow.show();
|
|
}
|
|
if (mainWindow.isMinimized()) {
|
|
// minimized
|
|
mainWindow.restore();
|
|
}
|
|
mainWindow.focus();
|
|
}
|
|
});
|
|
}
|
|
|
|
app.on('new-window-for-tab', () => {
|
|
log.debug('app.new-window-for-tab');
|
|
if (mainWindow) {
|
|
mainWindow.emit('new-tab');
|
|
}
|
|
});
|
|
|
|
app.on(
|
|
'login',
|
|
(
|
|
event,
|
|
webContents,
|
|
request,
|
|
authInfo,
|
|
callback: (username?: string, password?: string) => void,
|
|
) => {
|
|
log.debug('app.login', { event, request });
|
|
// for http authentication
|
|
event.preventDefault();
|
|
|
|
if (appArgs.basicAuthUsername && appArgs.basicAuthPassword) {
|
|
callback(appArgs.basicAuthUsername, appArgs.basicAuthPassword);
|
|
} else {
|
|
createLoginWindow(callback, mainWindow).catch((err) =>
|
|
log.error('createLoginWindow ERROR', err),
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
async function onReady(): Promise<void> {
|
|
// Warning: `mainWindow` below is the *global* unique `mainWindow`, created at init time
|
|
mainWindow = await createMainWindow(appArgs, setDockBadge);
|
|
|
|
createTrayIcon(appArgs, mainWindow);
|
|
|
|
// Register global shortcuts
|
|
if (appArgs.globalShortcuts) {
|
|
appArgs.globalShortcuts.forEach((shortcut) => {
|
|
globalShortcut.register(shortcut.key, () => {
|
|
shortcut.inputEvents.forEach((inputEvent) => {
|
|
// @ts-expect-error without including electron in our models, these will never match
|
|
mainWindow.webContents.sendInputEvent(inputEvent);
|
|
});
|
|
});
|
|
});
|
|
|
|
if (isOSX() && appArgs.accessibilityPrompt) {
|
|
const mediaKeys = [
|
|
'MediaPlayPause',
|
|
'MediaNextTrack',
|
|
'MediaPreviousTrack',
|
|
'MediaStop',
|
|
];
|
|
const globalShortcutsKeys = appArgs.globalShortcuts.map((g) => g.key);
|
|
const mediaKeyWasSet = globalShortcutsKeys.find((g) =>
|
|
mediaKeys.includes(g),
|
|
);
|
|
if (
|
|
mediaKeyWasSet &&
|
|
!systemPreferences.isTrustedAccessibilityClient(false)
|
|
) {
|
|
// Since we're trying to set global keyboard shortcuts for media keys, we need to prompt
|
|
// the user for permission on Mac.
|
|
// For reference:
|
|
// https://www.electronjs.org/docs/api/global-shortcut?q=MediaPlayPause#globalshortcutregisteraccelerator-callback
|
|
const accessibilityPromptResult = dialog.showMessageBoxSync(
|
|
mainWindow,
|
|
{
|
|
type: 'question',
|
|
message: 'Accessibility Permissions Needed',
|
|
buttons: ['Yes', 'No', 'No and never ask again'],
|
|
defaultId: 0,
|
|
detail:
|
|
`${appArgs.name} would like to use one or more of your keyboard's media keys (start, stop, next track, or previous track) to control it.\n\n` +
|
|
`Would you like Mac OS to ask for your permission to do so?\n\n` +
|
|
`If so, you will need to restart ${appArgs.name} after granting permissions for these keyboard shortcuts to begin working.`,
|
|
},
|
|
);
|
|
switch (accessibilityPromptResult) {
|
|
// User clicked Yes, prompt for accessibility
|
|
case 0:
|
|
systemPreferences.isTrustedAccessibilityClient(true);
|
|
break;
|
|
// User cliecked Never Ask Me Again, save that info
|
|
case 2:
|
|
appArgs.accessibilityPrompt = false;
|
|
saveAppArgs(appArgs);
|
|
break;
|
|
// User clicked No
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (
|
|
!appArgs.disableOldBuildWarning &&
|
|
new Date().getTime() - appArgs.buildDate > OLD_BUILD_WARNING_THRESHOLD_MS
|
|
) {
|
|
const oldBuildWarningText =
|
|
appArgs.oldBuildWarningText ||
|
|
'This app was built a long time ago. Nativefier uses the Chrome browser (through Electron), and it is insecure to keep using an old version of it. Please upgrade Nativefier and rebuild this app.';
|
|
dialog
|
|
.showMessageBox(mainWindow, {
|
|
type: 'warning',
|
|
message: 'Old build detected',
|
|
detail: oldBuildWarningText,
|
|
})
|
|
.catch((err) => log.error('dialog.showMessageBox ERROR', err));
|
|
}
|
|
|
|
if (appArgs.targetUrl) {
|
|
await mainWindow.loadURL(appArgs.targetUrl);
|
|
}
|
|
}
|
|
|
|
app.on(
|
|
'accessibility-support-changed',
|
|
(event: Event, accessibilitySupportEnabled: boolean) => {
|
|
log.debug('app.accessibility-support-changed', {
|
|
event,
|
|
accessibilitySupportEnabled,
|
|
});
|
|
},
|
|
);
|
|
|
|
app.on(
|
|
'activity-was-continued',
|
|
(event: Event, type: string, userInfo: unknown) => {
|
|
log.debug('app.activity-was-continued', { event, type, userInfo });
|
|
},
|
|
);
|
|
|
|
app.on('browser-window-blur', () => {
|
|
log.debug('app.browser-window-blur');
|
|
});
|
|
|
|
app.on('browser-window-created', () => {
|
|
log.debug('app.browser-window-created');
|
|
});
|
|
|
|
app.on('browser-window-focus', () => {
|
|
log.debug('app.browser-window-focus');
|
|
});
|