mirror of
https://github.com/scratchfoundation/scratch-desktop.git
synced 2025-01-09 22:22:34 -05:00
improve user experience around mic/camera permission
- ask for permission when trying to use a feature, not on startup - if permission is denied, explain the consequence and provide a hint for fixing it
This commit is contained in:
parent
b26c0b6bd3
commit
74968704c8
1 changed files with 111 additions and 19 deletions
|
@ -18,22 +18,48 @@ const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||||
// global window references prevent them from being garbage-collected
|
// global window references prevent them from being garbage-collected
|
||||||
const _windows = {};
|
const _windows = {};
|
||||||
|
|
||||||
const createWindow = ({search = null, url = 'index.html', ...browserWindowOptions}) => {
|
const displayPermissionDeniedWarning = (browserWindow, permissionType) => {
|
||||||
const window = new BrowserWindow({
|
let title;
|
||||||
useContentSize: true,
|
let message;
|
||||||
show: false,
|
switch (permissionType) {
|
||||||
webPreferences: {
|
case 'camera':
|
||||||
nodeIntegration: true
|
title = 'Camera Permission Denied';
|
||||||
},
|
message = 'Permission to use the camera has been denied. ' +
|
||||||
...browserWindowOptions
|
'Scratch will not be able to take a photo or use video sensing blocks.';
|
||||||
});
|
break;
|
||||||
const webContents = window.webContents;
|
case 'microphone':
|
||||||
|
title = 'Microphone Permission Denied';
|
||||||
if (isDevelopment) {
|
message = 'Permission to use the microphone has been denied. ' +
|
||||||
webContents.openDevTools({mode: 'detach', activate: true});
|
'Scratch will not be able to record sounds or detect loudness.';
|
||||||
|
break;
|
||||||
|
default: // shouldn't ever happen...
|
||||||
|
title = 'Permission Denied';
|
||||||
|
message = 'A permission has been denied.';
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullUrl = formatUrl(isDevelopment ?
|
let instructions;
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'darwin':
|
||||||
|
instructions = 'To change Scratch permissions, please check "Security & Privacy" in System Preferences.';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
instructions = 'To change Scratch permissions, please check your system settings and restart Scratch.';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
message = `${message}\n\n${instructions}`;
|
||||||
|
|
||||||
|
dialog.showMessageBox(browserWindow, {type: 'warning', title, message});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an absolute URL from a relative one, optionally adding search query parameters.
|
||||||
|
* The base of the URL will depend on whether or not the application is running in development mode.
|
||||||
|
* @param {string} url - the relative URL, like 'index.html'
|
||||||
|
* @param {*} search - the optional "search" parameters (the part of the URL after '?'), like "route=about"
|
||||||
|
* @returns {string} - an absolute URL as a string
|
||||||
|
*/
|
||||||
|
const makeFullUrl = (url, search = null) =>
|
||||||
|
encodeURI(formatUrl(isDevelopment ?
|
||||||
{ // Webpack Dev Server
|
{ // Webpack Dev Server
|
||||||
hostname: 'localhost',
|
hostname: 'localhost',
|
||||||
pathname: url,
|
pathname: url,
|
||||||
|
@ -47,7 +73,77 @@ const createWindow = ({search = null, url = 'index.html', ...browserWindowOption
|
||||||
search,
|
search,
|
||||||
slashes: true
|
slashes: true
|
||||||
}
|
}
|
||||||
);
|
));
|
||||||
|
|
||||||
|
const handlePermissionRequest = async (webContents, permission, callback, details) => {
|
||||||
|
if (webContents !== _windows.main.webContents) {
|
||||||
|
// deny: request came from somewhere other than the main window's web contents
|
||||||
|
return callback(false);
|
||||||
|
}
|
||||||
|
if (!details.isMainFrame) {
|
||||||
|
// deny: request came from a subframe of the main window, not the main frame
|
||||||
|
return callback(false);
|
||||||
|
}
|
||||||
|
if (permission !== 'media') {
|
||||||
|
// deny: request is for some other kind of access like notifications or pointerLock
|
||||||
|
return callback(false);
|
||||||
|
}
|
||||||
|
const requiredBase = makeFullUrl('/');
|
||||||
|
if (details.requestingUrl.indexOf(requiredBase) !== 0) {
|
||||||
|
// deny: request came from a URL outside of our "sandbox"
|
||||||
|
return callback(false);
|
||||||
|
}
|
||||||
|
let askForMicrophone = false;
|
||||||
|
let askForCamera = false;
|
||||||
|
for (const mediaType of details.mediaTypes) {
|
||||||
|
switch (mediaType) {
|
||||||
|
case 'audio':
|
||||||
|
askForMicrophone = true;
|
||||||
|
break;
|
||||||
|
case 'video':
|
||||||
|
askForCamera = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// deny: unhandled media type
|
||||||
|
return callback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parentWindow = _windows.main; // if we ever allow media in non-main windows we'll also need to change this
|
||||||
|
if (askForMicrophone) {
|
||||||
|
const microphoneResult = await systemPreferences.askForMediaAccess('microphone');
|
||||||
|
if (!microphoneResult) {
|
||||||
|
displayPermissionDeniedWarning(parentWindow, 'microphone');
|
||||||
|
return callback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (askForCamera) {
|
||||||
|
const cameraResult = await systemPreferences.askForMediaAccess('camera');
|
||||||
|
if (!cameraResult) {
|
||||||
|
displayPermissionDeniedWarning(parentWindow, 'camera');
|
||||||
|
return callback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return callback(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createWindow = ({search = null, url = 'index.html', ...browserWindowOptions}) => {
|
||||||
|
const window = new BrowserWindow({
|
||||||
|
useContentSize: true,
|
||||||
|
show: false,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true
|
||||||
|
},
|
||||||
|
...browserWindowOptions
|
||||||
|
});
|
||||||
|
const webContents = window.webContents;
|
||||||
|
|
||||||
|
webContents.session.setPermissionRequestHandler(handlePermissionRequest);
|
||||||
|
|
||||||
|
if (isDevelopment) {
|
||||||
|
webContents.openDevTools({mode: 'detach', activate: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullUrl = makeFullUrl(url, search);
|
||||||
window.loadURL(fullUrl);
|
window.loadURL(fullUrl);
|
||||||
|
|
||||||
return window;
|
return window;
|
||||||
|
@ -140,10 +236,6 @@ const createMainWindow = () => {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
const osxMenu = Menu.buildFromTemplate(MacOSMenu(app));
|
const osxMenu = Menu.buildFromTemplate(MacOSMenu(app));
|
||||||
Menu.setApplicationMenu(osxMenu);
|
Menu.setApplicationMenu(osxMenu);
|
||||||
(async () => {
|
|
||||||
await systemPreferences.askForMediaAccess('microphone');
|
|
||||||
await systemPreferences.askForMediaAccess('camera');
|
|
||||||
})();
|
|
||||||
} else {
|
} else {
|
||||||
// disable menu for other platforms
|
// disable menu for other platforms
|
||||||
Menu.setApplicationMenu(null);
|
Menu.setApplicationMenu(null);
|
||||||
|
|
Loading…
Reference in a new issue