diff options
Diffstat (limited to 'src/webview')
-rw-r--r-- | src/webview/badge.js | 33 | ||||
-rw-r--r-- | src/webview/contextMenuBuilder.js | 25 | ||||
-rw-r--r-- | src/webview/darkmode.js | 10 | ||||
-rw-r--r-- | src/webview/find.js | 23 | ||||
-rw-r--r-- | src/webview/lib/RecipeWebview.js | 50 | ||||
-rw-r--r-- | src/webview/notifications.js | 88 | ||||
-rw-r--r-- | src/webview/recipe.js | 294 | ||||
-rw-r--r-- | src/webview/screenshare.js | 91 | ||||
-rw-r--r-- | src/webview/spellchecker.js | 6 |
9 files changed, 376 insertions, 244 deletions
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/contextMenuBuilder.js b/src/webview/contextMenuBuilder.js index 63eed2ebe..602ce06f5 100644 --- a/src/webview/contextMenuBuilder.js +++ b/src/webview/contextMenuBuilder.js | |||
@@ -10,7 +10,7 @@ import { | |||
10 | clipboard, ipcRenderer, nativeImage, shell, | 10 | clipboard, ipcRenderer, nativeImage, shell, |
11 | } from 'electron'; | 11 | } from 'electron'; |
12 | import { Menu, MenuItem } from '@electron/remote'; | 12 | import { Menu, MenuItem } from '@electron/remote'; |
13 | import { isMac } from '../environment'; | 13 | import { shortcutKey, isMac } from '../environment'; |
14 | 14 | ||
15 | import { SEARCH_ENGINE_NAMES, SEARCH_ENGINE_URLS } from '../config'; | 15 | import { SEARCH_ENGINE_NAMES, SEARCH_ENGINE_URLS } from '../config'; |
16 | 16 | ||
@@ -28,6 +28,7 @@ const contextMenuStringTable = { | |||
28 | cut: () => 'Cut', | 28 | cut: () => 'Cut', |
29 | copy: () => 'Copy', | 29 | copy: () => 'Copy', |
30 | paste: () => 'Paste', | 30 | paste: () => 'Paste', |
31 | pasteAndMatchStyle: () => 'Paste and match style', | ||
31 | searchWith: ({ searchEngine }) => `Search with ${searchEngine}`, | 32 | searchWith: ({ searchEngine }) => `Search with ${searchEngine}`, |
32 | openLinkUrl: () => 'Open Link', | 33 | openLinkUrl: () => 'Open Link', |
33 | openLinkInFerdiUrl: () => 'Open Link in Ferdi', | 34 | openLinkInFerdiUrl: () => 'Open Link in Ferdi', |
@@ -61,7 +62,7 @@ module.exports = class ContextMenuBuilder { | |||
61 | * @param {function} processMenu If passed, this method will be passed the menu to change | 62 | * @param {function} processMenu If passed, this method will be passed the menu to change |
62 | * it prior to display. Signature: (menu, info) => menu | 63 | * it prior to display. Signature: (menu, info) => menu |
63 | */ | 64 | */ |
64 | constructor(webContents, debugMode = false, processMenu = m => m) { | 65 | constructor(webContents, debugMode = false, processMenu = (m) => m) { |
65 | this.debugMode = debugMode; | 66 | this.debugMode = debugMode; |
66 | this.processMenu = processMenu; | 67 | this.processMenu = processMenu; |
67 | this.menu = null; | 68 | this.menu = null; |
@@ -261,7 +262,7 @@ module.exports = class ContextMenuBuilder { | |||
261 | if (menuInfo.misspelledWord) { | 262 | if (menuInfo.misspelledWord) { |
262 | menu.append( | 263 | menu.append( |
263 | new MenuItem({ | 264 | new MenuItem({ |
264 | label: 'Add to dictionary', | 265 | label: this.stringTable.addToDictionary(), |
265 | click: () => webContents.session.addWordToSpellCheckerDictionary(menuInfo.misspelledWord), | 266 | click: () => webContents.session.addWordToSpellCheckerDictionary(menuInfo.misspelledWord), |
266 | }), | 267 | }), |
267 | ); | 268 | ); |
@@ -320,7 +321,7 @@ module.exports = class ContextMenuBuilder { | |||
320 | label: this.stringTable.copyImage(), | 321 | label: this.stringTable.copyImage(), |
321 | click: () => { | 322 | click: () => { |
322 | const result = this.convertImageToBase64(menuInfo.srcURL, | 323 | const result = this.convertImageToBase64(menuInfo.srcURL, |
323 | dataURL => clipboard.writeImage(nativeImage.createFromDataURL(dataURL))); | 324 | (dataURL) => clipboard.writeImage(nativeImage.createFromDataURL(dataURL))); |
324 | 325 | ||
325 | this._sendNotificationOnClipboardEvent(menuInfo.clipboardNotifications, () => `Image copied from URL: ${menuInfo.srcURL}`); | 326 | this._sendNotificationOnClipboardEvent(menuInfo.clipboardNotifications, () => `Image copied from URL: ${menuInfo.srcURL}`); |
326 | return result; | 327 | return result; |
@@ -375,7 +376,7 @@ module.exports = class ContextMenuBuilder { | |||
375 | const webContents = this.getWebContents(); | 376 | const webContents = this.getWebContents(); |
376 | menu.append(new MenuItem({ | 377 | menu.append(new MenuItem({ |
377 | label: this.stringTable.cut(), | 378 | label: this.stringTable.cut(), |
378 | accelerator: 'CommandOrControl+X', | 379 | accelerator: `${shortcutKey()}+X`, |
379 | enabled: menuInfo.editFlags.canCut, | 380 | enabled: menuInfo.editFlags.canCut, |
380 | click: () => webContents.cut(), | 381 | click: () => webContents.cut(), |
381 | })); | 382 | })); |
@@ -390,7 +391,7 @@ module.exports = class ContextMenuBuilder { | |||
390 | const webContents = this.getWebContents(); | 391 | const webContents = this.getWebContents(); |
391 | menu.append(new MenuItem({ | 392 | menu.append(new MenuItem({ |
392 | label: this.stringTable.copy(), | 393 | label: this.stringTable.copy(), |
393 | accelerator: 'CommandOrControl+C', | 394 | accelerator: `${shortcutKey()}+C`, |
394 | enabled: menuInfo.editFlags.canCopy, | 395 | enabled: menuInfo.editFlags.canCopy, |
395 | click: () => webContents.copy(), | 396 | click: () => webContents.copy(), |
396 | })); | 397 | })); |
@@ -405,7 +406,7 @@ module.exports = class ContextMenuBuilder { | |||
405 | const webContents = this.getWebContents(); | 406 | const webContents = this.getWebContents(); |
406 | menu.append(new MenuItem({ | 407 | menu.append(new MenuItem({ |
407 | label: this.stringTable.paste(), | 408 | label: this.stringTable.paste(), |
408 | accelerator: 'CommandOrControl+V', | 409 | accelerator: `${shortcutKey()}+V`, |
409 | enabled: menuInfo.editFlags.canPaste, | 410 | enabled: menuInfo.editFlags.canPaste, |
410 | click: () => webContents.paste(), | 411 | click: () => webContents.paste(), |
411 | })); | 412 | })); |
@@ -422,8 +423,8 @@ module.exports = class ContextMenuBuilder { | |||
422 | const webContents = this.getWebContents(); | 423 | const webContents = this.getWebContents(); |
423 | menu.append( | 424 | menu.append( |
424 | new MenuItem({ | 425 | new MenuItem({ |
425 | label: 'Paste as plain text', | 426 | label: this.stringTable.pasteAndMatchStyle(), |
426 | accelerator: 'CommandOrControl+Shift+V', | 427 | accelerator: `${shortcutKey()}+Shift+V`, |
427 | click: () => webContents.pasteAndMatchStyle(), | 428 | click: () => webContents.pasteAndMatchStyle(), |
428 | }), | 429 | }), |
429 | ); | 430 | ); |
@@ -489,7 +490,7 @@ module.exports = class ContextMenuBuilder { | |||
489 | const webContents = this.getWebContents(); | 490 | const webContents = this.getWebContents(); |
490 | menu.append(new MenuItem({ | 491 | menu.append(new MenuItem({ |
491 | label: this.stringTable.goBack(), | 492 | label: this.stringTable.goBack(), |
492 | accelerator: 'CommandOrControl+left', | 493 | accelerator: `${shortcutKey()}+left`, |
493 | enabled: webContents.canGoBack(), | 494 | enabled: webContents.canGoBack(), |
494 | click: () => webContents.goBack(), | 495 | click: () => webContents.goBack(), |
495 | })); | 496 | })); |
@@ -504,7 +505,7 @@ module.exports = class ContextMenuBuilder { | |||
504 | const webContents = this.getWebContents(); | 505 | const webContents = this.getWebContents(); |
505 | menu.append(new MenuItem({ | 506 | menu.append(new MenuItem({ |
506 | label: this.stringTable.goForward(), | 507 | label: this.stringTable.goForward(), |
507 | accelerator: 'CommandOrControl+right', | 508 | accelerator: `${shortcutKey()}+right`, |
508 | enabled: webContents.canGoForward(), | 509 | enabled: webContents.canGoForward(), |
509 | click: () => webContents.goForward(), | 510 | click: () => webContents.goForward(), |
510 | })); | 511 | })); |
@@ -535,7 +536,7 @@ module.exports = class ContextMenuBuilder { | |||
535 | const baseURL = new URL(menuInfo.pageURL); | 536 | const baseURL = new URL(menuInfo.pageURL); |
536 | menu.append(new MenuItem({ | 537 | menu.append(new MenuItem({ |
537 | label: this.stringTable.goToHomePage(), | 538 | label: this.stringTable.goToHomePage(), |
538 | accelerator: 'CommandOrControl+Home', | 539 | accelerator: `${shortcutKey()}+Home`, |
539 | enabled: true, | 540 | enabled: true, |
540 | click: () => { | 541 | click: () => { |
541 | // webContents.loadURL(baseURL.origin); | 542 | // webContents.loadURL(baseURL.origin); |
diff --git a/src/webview/darkmode.js b/src/webview/darkmode.js index ab629435c..7435d6404 100644 --- a/src/webview/darkmode.js +++ b/src/webview/darkmode.js | |||
@@ -1,7 +1,7 @@ | |||
1 | /* eslint no-bitwise: ["error", { "int32Hint": true }] */ | 1 | /* eslint no-bitwise: ["error", { "int32Hint": true }] */ |
2 | 2 | ||
3 | import path from 'path'; | 3 | import { join } from 'path'; |
4 | import fs from 'fs-extra'; | 4 | import { pathExistsSync, readFileSync } from 'fs-extra'; |
5 | 5 | ||
6 | const debug = require('debug')('Ferdi:DarkMode'); | 6 | const debug = require('debug')('Ferdi:DarkMode'); |
7 | 7 | ||
@@ -10,9 +10,9 @@ const chars = [...'abcdefghijklmnopqrstuvwxyz']; | |||
10 | const ID = [...Array(20)].map(() => chars[Math.random() * chars.length | 0]).join``; | 10 | const ID = [...Array(20)].map(() => chars[Math.random() * chars.length | 0]).join``; |
11 | 11 | ||
12 | export function injectDarkModeStyle(recipePath) { | 12 | export function injectDarkModeStyle(recipePath) { |
13 | const darkModeStyle = path.join(recipePath, 'darkmode.css'); | 13 | const darkModeStyle = join(recipePath, 'darkmode.css'); |
14 | if (fs.pathExistsSync(darkModeStyle)) { | 14 | if (pathExistsSync(darkModeStyle)) { |
15 | const data = fs.readFileSync(darkModeStyle); | 15 | const data = readFileSync(darkModeStyle); |
16 | const styles = document.createElement('style'); | 16 | const styles = document.createElement('style'); |
17 | styles.id = ID; | 17 | styles.id = ID; |
18 | styles.innerHTML = data.toString(); | 18 | styles.innerHTML = data.toString(); |
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..96caa125e 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, readFileSync } 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 | /** |
@@ -74,9 +55,8 @@ class RecipeWebview { | |||
74 | injectCSS(...files) { | 55 | injectCSS(...files) { |
75 | files.forEach(async (file) => { | 56 | files.forEach(async (file) => { |
76 | if (pathExistsSync(file)) { | 57 | if (pathExistsSync(file)) { |
77 | const data = await readFile(file, 'utf8'); | ||
78 | const styles = document.createElement('style'); | 58 | const styles = document.createElement('style'); |
79 | styles.innerHTML = data; | 59 | styles.innerHTML = readFileSync(file, 'utf8'); |
80 | 60 | ||
81 | document.querySelector('head').appendChild(styles); | 61 | document.querySelector('head').appendChild(styles); |
82 | 62 | ||
@@ -85,6 +65,22 @@ class RecipeWebview { | |||
85 | }); | 65 | }); |
86 | } | 66 | } |
87 | 67 | ||
68 | injectJSUnsafe(...files) { | ||
69 | Promise.all(files.map(async (file) => { | ||
70 | if (await exists(file)) { | ||
71 | return readFileSync(file, 'utf8'); | ||
72 | } | ||
73 | debug('Script not found', file); | ||
74 | return null; | ||
75 | })).then(async (scripts) => { | ||
76 | const scriptsFound = scripts.filter((script) => script !== null); | ||
77 | if (scriptsFound.length > 0) { | ||
78 | debug('Inject scripts to main world', scriptsFound); | ||
79 | ipcRenderer.sendToHost('inject-js-unsafe', ...scriptsFound); | ||
80 | } | ||
81 | }); | ||
82 | } | ||
83 | |||
88 | /** | 84 | /** |
89 | * Set a custom handler for turning on and off dark mode | 85 | * Set a custom handler for turning on and off dark mode |
90 | * | 86 | * |
@@ -96,7 +92,7 @@ class RecipeWebview { | |||
96 | 92 | ||
97 | onNotify(fn) { | 93 | onNotify(fn) { |
98 | if (typeof fn === 'function') { | 94 | if (typeof fn === 'function') { |
99 | window.Notification.prototype.onNotify = fn; | 95 | this.notificationsHandler.onNotify = fn; |
100 | } | 96 | } |
101 | } | 97 | } |
102 | 98 | ||
diff --git a/src/webview/notifications.js b/src/webview/notifications.js index 021f05cc3..205a3220c 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..a45c34002 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'; | 3 | import { join } from 'path'; |
4 | 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 { pathExistsSync, readFileSync } 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,99 @@ 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 { |
30 | import { screenShareCss } from './screenshare'; | 27 | injectDarkModeStyle, |
28 | isDarkModeStyleInjected, | ||
29 | removeDarkModeStyle, | ||
30 | } from './darkmode'; | ||
31 | import FindInPage from './find'; | ||
32 | import { | ||
33 | NotificationsHandler, | ||
34 | notificationsClassDefinition, | ||
35 | } from './notifications'; | ||
36 | import { | ||
37 | getDisplayMediaSelector, | ||
38 | screenShareCss, | ||
39 | screenShareJs, | ||
40 | } from './screenshare'; | ||
41 | import { | ||
42 | switchDict, | ||
43 | getSpellcheckerLocaleByFuzzyIdentifier, | ||
44 | } from './spellchecker'; | ||
31 | 45 | ||
32 | import { DEFAULT_APP_SETTINGS, isDevMode } from '../environment'; | 46 | import { DEFAULT_APP_SETTINGS } from '../environment'; |
33 | 47 | ||
34 | const debug = require('debug')('Ferdi:Plugin'); | 48 | const debug = require('debug')('Ferdi:Plugin'); |
35 | 49 | ||
50 | const badgeHandler = new BadgeHandler(); | ||
51 | |||
52 | const notificationsHandler = new NotificationsHandler(); | ||
53 | |||
54 | // Patching window.open | ||
55 | const originalWindowOpen = window.open; | ||
56 | |||
57 | window.open = (url, frameName, features) => { | ||
58 | debug('window.open', url, frameName, features); | ||
59 | if (!url) { | ||
60 | // The service hasn't yet supplied a URL (as used in Skype). | ||
61 | // Return a new dummy window object and wait for the service to change the properties | ||
62 | const newWindow = { | ||
63 | location: { | ||
64 | href: '', | ||
65 | }, | ||
66 | }; | ||
67 | |||
68 | const checkInterval = setInterval(() => { | ||
69 | // Has the service changed the URL yet? | ||
70 | if (newWindow.location.href !== '') { | ||
71 | if (features) { | ||
72 | originalWindowOpen(newWindow.location.href, frameName, features); | ||
73 | } else { | ||
74 | // Open the new URL | ||
75 | ipcRenderer.sendToHost('new-window', newWindow.location.href); | ||
76 | } | ||
77 | clearInterval(checkInterval); | ||
78 | } | ||
79 | }, 0); | ||
80 | |||
81 | setTimeout(() => { | ||
82 | // Stop checking for location changes after 1 second | ||
83 | clearInterval(checkInterval); | ||
84 | }, 1000); | ||
85 | |||
86 | return newWindow; | ||
87 | } | ||
88 | |||
89 | // We need to differentiate if the link should be opened in a popup or in the systems default browser | ||
90 | if (!frameName && !features && typeof features !== 'string') { | ||
91 | return ipcRenderer.sendToHost('new-window', url); | ||
92 | } | ||
93 | |||
94 | if (url) { | ||
95 | return originalWindowOpen(url, frameName, features); | ||
96 | } | ||
97 | }; | ||
98 | |||
99 | // We can't override APIs here, so we first expose functions via window.ferdi, | ||
100 | // then overwrite the corresponding field of the window object by injected JS. | ||
101 | contextBridge.exposeInMainWorld('ferdi', { | ||
102 | open: window.open, | ||
103 | setBadge: (direct, indirect) => | ||
104 | badgeHandler.setBadge(direct || 0, indirect || 0), | ||
105 | displayNotification: (title, options) => | ||
106 | notificationsHandler.displayNotification(title, options), | ||
107 | getDisplayMediaSelector, | ||
108 | }); | ||
109 | |||
110 | ipcRenderer.sendToHost( | ||
111 | 'inject-js-unsafe', | ||
112 | 'window.open = window.ferdi.open;', | ||
113 | notificationsClassDefinition, | ||
114 | screenShareJs, | ||
115 | ); | ||
116 | |||
36 | class RecipeController { | 117 | class RecipeController { |
37 | @observable settings = { | 118 | @observable settings = { |
38 | overrideSpellcheckerLanguage: false, | 119 | overrideSpellcheckerLanguage: false, |
@@ -66,7 +147,9 @@ class RecipeController { | |||
66 | } | 147 | } |
67 | 148 | ||
68 | @computed get spellcheckerLanguage() { | 149 | @computed get spellcheckerLanguage() { |
69 | const selected = this.settings.service.spellcheckerLanguage || this.settings.app.spellcheckerLanguage; | 150 | const selected = |
151 | this.settings.service.spellcheckerLanguage || | ||
152 | this.settings.app.spellcheckerLanguage; | ||
70 | return selected; | 153 | return selected; |
71 | } | 154 | } |
72 | 155 | ||
@@ -75,7 +158,7 @@ class RecipeController { | |||
75 | findInPage = null; | 158 | findInPage = null; |
76 | 159 | ||
77 | async initialize() { | 160 | async initialize() { |
78 | Object.keys(this.ipcEvents).forEach((channel) => { | 161 | Object.keys(this.ipcEvents).forEach(channel => { |
79 | ipcRenderer.on(channel, (...args) => { | 162 | ipcRenderer.on(channel, (...args) => { |
80 | debug('Received IPC event for channel', channel, 'with', ...args); | 163 | debug('Received IPC event for channel', channel, 'with', ...args); |
81 | this[this.ipcEvents[channel]](...args); | 164 | this[this.ipcEvents[channel]](...args); |
@@ -97,7 +180,7 @@ class RecipeController { | |||
97 | autorun(() => this.update()); | 180 | autorun(() => this.update()); |
98 | 181 | ||
99 | document.addEventListener('DOMContentLoaded', () => { | 182 | document.addEventListener('DOMContentLoaded', () => { |
100 | this.findInPage = new FindInPage(getCurrentWebContents(), { | 183 | this.findInPage = new FindInPage({ |
101 | inputFocusColor: '#CE9FFC', | 184 | inputFocusColor: '#CE9FFC', |
102 | textColor: '#212121', | 185 | textColor: '#212121', |
103 | }); | 186 | }); |
@@ -106,14 +189,14 @@ class RecipeController { | |||
106 | 189 | ||
107 | loadRecipeModule(event, config, recipe) { | 190 | loadRecipeModule(event, config, recipe) { |
108 | debug('loadRecipeModule'); | 191 | debug('loadRecipeModule'); |
109 | const modulePath = path.join(recipe.path, 'webview.js'); | 192 | const modulePath = join(recipe.path, 'webview.js'); |
110 | debug('module path', modulePath); | 193 | debug('module path', modulePath); |
111 | // Delete module from cache | 194 | // Delete module from cache |
112 | delete require.cache[require.resolve(modulePath)]; | 195 | delete require.cache[require.resolve(modulePath)]; |
113 | try { | 196 | try { |
114 | this.recipe = new RecipeWebview(); | 197 | this.recipe = new RecipeWebview(badgeHandler, notificationsHandler); |
115 | // eslint-disable-next-line | 198 | // eslint-disable-next-line |
116 | require(modulePath)(this.recipe, {...config, recipe,}); | 199 | require(modulePath)(this.recipe, { ...config, recipe }); |
117 | debug('Initialize Recipe', config, recipe); | 200 | debug('Initialize Recipe', config, recipe); |
118 | 201 | ||
119 | this.settings.service = Object.assign(config, { recipe }); | 202 | this.settings.service = Object.assign(config, { recipe }); |
@@ -131,15 +214,15 @@ class RecipeController { | |||
131 | const styles = document.createElement('style'); | 214 | const styles = document.createElement('style'); |
132 | styles.innerHTML = screenShareCss; | 215 | styles.innerHTML = screenShareCss; |
133 | 216 | ||
134 | const userCss = path.join(recipe.path, 'user.css'); | 217 | const userCss = join(recipe.path, 'user.css'); |
135 | if (await fs.exists(userCss)) { | 218 | if (pathExistsSync(userCss)) { |
136 | const data = await fs.readFile(userCss); | 219 | const data = readFileSync(userCss); |
137 | styles.innerHTML += data.toString(); | 220 | styles.innerHTML += data.toString(); |
138 | } | 221 | } |
139 | document.querySelector('head').appendChild(styles); | 222 | document.querySelector('head').appendChild(styles); |
140 | 223 | ||
141 | const userJs = path.join(recipe.path, 'user.js'); | 224 | const userJs = join(recipe.path, 'user.js'); |
142 | if (await fs.exists(userJs)) { | 225 | if (pathExistsSync(userJs)) { |
143 | const loadUserJs = () => { | 226 | const loadUserJs = () => { |
144 | // eslint-disable-next-line | 227 | // eslint-disable-next-line |
145 | const userJsModule = require(userJs); | 228 | const userJsModule = require(userJs); |
@@ -167,8 +250,14 @@ class RecipeController { | |||
167 | update() { | 250 | update() { |
168 | debug('enableSpellchecking', this.settings.app.enableSpellchecking); | 251 | debug('enableSpellchecking', this.settings.app.enableSpellchecking); |
169 | debug('isDarkModeEnabled', this.settings.service.isDarkModeEnabled); | 252 | debug('isDarkModeEnabled', this.settings.service.isDarkModeEnabled); |
170 | debug('System spellcheckerLanguage', this.settings.app.spellcheckerLanguage); | 253 | debug( |
171 | debug('Service spellcheckerLanguage', this.settings.service.spellcheckerLanguage); | 254 | 'System spellcheckerLanguage', |
255 | this.settings.app.spellcheckerLanguage, | ||
256 | ); | ||
257 | debug( | ||
258 | 'Service spellcheckerLanguage', | ||
259 | this.settings.service.spellcheckerLanguage, | ||
260 | ); | ||
172 | debug('darkReaderSettigs', this.settings.service.darkReaderSettings); | 261 | debug('darkReaderSettigs', this.settings.service.darkReaderSettings); |
173 | debug('searchEngine', this.settings.app.searchEngine); | 262 | debug('searchEngine', this.settings.app.searchEngine); |
174 | 263 | ||
@@ -181,7 +270,10 @@ class RecipeController { | |||
181 | let { spellcheckerLanguage } = this; | 270 | let { spellcheckerLanguage } = this; |
182 | if (spellcheckerLanguage.includes('automatic')) { | 271 | if (spellcheckerLanguage.includes('automatic')) { |
183 | this.automaticLanguageDetection(); | 272 | this.automaticLanguageDetection(); |
184 | debug('Found `automatic` locale, falling back to user locale until detected', this.settings.app.locale); | 273 | debug( |
274 | 'Found `automatic` locale, falling back to user locale until detected', | ||
275 | this.settings.app.locale, | ||
276 | ); | ||
185 | spellcheckerLanguage = this.settings.app.locale; | 277 | spellcheckerLanguage = this.settings.app.locale; |
186 | } | 278 | } |
187 | switchDict(spellcheckerLanguage); | 279 | switchDict(spellcheckerLanguage); |
@@ -193,7 +285,7 @@ class RecipeController { | |||
193 | this.hasUpdatedBeforeRecipeLoaded = true; | 285 | this.hasUpdatedBeforeRecipeLoaded = true; |
194 | } | 286 | } |
195 | 287 | ||
196 | console.log( | 288 | debug( |
197 | 'Darkmode enabled?', | 289 | 'Darkmode enabled?', |
198 | this.settings.service.isDarkModeEnabled, | 290 | this.settings.service.isDarkModeEnabled, |
199 | 'Dark theme active?', | 291 | 'Dark theme active?', |
@@ -204,22 +296,29 @@ class RecipeController { | |||
204 | removeDarkModeStyle, | 296 | removeDarkModeStyle, |
205 | disableDarkMode, | 297 | disableDarkMode, |
206 | enableDarkMode, | 298 | enableDarkMode, |
207 | injectDarkModeStyle: () => injectDarkModeStyle(this.settings.service.recipe.path), | 299 | injectDarkModeStyle: () => |
300 | injectDarkModeStyle(this.settings.service.recipe.path), | ||
208 | isDarkModeStyleInjected, | 301 | isDarkModeStyleInjected, |
209 | }; | 302 | }; |
210 | 303 | ||
211 | if (this.settings.service.isDarkModeEnabled && this.settings.app.isDarkThemeActive !== false) { | 304 | if ( |
305 | this.settings.service.isDarkModeEnabled && | ||
306 | this.settings.app.isDarkThemeActive !== false | ||
307 | ) { | ||
212 | debug('Enable dark mode'); | 308 | debug('Enable dark mode'); |
213 | 309 | ||
214 | // Check if recipe has a darkmode.css | 310 | // Check if recipe has a darkmode.css |
215 | const darkModeStyle = path.join(this.settings.service.recipe.path, 'darkmode.css'); | 311 | const darkModeStyle = join( |
216 | const darkModeExists = fs.pathExistsSync(darkModeStyle); | 312 | this.settings.service.recipe.path, |
313 | 'darkmode.css', | ||
314 | ); | ||
315 | const darkModeExists = pathExistsSync(darkModeStyle); | ||
217 | 316 | ||
218 | console.log('darkmode.css exists? ', darkModeExists ? 'Yes' : 'No'); | 317 | debug('darkmode.css exists? ', darkModeExists ? 'Yes' : 'No'); |
219 | 318 | ||
220 | // Check if recipe has a custom dark mode handler | 319 | // Check if recipe has a custom dark mode handler |
221 | if (this.recipe && this.recipe.darkModeHandler) { | 320 | if (this.recipe && this.recipe.darkModeHandler) { |
222 | console.log('Using custom dark mode handler'); | 321 | debug('Using custom dark mode handler'); |
223 | 322 | ||
224 | // Remove other dark mode styles if they were already loaded | 323 | // Remove other dark mode styles if they were already loaded |
225 | if (this.hasUpdatedBeforeRecipeLoaded) { | 324 | if (this.hasUpdatedBeforeRecipeLoaded) { |
@@ -230,25 +329,32 @@ class RecipeController { | |||
230 | 329 | ||
231 | this.recipe.darkModeHandler(true, handlerConfig); | 330 | this.recipe.darkModeHandler(true, handlerConfig); |
232 | } else if (darkModeExists) { | 331 | } else if (darkModeExists) { |
233 | console.log('Injecting darkmode.css'); | 332 | debug('Injecting darkmode.css'); |
234 | injectDarkModeStyle(this.settings.service.recipe.path); | 333 | injectDarkModeStyle(this.settings.service.recipe.path); |
235 | 334 | ||
236 | // Make sure universal dark mode is disabled | 335 | // Make sure universal dark mode is disabled |
237 | disableDarkMode(); | 336 | disableDarkMode(); |
238 | this.universalDarkModeInjected = false; | 337 | this.universalDarkModeInjected = false; |
239 | } else if (this.settings.app.universalDarkMode && !ignoreList.includes(window.location.host)) { | 338 | } else if ( |
240 | console.log('Injecting Dark Reader'); | 339 | this.settings.app.universalDarkMode && |
340 | !ignoreList.includes(window.location.host) | ||
341 | ) { | ||
342 | debug('Injecting Dark Reader'); | ||
241 | 343 | ||
242 | // Use Dark Reader instead | 344 | // Use Dark Reader instead |
243 | const { brightness, contrast, sepia } = this.settings.service.darkReaderSettings; | 345 | const { brightness, contrast, sepia } = |
244 | enableDarkMode({ brightness, contrast, sepia }, { | 346 | this.settings.service.darkReaderSettings; |
245 | css: customDarkModeCss[window.location.host] || '', | 347 | enableDarkMode( |
246 | }); | 348 | { brightness, contrast, sepia }, |
349 | { | ||
350 | css: customDarkModeCss[window.location.host] || '', | ||
351 | }, | ||
352 | ); | ||
247 | this.universalDarkModeInjected = true; | 353 | this.universalDarkModeInjected = true; |
248 | } | 354 | } |
249 | } else { | 355 | } else { |
250 | debug('Remove dark mode'); | 356 | debug('Remove dark mode'); |
251 | console.log('DarkMode disabled - removing remaining styles'); | 357 | debug('DarkMode disabled - removing remaining styles'); |
252 | 358 | ||
253 | if (this.recipe && this.recipe.darkModeHandler) { | 359 | if (this.recipe && this.recipe.darkModeHandler) { |
254 | // Remove other dark mode styles if they were already loaded | 360 | // Remove other dark mode styles if they were already loaded |
@@ -260,10 +366,10 @@ class RecipeController { | |||
260 | 366 | ||
261 | this.recipe.darkModeHandler(false, handlerConfig); | 367 | this.recipe.darkModeHandler(false, handlerConfig); |
262 | } else if (isDarkModeStyleInjected()) { | 368 | } else if (isDarkModeStyleInjected()) { |
263 | console.log('Removing injected darkmode.css'); | 369 | debug('Removing injected darkmode.css'); |
264 | removeDarkModeStyle(); | 370 | removeDarkModeStyle(); |
265 | } else { | 371 | } else { |
266 | console.log('Removing Dark Reader'); | 372 | debug('Removing Dark Reader'); |
267 | 373 | ||
268 | disableDarkMode(); | 374 | disableDarkMode(); |
269 | this.universalDarkModeInjected = false; | 375 | this.universalDarkModeInjected = false; |
@@ -273,9 +379,9 @@ class RecipeController { | |||
273 | // Remove dark reader if (universal) dark mode was just disabled | 379 | // Remove dark reader if (universal) dark mode was just disabled |
274 | if (this.universalDarkModeInjected) { | 380 | if (this.universalDarkModeInjected) { |
275 | if ( | 381 | if ( |
276 | !this.settings.app.darkMode | 382 | !this.settings.app.darkMode || |
277 | || !this.settings.service.isDarkModeEnabled | 383 | !this.settings.service.isDarkModeEnabled || |
278 | || !this.settings.app.universalDarkMode | 384 | !this.settings.app.universalDarkMode |
279 | ) { | 385 | ) { |
280 | disableDarkMode(); | 386 | disableDarkMode(); |
281 | this.universalDarkModeInjected = false; | 387 | this.universalDarkModeInjected = false; |
@@ -297,82 +403,42 @@ class RecipeController { | |||
297 | } | 403 | } |
298 | 404 | ||
299 | async automaticLanguageDetection() { | 405 | async automaticLanguageDetection() { |
300 | window.addEventListener('keyup', debounce(async (e) => { | 406 | window.addEventListener( |
301 | const element = e.target; | 407 | 'keyup', |
302 | 408 | debounce(async e => { | |
303 | if (!element) return; | 409 | const element = e.target; |
304 | 410 | ||
305 | let value = ''; | 411 | if (!element) return; |
306 | if (element.isContentEditable) { | 412 | |
307 | value = element.textContent; | 413 | let value = ''; |
308 | } else if (element.value) { | 414 | if (element.isContentEditable) { |
309 | value = element.value; | 415 | value = element.textContent; |
310 | } | 416 | } else if (element.value) { |
417 | value = element.value; | ||
418 | } | ||
311 | 419 | ||
312 | // Force a minimum length to get better detection results | 420 | // Force a minimum length to get better detection results |
313 | if (value.length < 25) return; | 421 | if (value.length < 25) return; |
314 | 422 | ||
315 | debug('Detecting language for', value); | 423 | debug('Detecting language for', value); |
316 | const locale = await ipcRenderer.invoke('detect-language', { sample: value }); | 424 | const locale = await ipcRenderer.invoke('detect-language', { |
425 | sample: value, | ||
426 | }); | ||
317 | 427 | ||
318 | const spellcheckerLocale = getSpellcheckerLocaleByFuzzyIdentifier(locale); | 428 | const spellcheckerLocale = |
319 | debug('Language detected reliably, setting spellchecker language to', spellcheckerLocale); | 429 | getSpellcheckerLocaleByFuzzyIdentifier(locale); |
320 | if (spellcheckerLocale) { | 430 | debug( |
321 | switchDict(spellcheckerLocale); | 431 | 'Language detected reliably, setting spellchecker language to', |
322 | } | 432 | spellcheckerLocale, |
323 | }, 225)); | 433 | ); |
434 | if (spellcheckerLocale) { | ||
435 | switchDict(spellcheckerLocale); | ||
436 | } | ||
437 | }, 225), | ||
438 | ); | ||
324 | } | 439 | } |
325 | } | 440 | } |
326 | 441 | ||
327 | /* eslint-disable no-new */ | 442 | /* eslint-disable no-new */ |
328 | new RecipeController(); | 443 | new RecipeController(); |
329 | /* eslint-enable no-new */ | 444 | /* 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..e7e43c04e 100644 --- a/src/webview/screenshare.js +++ b/src/webview/screenshare.js | |||
@@ -2,6 +2,33 @@ 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({ | ||
7 | types: ['screen', 'window'], | ||
8 | }); | ||
9 | return `<div class="desktop-capturer-selection__scroller"> | ||
10 | <ul class="desktop-capturer-selection__list"> | ||
11 | ${sources | ||
12 | .map( | ||
13 | ({ id, name, thumbnail }) => ` | ||
14 | <li class="desktop-capturer-selection__item"> | ||
15 | <button class="desktop-capturer-selection__btn" data-id="${id}" title="${name}"> | ||
16 | <img class="desktop-capturer-selection__thumbnail" src="${thumbnail.toDataURL()}" /> | ||
17 | <span class="desktop-capturer-selection__name">${name}</span> | ||
18 | </button> | ||
19 | </li> | ||
20 | `, | ||
21 | ) | ||
22 | .join('')} | ||
23 | <li class="desktop-capturer-selection__item"> | ||
24 | <button class="desktop-capturer-selection__btn" data-id="${CANCEL_ID}" title="Cancel"> | ||
25 | <span class="desktop-capturer-selection__name desktop-capturer-selection__name--cancel">Cancel</span> | ||
26 | </button> | ||
27 | </li> | ||
28 | </ul> | ||
29 | </div>`; | ||
30 | } | ||
31 | |||
5 | export const screenShareCss = ` | 32 | export const screenShareCss = ` |
6 | .desktop-capturer-selection { | 33 | .desktop-capturer-selection { |
7 | position: fixed; | 34 | position: fixed; |
@@ -46,7 +73,9 @@ export const screenShareCss = ` | |||
46 | padding: 4px; | 73 | padding: 4px; |
47 | background: #252626; | 74 | background: #252626; |
48 | text-align: left; | 75 | text-align: left; |
49 | transition: background-color .15s, box-shadow .15s, color .15s; | 76 | @media (prefers-reduced-motion: no-preference) { |
77 | transition: background-color .15s, box-shadow .15s, color .15s; | ||
78 | } | ||
50 | color: #dedede; | 79 | color: #dedede; |
51 | } | 80 | } |
52 | .desktop-capturer-selection__btn:hover, | 81 | .desktop-capturer-selection__btn:hover, |
@@ -72,38 +101,12 @@ export const screenShareCss = ` | |||
72 | } | 101 | } |
73 | `; | 102 | `; |
74 | 103 | ||
75 | // Patch getDisplayMedia for screen sharing | 104 | export const screenShareJs = ` |
76 | window.navigator.mediaDevices.getDisplayMedia = () => async (resolve, reject) => { | 105 | window.navigator.mediaDevices.getDisplayMedia = () => new Promise(async (resolve, reject) => { |
77 | try { | 106 | try { |
78 | const sources = await desktopCapturer.getSources({ | ||
79 | types: ['screen', 'window'], | ||
80 | }); | ||
81 | |||
82 | const selectionElem = document.createElement('div'); | 107 | const selectionElem = document.createElement('div'); |
83 | selectionElem.classList = 'desktop-capturer-selection'; | 108 | selectionElem.classList = ['desktop-capturer-selection']; |
84 | selectionElem.innerHTML = ` | 109 | 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); | 110 | document.body.appendChild(selectionElem); |
108 | 111 | ||
109 | document | 112 | document |
@@ -112,25 +115,18 @@ window.navigator.mediaDevices.getDisplayMedia = () => async (resolve, reject) => | |||
112 | button.addEventListener('click', async () => { | 115 | button.addEventListener('click', async () => { |
113 | try { | 116 | try { |
114 | const id = button.getAttribute('data-id'); | 117 | const id = button.getAttribute('data-id'); |
115 | if (id === CANCEL_ID) { | 118 | if (id === '${CANCEL_ID}') { |
116 | reject(new Error('Cancelled by user')); | 119 | reject(new Error('Cancelled by user')); |
117 | } else { | 120 | } else { |
118 | const mediaSource = sources.find((source) => source.id === id); | 121 | const stream = await window.navigator.mediaDevices.getUserMedia({ |
119 | if (!mediaSource) { | 122 | audio: false, |
120 | throw new Error(`Source with id ${id} does not exist`); | 123 | video: { |
121 | } | 124 | mandatory: { |
122 | 125 | chromeMediaSource: 'desktop', | |
123 | const stream = await window.navigator.mediaDevices.getUserMedia( | 126 | chromeMediaSourceId: id, |
124 | { | ||
125 | audio: false, | ||
126 | video: { | ||
127 | mandatory: { | ||
128 | chromeMediaSource: 'desktop', | ||
129 | chromeMediaSourceId: mediaSource.id, | ||
130 | }, | ||
131 | }, | 127 | }, |
132 | }, | 128 | }, |
133 | ); | 129 | }); |
134 | resolve(stream); | 130 | resolve(stream); |
135 | } | 131 | } |
136 | } catch (err) { | 132 | } catch (err) { |
@@ -143,4 +139,5 @@ window.navigator.mediaDevices.getDisplayMedia = () => async (resolve, reject) => | |||
143 | } catch (err) { | 139 | } catch (err) { |
144 | reject(err); | 140 | reject(err); |
145 | } | 141 | } |
146 | }; | 142 | }); |
143 | `; | ||
diff --git a/src/webview/spellchecker.js b/src/webview/spellchecker.js index 58a04b728..0f4715207 100644 --- a/src/webview/spellchecker.js +++ b/src/webview/spellchecker.js | |||
@@ -1,6 +1,6 @@ | |||
1 | import { getCurrentWebContents } from '@electron/remote'; | 1 | import { getCurrentWebContents } from '@electron/remote'; |
2 | import { SPELLCHECKER_LOCALES } from '../i18n/languages'; | 2 | import { SPELLCHECKER_LOCALES } from '../i18n/languages'; |
3 | import { isMac } from '../environment'; | 3 | import { DEFAULT_APP_SETTINGS, isMac } from '../environment'; |
4 | 4 | ||
5 | const debug = require('debug')('Ferdi:spellchecker'); | 5 | const debug = require('debug')('Ferdi:spellchecker'); |
6 | 6 | ||
@@ -9,7 +9,7 @@ const [defaultLocale] = webContents.session.getSpellCheckerLanguages(); | |||
9 | debug('Spellchecker default locale is', defaultLocale); | 9 | debug('Spellchecker default locale is', defaultLocale); |
10 | 10 | ||
11 | export function getSpellcheckerLocaleByFuzzyIdentifier(identifier) { | 11 | export function getSpellcheckerLocaleByFuzzyIdentifier(identifier) { |
12 | const locales = Object.keys(SPELLCHECKER_LOCALES).filter(key => key.toLocaleLowerCase() === identifier.toLowerCase() || key.split('-')[0] === identifier.toLowerCase()); | 12 | const locales = Object.keys(SPELLCHECKER_LOCALES).filter((key) => key.toLocaleLowerCase() === identifier.toLowerCase() || key.split('-')[0] === identifier.toLowerCase()); |
13 | 13 | ||
14 | if (locales.length >= 1) { | 14 | if (locales.length >= 1) { |
15 | return locales[0]; | 15 | return locales[0]; |
@@ -33,7 +33,7 @@ export function switchDict(locale) { | |||
33 | locales.push(foundLocale); | 33 | locales.push(foundLocale); |
34 | } | 34 | } |
35 | 35 | ||
36 | locales.push(defaultLocale, 'de'); | 36 | locales.push(defaultLocale, DEFAULT_APP_SETTINGS.fallbackLocale); |
37 | 37 | ||
38 | webContents.session.setSpellCheckerLanguages(locales); | 38 | webContents.session.setSpellCheckerLanguages(locales); |
39 | } | 39 | } |