diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/components/services/content/ServiceWebview.js | 2 | ||||
-rw-r--r-- | src/index.js | 30 | ||||
-rw-r--r-- | src/models/Service.js | 21 | ||||
-rw-r--r-- | src/webview/badge.js | 33 | ||||
-rw-r--r-- | src/webview/find.js | 23 | ||||
-rw-r--r-- | src/webview/lib/RecipeWebview.js | 48 | ||||
-rw-r--r-- | src/webview/notifications.js | 88 | ||||
-rw-r--r-- | src/webview/recipe.js | 132 | ||||
-rw-r--r-- | src/webview/screenshare.js | 81 |
9 files changed, 286 insertions, 172 deletions
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js index 9e5fed996..4edbde5e2 100644 --- a/src/components/services/content/ServiceWebview.js +++ b/src/components/services/content/ServiceWebview.js | |||
@@ -83,7 +83,7 @@ class ServiceWebview extends Component { | |||
83 | useragent={service.userAgent} | 83 | useragent={service.userAgent} |
84 | disablewebsecurity={service.recipe.disablewebsecurity ? true : undefined} | 84 | disablewebsecurity={service.recipe.disablewebsecurity ? true : undefined} |
85 | allowpopups | 85 | allowpopups |
86 | webpreferences={`spellcheck=${isSpellcheckerEnabled ? 1 : 0}, contextIsolation=false`} | 86 | webpreferences={`spellcheck=${isSpellcheckerEnabled ? 1 : 0}`} |
87 | /> | 87 | /> |
88 | ); | 88 | ); |
89 | } | 89 | } |
diff --git a/src/index.js b/src/index.js index 91004aac7..63e6e3d0f 100644 --- a/src/index.js +++ b/src/index.js | |||
@@ -449,6 +449,36 @@ ipcMain.on('feature-basic-auth-cancel', () => { | |||
449 | authCallback = noop; | 449 | authCallback = noop; |
450 | }); | 450 | }); |
451 | 451 | ||
452 | // Handle synchronous messages from service webviews. | ||
453 | |||
454 | ipcMain.on('find-in-page', (e, text, options) => { | ||
455 | const { sender: webContents } = e; | ||
456 | if (webContents !== mainWindow.webContents && typeof (text) === 'string') { | ||
457 | const sanitizedOptions = {}; | ||
458 | for (const option of ['forward', 'findNext', 'matchCase']) { | ||
459 | if (option in options) { | ||
460 | sanitizedOptions[option] = !!options[option]; | ||
461 | } | ||
462 | } | ||
463 | const requestId = webContents.findInPage(text, sanitizedOptions); | ||
464 | debug('Find in page', text, options, requestId); | ||
465 | e.returnValue = requestId; | ||
466 | } else { | ||
467 | e.returnValue = null; | ||
468 | } | ||
469 | }); | ||
470 | |||
471 | ipcMain.on('stop-find-in-page', (e, action) => { | ||
472 | const { sender: webContents } = e; | ||
473 | if (webContents !== mainWindow.webContents) { | ||
474 | const validActions = ['clearSelection', 'keepSelection', 'activateSelection']; | ||
475 | if (validActions.includes(action)) { | ||
476 | webContents.stopFindInPage(action); | ||
477 | } | ||
478 | } | ||
479 | e.returnValue = null; | ||
480 | }); | ||
481 | |||
452 | // Quit when all windows are closed. | 482 | // Quit when all windows are closed. |
453 | app.on('window-all-closed', () => { | 483 | app.on('window-all-closed', () => { |
454 | // On OS X it is common for applications and their menu bar | 484 | // On OS X it is common for applications and their menu bar |
diff --git a/src/models/Service.js b/src/models/Service.js index 74e100ea4..36b310da1 100644 --- a/src/models/Service.js +++ b/src/models/Service.js | |||
@@ -275,11 +275,17 @@ export default class Service { | |||
275 | debug(this.name, 'modifyRequestHeaders is not defined in the recipe'); | 275 | debug(this.name, 'modifyRequestHeaders is not defined in the recipe'); |
276 | } | 276 | } |
277 | 277 | ||
278 | this.webview.addEventListener('ipc-message', e => handleIPCMessage({ | 278 | this.webview.addEventListener('ipc-message', async (e) => { |
279 | serviceId: this.id, | 279 | if (e.channel === 'inject-js-unsafe') { |
280 | channel: e.channel, | 280 | await Promise.all(e.args.map(script => this.webview.executeJavaScript(`"use strict"; (() => { ${script} })();`))); |
281 | args: e.args, | 281 | } else { |
282 | })); | 282 | handleIPCMessage({ |
283 | serviceId: this.id, | ||
284 | channel: e.channel, | ||
285 | args: e.args, | ||
286 | }); | ||
287 | } | ||
288 | }); | ||
283 | 289 | ||
284 | this.webview.addEventListener('new-window', (event, url, frameName, options) => { | 290 | this.webview.addEventListener('new-window', (event, url, frameName, options) => { |
285 | debug('new-window', event, url, frameName, options); | 291 | debug('new-window', event, url, frameName, options); |
@@ -334,6 +340,11 @@ export default class Service { | |||
334 | this.hasCrashed = true; | 340 | this.hasCrashed = true; |
335 | }); | 341 | }); |
336 | 342 | ||
343 | this.webview.addEventListener('found-in-page', ({ result }) => { | ||
344 | debug('Found in page', result); | ||
345 | this.webview.send('found-in-page', result); | ||
346 | }); | ||
347 | |||
337 | webviewWebContents.on('login', (event, request, authInfo, callback) => { | 348 | webviewWebContents.on('login', (event, request, authInfo, callback) => { |
338 | // const authCallback = callback; | 349 | // const authCallback = callback; |
339 | debug('browser login event', authInfo); | 350 | debug('browser login event', authInfo); |
diff --git a/src/webview/badge.js b/src/webview/badge.js new file mode 100644 index 000000000..1e02fb56a --- /dev/null +++ b/src/webview/badge.js | |||
@@ -0,0 +1,33 @@ | |||
1 | const { ipcRenderer } = require('electron'); | ||
2 | |||
3 | const debug = require('debug')('Ferdi:Plugin:BadgeHandler'); | ||
4 | |||
5 | export class BadgeHandler { | ||
6 | constructor() { | ||
7 | this.countCache = { | ||
8 | direct: 0, | ||
9 | indirect: 0, | ||
10 | }; | ||
11 | } | ||
12 | |||
13 | setBadge(direct, indirect) { | ||
14 | if (this.countCache.direct === direct | ||
15 | && this.countCache.indirect === indirect) return; | ||
16 | |||
17 | // Parse number to integer | ||
18 | // This will correct errors that recipes may introduce, e.g. | ||
19 | // by sending a String instead of an integer | ||
20 | const directInt = parseInt(direct, 10); | ||
21 | const indirectInt = parseInt(indirect, 10); | ||
22 | |||
23 | const count = { | ||
24 | direct: Math.max(directInt, 0), | ||
25 | indirect: Math.max(indirectInt, 0), | ||
26 | }; | ||
27 | |||
28 | ipcRenderer.sendToHost('message-counts', count); | ||
29 | Object.assign(this.countCache, count); | ||
30 | |||
31 | debug('Sending badge count to host', count); | ||
32 | } | ||
33 | } | ||
diff --git a/src/webview/find.js b/src/webview/find.js new file mode 100644 index 000000000..040811d68 --- /dev/null +++ b/src/webview/find.js | |||
@@ -0,0 +1,23 @@ | |||
1 | import { ipcRenderer } from 'electron'; | ||
2 | import { FindInPage as ElectronFindInPage } from 'electron-find'; | ||
3 | |||
4 | // Shim to expose webContents functionality to electron-find without @electron/remote | ||
5 | const webContentsShim = { | ||
6 | findInPage: (text, options = {}) => ipcRenderer.sendSync('find-in-page', text, options), | ||
7 | stopFindInPage: (action) => { | ||
8 | ipcRenderer.sendSync('stop-find-in-page', action); | ||
9 | }, | ||
10 | on: (eventName, listener) => { | ||
11 | if (eventName === 'found-in-page') { | ||
12 | ipcRenderer.on('found-in-page', (_, result) => { | ||
13 | listener({ sender: this }, result); | ||
14 | }); | ||
15 | } | ||
16 | }, | ||
17 | }; | ||
18 | |||
19 | export default class FindInPage extends ElectronFindInPage { | ||
20 | constructor(options = {}) { | ||
21 | super(webContentsShim, options); | ||
22 | } | ||
23 | } | ||
diff --git a/src/webview/lib/RecipeWebview.js b/src/webview/lib/RecipeWebview.js index b8fe7dc52..3bb9352f6 100644 --- a/src/webview/lib/RecipeWebview.js +++ b/src/webview/lib/RecipeWebview.js | |||
@@ -1,14 +1,12 @@ | |||
1 | import { ipcRenderer } from 'electron'; | 1 | import { ipcRenderer } from 'electron'; |
2 | import { pathExistsSync, readFile } from 'fs-extra'; | 2 | import { exists, pathExistsSync, readFile } from 'fs-extra'; |
3 | 3 | ||
4 | const debug = require('debug')('Ferdi:Plugin:RecipeWebview'); | 4 | const debug = require('debug')('Ferdi:Plugin:RecipeWebview'); |
5 | 5 | ||
6 | class RecipeWebview { | 6 | class RecipeWebview { |
7 | constructor() { | 7 | constructor(badgeHandler, notificationsHandler) { |
8 | this.countCache = { | 8 | this.badgeHandler = badgeHandler; |
9 | direct: 0, | 9 | this.notificationsHandler = notificationsHandler; |
10 | indirect: 0, | ||
11 | }; | ||
12 | 10 | ||
13 | ipcRenderer.on('poll', () => { | 11 | ipcRenderer.on('poll', () => { |
14 | this.loopFunc(); | 12 | this.loopFunc(); |
@@ -45,24 +43,7 @@ class RecipeWebview { | |||
45 | * me directly to me eg. in a channel | 43 | * me directly to me eg. in a channel |
46 | */ | 44 | */ |
47 | setBadge(direct = 0, indirect = 0) { | 45 | setBadge(direct = 0, indirect = 0) { |
48 | if (this.countCache.direct === direct | 46 | this.badgeHandler.setBadge(direct, indirect); |
49 | && this.countCache.indirect === indirect) return; | ||
50 | |||
51 | // Parse number to integer | ||
52 | // This will correct errors that recipes may introduce, e.g. | ||
53 | // by sending a String instead of an integer | ||
54 | const directInt = parseInt(direct, 10); | ||
55 | const indirectInt = parseInt(indirect, 10); | ||
56 | |||
57 | const count = { | ||
58 | direct: Math.max(directInt, 0), | ||
59 | indirect: Math.max(indirectInt, 0), | ||
60 | }; | ||
61 | |||
62 | ipcRenderer.sendToHost('message-counts', count); | ||
63 | Object.assign(this.countCache, count); | ||
64 | |||
65 | debug('Sending badge count to host', count); | ||
66 | } | 47 | } |
67 | 48 | ||
68 | /** | 49 | /** |
@@ -85,6 +66,23 @@ class RecipeWebview { | |||
85 | }); | 66 | }); |
86 | } | 67 | } |
87 | 68 | ||
69 | injectJSUnsafe(...files) { | ||
70 | Promise.all(files.map(async (file) => { | ||
71 | if (await exists(file)) { | ||
72 | const data = await readFile(file, 'utf8'); | ||
73 | return data; | ||
74 | } | ||
75 | debug('Script not found', file); | ||
76 | return null; | ||
77 | })).then(async (scripts) => { | ||
78 | const scriptsFound = scripts.filter(script => script !== null); | ||
79 | if (scriptsFound.length > 0) { | ||
80 | debug('Inject scripts to main world', scriptsFound); | ||
81 | ipcRenderer.sendToHost('inject-js-unsafe', ...scriptsFound); | ||
82 | } | ||
83 | }); | ||
84 | } | ||
85 | |||
88 | /** | 86 | /** |
89 | * Set a custom handler for turning on and off dark mode | 87 | * Set a custom handler for turning on and off dark mode |
90 | * | 88 | * |
@@ -96,7 +94,7 @@ class RecipeWebview { | |||
96 | 94 | ||
97 | onNotify(fn) { | 95 | onNotify(fn) { |
98 | if (typeof fn === 'function') { | 96 | if (typeof fn === 'function') { |
99 | window.Notification.prototype.onNotify = fn; | 97 | this.notificationsHandler.onNotify = fn; |
100 | } | 98 | } |
101 | } | 99 | } |
102 | 100 | ||
diff --git a/src/webview/notifications.js b/src/webview/notifications.js index 021f05cc3..39a515143 100644 --- a/src/webview/notifications.js +++ b/src/webview/notifications.js | |||
@@ -3,49 +3,65 @@ import uuidV1 from 'uuid/v1'; | |||
3 | 3 | ||
4 | const debug = require('debug')('Ferdi:Notifications'); | 4 | const debug = require('debug')('Ferdi:Notifications'); |
5 | 5 | ||
6 | class Notification { | 6 | export class NotificationsHandler { |
7 | static permission = 'granted'; | 7 | onNotify = data => data; |
8 | 8 | ||
9 | constructor(title = '', options = {}) { | 9 | displayNotification(title, options) { |
10 | debug('New notification', title, options); | 10 | return new Promise((resolve) => { |
11 | this.title = title; | 11 | debug('New notification', title, options); |
12 | this.options = options; | 12 | |
13 | this.notificationId = uuidV1(); | 13 | const notificationId = uuidV1(); |
14 | 14 | ||
15 | ipcRenderer.sendToHost('notification', this.onNotify({ | 15 | ipcRenderer.sendToHost('notification', this.onNotify({ |
16 | title: this.title, | 16 | title, |
17 | options: this.options, | 17 | options, |
18 | notificationId: this.notificationId, | 18 | notificationId, |
19 | })); | 19 | })); |
20 | 20 | ||
21 | ipcRenderer.once(`notification-onclick:${this.notificationId}`, () => { | 21 | ipcRenderer.once(`notification-onclick:${notificationId}`, () => { |
22 | if (typeof this.onclick === 'function') { | 22 | resolve(); |
23 | this.onclick(); | 23 | }); |
24 | } | ||
25 | }); | 24 | }); |
26 | } | 25 | } |
26 | } | ||
27 | 27 | ||
28 | static requestPermission(cb = null) { | 28 | export const notificationsClassDefinition = `(() => { |
29 | if (!cb) { | 29 | class Notification { |
30 | return new Promise((resolve) => { | 30 | static permission = 'granted'; |
31 | resolve(Notification.permission); | ||
32 | }); | ||
33 | } | ||
34 | 31 | ||
35 | if (typeof (cb) === 'function') { | 32 | constructor(title = '', options = {}) { |
36 | return cb(Notification.permission); | 33 | this.title = title; |
34 | this.options = options; | ||
35 | window.ferdi.displayNotification(title, options) | ||
36 | .then(() => { | ||
37 | if (typeof (this.onClick) === 'function') { | ||
38 | this.onClick(); | ||
39 | } | ||
40 | }); | ||
37 | } | 41 | } |
38 | 42 | ||
39 | return Notification.permission; | 43 | static requestPermission(cb = null) { |
40 | } | 44 | if (!cb) { |
45 | return new Promise((resolve) => { | ||
46 | resolve(Notification.permission); | ||
47 | }); | ||
48 | } | ||
41 | 49 | ||
42 | onNotify(data) { | 50 | if (typeof (cb) === 'function') { |
43 | return data; | 51 | return cb(Notification.permission); |
44 | } | 52 | } |
45 | 53 | ||
46 | onClick() {} | 54 | return Notification.permission; |
55 | } | ||
47 | 56 | ||
48 | close() {} | 57 | onNotify(data) { |
49 | } | 58 | return data; |
59 | } | ||
60 | |||
61 | onClick() {} | ||
62 | |||
63 | close() {} | ||
64 | } | ||
50 | 65 | ||
51 | window.Notification = Notification; | 66 | window.Notification = Notification; |
67 | })();`; | ||
diff --git a/src/webview/recipe.js b/src/webview/recipe.js index 8da45864b..d143675dc 100644 --- a/src/webview/recipe.js +++ b/src/webview/recipe.js | |||
@@ -1,11 +1,9 @@ | |||
1 | /* eslint-disable import/first */ | 1 | /* eslint-disable import/first */ |
2 | import { ipcRenderer } from 'electron'; | 2 | import { contextBridge, ipcRenderer } from 'electron'; |
3 | import { getCurrentWebContents } from '@electron/remote'; | ||
4 | import path from 'path'; | 3 | import path from 'path'; |
5 | import { autorun, computed, observable } from 'mobx'; | 4 | import { autorun, computed, observable } from 'mobx'; |
6 | import fs from 'fs-extra'; | 5 | import fs from 'fs-extra'; |
7 | import { debounce } from 'lodash'; | 6 | import { debounce } from 'lodash'; |
8 | import { FindInPage } from 'electron-find'; | ||
9 | 7 | ||
10 | // For some services darkreader tries to use the chrome extension message API | 8 | // For some services darkreader tries to use the chrome extension message API |
11 | // This will cause the service to fail loading | 9 | // This will cause the service to fail loading |
@@ -23,16 +21,81 @@ import customDarkModeCss from './darkmode/custom'; | |||
23 | import RecipeWebview from './lib/RecipeWebview'; | 21 | import RecipeWebview from './lib/RecipeWebview'; |
24 | import Userscript from './lib/Userscript'; | 22 | import Userscript from './lib/Userscript'; |
25 | 23 | ||
26 | import { switchDict, getSpellcheckerLocaleByFuzzyIdentifier } from './spellchecker'; | 24 | import { BadgeHandler } from './badge'; |
27 | import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode'; | ||
28 | import contextMenu from './contextMenu'; | 25 | import contextMenu from './contextMenu'; |
29 | import './notifications'; | 26 | import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode'; |
30 | import { screenShareCss } from './screenshare'; | 27 | import FindInPage from './find'; |
28 | import { NotificationsHandler, notificationsClassDefinition } from './notifications'; | ||
29 | import { getDisplayMediaSelector, screenShareCss, screenShareJs } from './screenshare'; | ||
30 | import { switchDict, getSpellcheckerLocaleByFuzzyIdentifier } from './spellchecker'; | ||
31 | 31 | ||
32 | import { DEFAULT_APP_SETTINGS, isDevMode } from '../environment'; | 32 | import { DEFAULT_APP_SETTINGS } from '../environment'; |
33 | 33 | ||
34 | const debug = require('debug')('Ferdi:Plugin'); | 34 | const debug = require('debug')('Ferdi:Plugin'); |
35 | 35 | ||
36 | const badgeHandler = new BadgeHandler(); | ||
37 | |||
38 | const notificationsHandler = new NotificationsHandler(); | ||
39 | |||
40 | // Patching window.open | ||
41 | const originalWindowOpen = window.open; | ||
42 | |||
43 | window.open = (url, frameName, features) => { | ||
44 | debug('window.open', url, frameName, features); | ||
45 | if (!url) { | ||
46 | // The service hasn't yet supplied a URL (as used in Skype). | ||
47 | // Return a new dummy window object and wait for the service to change the properties | ||
48 | const newWindow = { | ||
49 | location: { | ||
50 | href: '', | ||
51 | }, | ||
52 | }; | ||
53 | |||
54 | const checkInterval = setInterval(() => { | ||
55 | // Has the service changed the URL yet? | ||
56 | if (newWindow.location.href !== '') { | ||
57 | if (features) { | ||
58 | originalWindowOpen(newWindow.location.href, frameName, features); | ||
59 | } else { | ||
60 | // Open the new URL | ||
61 | ipcRenderer.sendToHost('new-window', newWindow.location.href); | ||
62 | } | ||
63 | clearInterval(checkInterval); | ||
64 | } | ||
65 | }, 0); | ||
66 | |||
67 | setTimeout(() => { | ||
68 | // Stop checking for location changes after 1 second | ||
69 | clearInterval(checkInterval); | ||
70 | }, 1000); | ||
71 | |||
72 | return newWindow; | ||
73 | } | ||
74 | |||
75 | // We need to differentiate if the link should be opened in a popup or in the systems default browser | ||
76 | if (!frameName && !features && typeof features !== 'string') { | ||
77 | return ipcRenderer.sendToHost('new-window', url); | ||
78 | } | ||
79 | |||
80 | if (url) { | ||
81 | return originalWindowOpen(url, frameName, features); | ||
82 | } | ||
83 | }; | ||
84 | |||
85 | // We can't override APIs here, so we first expose functions via window.ferdi, | ||
86 | // then overwrite the corresponding field of the window object by injected JS. | ||
87 | contextBridge.exposeInMainWorld('ferdi', { | ||
88 | open: window.open, | ||
89 | setBadge: (direct, indirect) => badgeHandler.setBadge(direct || 0, indirect || 0), | ||
90 | displayNotification: (title, options) => notificationsHandler.displayNotification(title, options), | ||
91 | getDisplayMediaSelector, | ||
92 | }); | ||
93 | |||
94 | ipcRenderer.sendToHost('inject-js-unsafe', | ||
95 | 'window.open = window.ferdi.open;', | ||
96 | notificationsClassDefinition, | ||
97 | screenShareJs); | ||
98 | |||
36 | class RecipeController { | 99 | class RecipeController { |
37 | @observable settings = { | 100 | @observable settings = { |
38 | overrideSpellcheckerLanguage: false, | 101 | overrideSpellcheckerLanguage: false, |
@@ -97,7 +160,7 @@ class RecipeController { | |||
97 | autorun(() => this.update()); | 160 | autorun(() => this.update()); |
98 | 161 | ||
99 | document.addEventListener('DOMContentLoaded', () => { | 162 | document.addEventListener('DOMContentLoaded', () => { |
100 | this.findInPage = new FindInPage(getCurrentWebContents(), { | 163 | this.findInPage = new FindInPage({ |
101 | inputFocusColor: '#CE9FFC', | 164 | inputFocusColor: '#CE9FFC', |
102 | textColor: '#212121', | 165 | textColor: '#212121', |
103 | }); | 166 | }); |
@@ -111,7 +174,7 @@ class RecipeController { | |||
111 | // Delete module from cache | 174 | // Delete module from cache |
112 | delete require.cache[require.resolve(modulePath)]; | 175 | delete require.cache[require.resolve(modulePath)]; |
113 | try { | 176 | try { |
114 | this.recipe = new RecipeWebview(); | 177 | this.recipe = new RecipeWebview(badgeHandler, notificationsHandler); |
115 | // eslint-disable-next-line | 178 | // eslint-disable-next-line |
116 | require(modulePath)(this.recipe, {...config, recipe,}); | 179 | require(modulePath)(this.recipe, {...config, recipe,}); |
117 | debug('Initialize Recipe', config, recipe); | 180 | debug('Initialize Recipe', config, recipe); |
@@ -327,52 +390,3 @@ class RecipeController { | |||
327 | /* eslint-disable no-new */ | 390 | /* eslint-disable no-new */ |
328 | new RecipeController(); | 391 | new RecipeController(); |
329 | /* eslint-enable no-new */ | 392 | /* eslint-enable no-new */ |
330 | |||
331 | // Patching window.open | ||
332 | const originalWindowOpen = window.open; | ||
333 | |||
334 | window.open = (url, frameName, features) => { | ||
335 | debug('window.open', url, frameName, features); | ||
336 | if (!url) { | ||
337 | // The service hasn't yet supplied a URL (as used in Skype). | ||
338 | // Return a new dummy window object and wait for the service to change the properties | ||
339 | const newWindow = { | ||
340 | location: { | ||
341 | href: '', | ||
342 | }, | ||
343 | }; | ||
344 | |||
345 | const checkInterval = setInterval(() => { | ||
346 | // Has the service changed the URL yet? | ||
347 | if (newWindow.location.href !== '') { | ||
348 | if (features) { | ||
349 | originalWindowOpen(newWindow.location.href, frameName, features); | ||
350 | } else { | ||
351 | // Open the new URL | ||
352 | ipcRenderer.sendToHost('new-window', newWindow.location.href); | ||
353 | } | ||
354 | clearInterval(checkInterval); | ||
355 | } | ||
356 | }, 0); | ||
357 | |||
358 | setTimeout(() => { | ||
359 | // Stop checking for location changes after 1 second | ||
360 | clearInterval(checkInterval); | ||
361 | }, 1000); | ||
362 | |||
363 | return newWindow; | ||
364 | } | ||
365 | |||
366 | // We need to differentiate if the link should be opened in a popup or in the systems default browser | ||
367 | if (!frameName && !features && typeof features !== 'string') { | ||
368 | return ipcRenderer.sendToHost('new-window', url); | ||
369 | } | ||
370 | |||
371 | if (url) { | ||
372 | return originalWindowOpen(url, frameName, features); | ||
373 | } | ||
374 | }; | ||
375 | |||
376 | if (isDevMode) { | ||
377 | window.log = console.log; | ||
378 | } | ||
diff --git a/src/webview/screenshare.js b/src/webview/screenshare.js index 84d2e1e95..ab548a625 100644 --- a/src/webview/screenshare.js +++ b/src/webview/screenshare.js | |||
@@ -2,6 +2,27 @@ import { desktopCapturer } from 'electron'; | |||
2 | 2 | ||
3 | const CANCEL_ID = 'desktop-capturer-selection__cancel'; | 3 | const CANCEL_ID = 'desktop-capturer-selection__cancel'; |
4 | 4 | ||
5 | export async function getDisplayMediaSelector() { | ||
6 | const sources = await desktopCapturer.getSources({ types: ['screen', 'window'] }); | ||
7 | return `<div class="desktop-capturer-selection__scroller"> | ||
8 | <ul class="desktop-capturer-selection__list"> | ||
9 | ${sources.map(({ id, name, thumbnail }) => ` | ||
10 | <li class="desktop-capturer-selection__item"> | ||
11 | <button class="desktop-capturer-selection__btn" data-id="${id}" title="${name}"> | ||
12 | <img class="desktop-capturer-selection__thumbnail" src="${thumbnail.toDataURL()}" /> | ||
13 | <span class="desktop-capturer-selection__name">${name}</span> | ||
14 | </button> | ||
15 | </li> | ||
16 | `).join('')} | ||
17 | <li class="desktop-capturer-selection__item"> | ||
18 | <button class="desktop-capturer-selection__btn" data-id="${CANCEL_ID}" title="Cancel"> | ||
19 | <span class="desktop-capturer-selection__name desktop-capturer-selection__name--cancel">Cancel</span> | ||
20 | </button> | ||
21 | </li> | ||
22 | </ul> | ||
23 | </div>`; | ||
24 | } | ||
25 | |||
5 | export const screenShareCss = ` | 26 | export const screenShareCss = ` |
6 | .desktop-capturer-selection { | 27 | .desktop-capturer-selection { |
7 | position: fixed; | 28 | position: fixed; |
@@ -72,38 +93,12 @@ export const screenShareCss = ` | |||
72 | } | 93 | } |
73 | `; | 94 | `; |
74 | 95 | ||
75 | // Patch getDisplayMedia for screen sharing | 96 | export const screenShareJs = ` |
76 | window.navigator.mediaDevices.getDisplayMedia = () => async (resolve, reject) => { | 97 | window.navigator.mediaDevices.getDisplayMedia = () => new Promise(async (resolve, reject) => { |
77 | try { | 98 | try { |
78 | const sources = await desktopCapturer.getSources({ | ||
79 | types: ['screen', 'window'], | ||
80 | }); | ||
81 | |||
82 | const selectionElem = document.createElement('div'); | 99 | const selectionElem = document.createElement('div'); |
83 | selectionElem.classList = 'desktop-capturer-selection'; | 100 | selectionElem.classList = ['desktop-capturer-selection']; |
84 | selectionElem.innerHTML = ` | 101 | selectionElem.innerHTML = await window.ferdi.getDisplayMediaSelector(); |
85 | <div class="desktop-capturer-selection__scroller"> | ||
86 | <ul class="desktop-capturer-selection__list"> | ||
87 | ${sources | ||
88 | .map( | ||
89 | ({ id, name, thumbnail }) => ` | ||
90 | <li class="desktop-capturer-selection__item"> | ||
91 | <button class="desktop-capturer-selection__btn" data-id="${id}" title="${name}"> | ||
92 | <img class="desktop-capturer-selection__thumbnail" src="${thumbnail.toDataURL()}" /> | ||
93 | <span class="desktop-capturer-selection__name">${name}</span> | ||
94 | </button> | ||
95 | </li> | ||
96 | `, | ||
97 | ) | ||
98 | .join('')} | ||
99 | <li class="desktop-capturer-selection__item"> | ||
100 | <button class="desktop-capturer-selection__btn" data-id="${CANCEL_ID}" title="Cancel"> | ||
101 | <span class="desktop-capturer-selection__name desktop-capturer-selection__name--cancel">Cancel</span> | ||
102 | </button> | ||
103 | </li> | ||
104 | </ul> | ||
105 | </div> | ||
106 | `; | ||
107 | document.body.appendChild(selectionElem); | 102 | document.body.appendChild(selectionElem); |
108 | 103 | ||
109 | document | 104 | document |
@@ -112,25 +107,18 @@ window.navigator.mediaDevices.getDisplayMedia = () => async (resolve, reject) => | |||
112 | button.addEventListener('click', async () => { | 107 | button.addEventListener('click', async () => { |
113 | try { | 108 | try { |
114 | const id = button.getAttribute('data-id'); | 109 | const id = button.getAttribute('data-id'); |
115 | if (id === CANCEL_ID) { | 110 | if (id === '${CANCEL_ID}') { |
116 | reject(new Error('Cancelled by user')); | 111 | reject(new Error('Cancelled by user')); |
117 | } else { | 112 | } else { |
118 | const mediaSource = sources.find((source) => source.id === id); | 113 | const stream = await window.navigator.mediaDevices.getUserMedia({ |
119 | if (!mediaSource) { | 114 | audio: false, |
120 | throw new Error(`Source with id ${id} does not exist`); | 115 | video: { |
121 | } | 116 | mandatory: { |
122 | 117 | chromeMediaSource: 'desktop', | |
123 | const stream = await window.navigator.mediaDevices.getUserMedia( | 118 | chromeMediaSourceId: id, |
124 | { | ||
125 | audio: false, | ||
126 | video: { | ||
127 | mandatory: { | ||
128 | chromeMediaSource: 'desktop', | ||
129 | chromeMediaSourceId: mediaSource.id, | ||
130 | }, | ||
131 | }, | 119 | }, |
132 | }, | 120 | }, |
133 | ); | 121 | }); |
134 | resolve(stream); | 122 | resolve(stream); |
135 | } | 123 | } |
136 | } catch (err) { | 124 | } catch (err) { |
@@ -143,4 +131,5 @@ window.navigator.mediaDevices.getDisplayMedia = () => async (resolve, reject) => | |||
143 | } catch (err) { | 131 | } catch (err) { |
144 | reject(err); | 132 | reject(err); |
145 | } | 133 | } |
146 | }; | 134 | }); |
135 | `; | ||