diff --git a/.dockerignore b/.dockerignore index 5206a3c..003f817 100644 --- a/.dockerignore +++ b/.dockerignore @@ -47,3 +47,5 @@ node_modules *.iml out gen + +.vscode diff --git a/.gitignore b/.gitignore index 9005f8b..81a0c2f 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,5 @@ gen # Builds when testing npm pack nativefier*.tgz + +.vscode diff --git a/.npmignore b/.npmignore index 9957c84..bbb2abd 100644 --- a/.npmignore +++ b/.npmignore @@ -19,3 +19,4 @@ app/* !app/inject/ !app/nativefier.json !app/package.json +.vscode/ diff --git a/app/src/components/mainWindow.ts b/app/src/components/mainWindow.ts index 93e8892..85219c1 100644 --- a/app/src/components/mainWindow.ts +++ b/app/src/components/mainWindow.ts @@ -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') { diff --git a/app/src/main.ts b/app/src/main.ts index e1d5dde..f341c97 100644 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -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 && diff --git a/docs/api.md b/docs/api.md index 28ad340..0e804f4 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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 --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] diff --git a/docs/development.md b/docs/development.md index cfd88df..a311844 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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 diff --git a/package.json b/package.json index 55b0d6f..7789efb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/build/prepareElectronApp.ts b/src/build/prepareElectronApp.ts index 3394c47..7632886 100644 --- a/src/build/prepareElectronApp.ts +++ b/src/build/prepareElectronApp.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, diff --git a/src/options/model.ts b/src/options/model.ts index a04071f..7c29f47 100644 --- a/src/options/model.ts +++ b/src/options/model.ts @@ -8,6 +8,7 @@ export interface ElectronPackagerOptions extends electronPackager.Options { export interface AppOptions { packager: ElectronPackagerOptions; nativefier: { + accessibilityPrompt: boolean; alwaysOnTop: boolean; backgroundColor: string; basicAuthPassword: string; diff --git a/src/options/optionsMain.test.ts b/src/options/optionsMain.test.ts index 2e1b229..eec1d7a 100644 --- a/src/options/optionsMain.test.ts +++ b/src/options/optionsMain.test.ts @@ -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); +}); diff --git a/src/options/optionsMain.ts b/src/options/optionsMain.ts index 654d6b2..8942eba 100644 --- a/src/options/optionsMain.ts +++ b/src/options/optionsMain.ts @@ -47,6 +47,7 @@ export async function getOptions(rawOptions: any): Promise { }, }, nativefier: { + accessibilityPrompt: true, alwaysOnTop: rawOptions.alwaysOnTop || false, backgroundColor: rawOptions.backgroundColor || null, basicAuthPassword: rawOptions.basicAuthPassword || null,