mirror of
https://github.com/Llewellynvdm/nativefier.git
synced 2025-01-03 06:10:20 +00:00
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>
This commit is contained in:
parent
75aa10382b
commit
adcf21a3df
@ -47,3 +47,5 @@ node_modules
|
||||
*.iml
|
||||
out
|
||||
gen
|
||||
|
||||
.vscode
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -52,3 +52,5 @@ gen
|
||||
|
||||
# Builds when testing npm pack
|
||||
nativefier*.tgz
|
||||
|
||||
.vscode
|
||||
|
@ -19,3 +19,4 @@ app/*
|
||||
!app/inject/
|
||||
!app/nativefier.json
|
||||
!app/package.json
|
||||
.vscode/
|
||||
|
@ -17,6 +17,7 @@ import { initContextMenu } from './contextMenu';
|
||||
import { onNewWindowHelper } from './mainWindowHelpers';
|
||||
import { createMenu } from './menu';
|
||||
|
||||
export const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json');
|
||||
const ZOOM_INTERVAL = 0.1;
|
||||
|
||||
function hideWindow(
|
||||
@ -72,6 +73,16 @@ function setProxyRules(browserWindow: BrowserWindow, proxyRules): void {
|
||||
});
|
||||
}
|
||||
|
||||
export function saveAppArgs(newAppArgs: any) {
|
||||
try {
|
||||
fs.writeFileSync(APP_ARGS_FILE_PATH, JSON.stringify(newAppArgs));
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`WARNING: Ignored nativefier.json rewrital (${(err as Error).toString()})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {{}} nativefierOptions AppArgs from nativefier.json
|
||||
* @param {function} onAppQuit
|
||||
@ -133,17 +144,7 @@ export function createMainWindow(
|
||||
if (options.maximize) {
|
||||
mainWindow.maximize();
|
||||
options.maximize = undefined;
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, '..', 'nativefier.json'),
|
||||
JSON.stringify(options),
|
||||
);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`WARNING: Ignored nativefier.json rewrital (${(err as Error).toString()})`,
|
||||
);
|
||||
}
|
||||
saveAppArgs(options);
|
||||
}
|
||||
|
||||
if (options.tray === 'start-in-tray') {
|
||||
|
@ -1,19 +1,23 @@
|
||||
import 'source-map-support/register';
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import {
|
||||
app,
|
||||
crashReporter,
|
||||
globalShortcut,
|
||||
BrowserWindow,
|
||||
dialog,
|
||||
globalShortcut,
|
||||
systemPreferences,
|
||||
BrowserWindow,
|
||||
} from 'electron';
|
||||
import electronDownload from 'electron-dl';
|
||||
|
||||
import { createLoginWindow } from './components/loginWindow';
|
||||
import { createMainWindow } from './components/mainWindow';
|
||||
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';
|
||||
@ -23,7 +27,6 @@ if (require('electron-squirrel-startup')) {
|
||||
app.exit();
|
||||
}
|
||||
|
||||
const APP_ARGS_FILE_PATH = path.join(__dirname, '..', 'nativefier.json');
|
||||
const appArgs = JSON.parse(fs.readFileSync(APP_ARGS_FILE_PATH, 'utf8'));
|
||||
|
||||
const OLD_BUILD_WARNING_THRESHOLD_DAYS = 60;
|
||||
@ -168,6 +171,52 @@ if (shouldQuit) {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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 &&
|
||||
|
@ -220,6 +220,8 @@ The icon parameter can either be a `.icns` or a `.png` file if the [optional dep
|
||||
|
||||
If you have the optional dependencies `iconutil`, Imagemagick `convert`, and Imagemagick `identify` in your `PATH`, Nativefier will automatically convert the `.png` to a `.icns` for you.
|
||||
|
||||
On MacOS 10.14+, if you have set a global shortcut that includes a Media key, the user will need to be prompted for permissions to enable these keys in System Preferences > Security & Privacy > Accessibility.
|
||||
|
||||
###### Manually Converting `.icns`
|
||||
|
||||
[iConvertIcons](https://iconverticons.com/online/) can be used to convert `.pngs`, though it can be quite cumbersome.
|
||||
@ -766,7 +768,7 @@ nativefier <your-website> --browserwindow-options '{ "webPreferences": { "defaul
|
||||
--darwin-dark-mode-support
|
||||
```
|
||||
|
||||
Enables Dark Mode support on macOS 10.4+.
|
||||
Enables Dark Mode support on macOS 10.14+.
|
||||
|
||||
#### [background-color]
|
||||
|
||||
|
@ -47,7 +47,7 @@ Then run your nativefier app _through the command line too_ (to see logs & error
|
||||
your-test-site-win32-x64/your-test-site.exe
|
||||
|
||||
# Under macOS
|
||||
open -a YourTestSite.app
|
||||
./YourTestSite-darwin-x64/YourTestSite.app/Contents/MacOS/YourTestSite --verbose
|
||||
```
|
||||
|
||||
## Linting & formatting
|
||||
|
@ -36,7 +36,7 @@
|
||||
"changelog": "./docs/generate-changelog",
|
||||
"ci": "npm run lint && npm test",
|
||||
"clean": "rimraf lib/ app/lib/ app/dist/",
|
||||
"clean:full": "rimraf lib/ app/lib/ app/dist/ node_modules/ app/node_modules/",
|
||||
"clean:full": "rimraf lib/ app/lib/ app/dist/ app/node_modules/ node_modules/",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint:format": "prettier --write 'src/**/*.js' 'app/src/**/*.js'",
|
||||
"lint": "eslint . --ext .ts",
|
||||
|
@ -15,6 +15,7 @@ const writeFileAsync = promisify(fs.writeFile);
|
||||
*/
|
||||
function pickElectronAppArgs(options: AppOptions): any {
|
||||
return {
|
||||
accessibilityPrompt: options.nativefier.accessibilityPrompt,
|
||||
alwaysOnTop: options.nativefier.alwaysOnTop,
|
||||
appCopyright: options.packager.appCopyright,
|
||||
appVersion: options.packager.appVersion,
|
||||
|
@ -8,6 +8,7 @@ export interface ElectronPackagerOptions extends electronPackager.Options {
|
||||
export interface AppOptions {
|
||||
packager: ElectronPackagerOptions;
|
||||
nativefier: {
|
||||
accessibilityPrompt: boolean;
|
||||
alwaysOnTop: boolean;
|
||||
backgroundColor: string;
|
||||
basicAuthPassword: string;
|
||||
|
@ -23,3 +23,18 @@ test('it should call the async config', async () => {
|
||||
);
|
||||
expect(result.packager.targetUrl).toEqual(params.targetUrl);
|
||||
});
|
||||
|
||||
test('it should set the accessibility prompt option to true by default', async () => {
|
||||
const params = {
|
||||
targetUrl: 'https://example.com/',
|
||||
};
|
||||
const result = await getOptions(params);
|
||||
expect(asyncConfigMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
nativefier: expect.objectContaining({
|
||||
accessibilityPrompt: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(result.nativefier.accessibilityPrompt).toEqual(true);
|
||||
});
|
||||
|
@ -47,6 +47,7 @@ export async function getOptions(rawOptions: any): Promise<AppOptions> {
|
||||
},
|
||||
},
|
||||
nativefier: {
|
||||
accessibilityPrompt: true,
|
||||
alwaysOnTop: rawOptions.alwaysOnTop || false,
|
||||
backgroundColor: rawOptions.backgroundColor || null,
|
||||
basicAuthPassword: rawOptions.basicAuthPassword || null,
|
||||
|
Loading…
Reference in New Issue
Block a user