2
2
mirror of https://github.com/Llewellynvdm/nativefier.git synced 2024-06-01 04:10:49 +00:00
nativefier/app/src/main.ts
Adam Weeden adcf21a3df
macOS: Prompt for accessibility permissions if needed by Global Shortcuts using Media Keys (Fix #1120) (PR #1121)
When setting a media key (play, pause, next/previous track) as global shortcut in Mac OS 10.14+, accessibility permissions must be given to the app for it to work (see https://www.electronjs.org/docs/api/global-shortcut?q=MediaPlayPause#globalshortcutregisteraccelerator-callback).

This PR will accomplish the following on generated app launch:
- Check if global shortcuts are being setup
- Check if the host OS is Mac OS
- Check if the global shortcuts were one of the media keys
- If the above are true, check if the app has accessibility permissions
- If the app does not have the accessibility permissions it will ask the user if they would like to be prompted for these permissions, and then ask Mac OS to prompt for accessibility permissions.

~~As well, a new command line flag is added (`--no-accessibility-prompt`) to preventatively suppress these prompts if desired.~~

Screenshots of the new behavior:
![Screen Shot 2021-02-26 at 2 41 21 PM](https://user-images.githubusercontent.com/547567/109356260-76bfde00-784e-11eb-8c36-3a51b911b780.png)
![Screen Shot 2021-02-26 at 2 41 28 PM](https://user-images.githubusercontent.com/547567/109356266-79223800-784e-11eb-94eb-66437c05fd10.png)
![Screen Shot 2021-02-26 at 2 41 50 PM](https://user-images.githubusercontent.com/547567/109356270-7aebfb80-784e-11eb-9e90-e09bb49752c6.png)

Co-authored-by: Ronan Jouchet <ronan@jouchet.fr>
2021-02-28 10:24:14 -05:00

253 lines
7.5 KiB
TypeScript

import 'source-map-support/register';
import fs from 'fs';
import {
app,
crashReporter,
dialog,
globalShortcut,
systemPreferences,
BrowserWindow,
} 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 } from './helpers/helpers';
import { inferFlashPath } from './helpers/inferFlash';
// Entrypoint for Squirrel, a windows update framework. See https://github.com/nativefier/nativefier/pull/744
if (require('electron-squirrel-startup')) {
app.exit();
}
const appArgs = JSON.parse(fs.readFileSync(APP_ARGS_FILE_PATH, 'utf8'));
const OLD_BUILD_WARNING_THRESHOLD_DAYS = 60;
const OLD_BUILD_WARNING_THRESHOLD_MS =
OLD_BUILD_WARNING_THRESHOLD_DAYS * 24 * 60 * 60 * 1000;
const fileDownloadOptions = { ...appArgs.fileDownloadOptions };
electronDownload(fileDownloadOptions);
if (appArgs.processEnvs) {
// This is compatibility if just a string was passed.
if (typeof appArgs.processEnvs === 'string') {
process.env.processEnvs = appArgs.processEnvs;
} else {
Object.keys(appArgs.processEnvs).forEach((key) => {
/* eslint-env node */
process.env[key] = appArgs.processEnvs[key];
});
}
}
let mainWindow: BrowserWindow;
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);
}
if (appArgs.basicAuthUsername) {
app.commandLine.appendSwitch(
'basic-auth-username',
appArgs.basicAuthUsername,
);
}
if (appArgs.basicAuthPassword) {
app.commandLine.appendSwitch(
'basic-auth-password',
appArgs.basicAuthPassword,
);
}
const isRunningMacos = isOSX();
let currentBadgeCount = 0;
const setDockBadge = isRunningMacos
? (count: number, bounce = false) => {
app.dock.setBadge(count.toString());
if (bounce && count > currentBadgeCount) app.dock.bounce();
currentBadgeCount = count;
}
: () => undefined;
app.on('window-all-closed', () => {
if (!isOSX() || appArgs.fastQuit) {
app.quit();
}
});
app.on('activate', (event, hasVisibleWindows) => {
if (isOSX()) {
// this is called when the dock is clicked
if (!hasVisibleWindows) {
mainWindow.show();
}
}
});
app.on('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);
}
});
if (appArgs.crashReporter) {
app.on('will-finish-launching', () => {
crashReporter.start({
companyName: appArgs.companyName || '',
productName: appArgs.name,
submitURL: appArgs.crashReporter,
uploadToServer: true,
});
});
}
// 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', () => {
if (mainWindow) {
if (!mainWindow.isVisible()) {
// try
mainWindow.show();
}
if (mainWindow.isMinimized()) {
// minimized
mainWindow.restore();
}
mainWindow.focus();
}
});
app.on('ready', () => {
mainWindow = createMainWindow(appArgs, app.quit.bind(this), setDockBadge);
createTrayIcon(appArgs, mainWindow);
// Register global shortcuts
if (appArgs.globalShortcuts) {
appArgs.globalShortcuts.forEach((shortcut) => {
globalShortcut.register(shortcut.key, () => {
shortcut.inputEvents.forEach((inputEvent) => {
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(null, {
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
) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
dialog.showMessageBox(null, {
type: 'warning',
message: 'Old build detected',
detail:
'This app was built a long time ago. Nativefier uses the Chrome browser (through Electron), and it is dangerous to keep using an old version of it. You should rebuild this app with a recent Electron. Using the latest Nativefier will default to it, or you can pass it manually.',
});
}
});
}
app.on('new-window-for-tab', () => {
mainWindow.emit('new-tab');
});
app.on('login', (event, webContents, request, authInfo, callback) => {
// for http authentication
event.preventDefault();
if (
appArgs.basicAuthUsername !== null &&
appArgs.basicAuthPassword !== null
) {
callback(appArgs.basicAuthUsername, appArgs.basicAuthPassword);
} else {
createLoginWindow(callback);
}
});