aboutsummaryrefslogtreecommitdiffstats
path: root/src/webview/recipe.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/webview/recipe.js')
-rw-r--r--src/webview/recipe.js294
1 files changed, 180 insertions, 114 deletions
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 */
2import { ipcRenderer } from 'electron'; 2import { contextBridge, ipcRenderer } from 'electron';
3import { getCurrentWebContents } from '@electron/remote'; 3import { join } from 'path';
4import path from 'path';
5import { autorun, computed, observable } from 'mobx'; 4import { autorun, computed, observable } from 'mobx';
6import fs from 'fs-extra'; 5import { pathExistsSync, readFileSync } from 'fs-extra';
7import { debounce } from 'lodash'; 6import { debounce } from 'lodash';
8import { 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';
23import RecipeWebview from './lib/RecipeWebview'; 21import RecipeWebview from './lib/RecipeWebview';
24import Userscript from './lib/Userscript'; 22import Userscript from './lib/Userscript';
25 23
26import { switchDict, getSpellcheckerLocaleByFuzzyIdentifier } from './spellchecker'; 24import { BadgeHandler } from './badge';
27import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode';
28import contextMenu from './contextMenu'; 25import contextMenu from './contextMenu';
29import './notifications'; 26import {
30import { screenShareCss } from './screenshare'; 27 injectDarkModeStyle,
28 isDarkModeStyleInjected,
29 removeDarkModeStyle,
30} from './darkmode';
31import FindInPage from './find';
32import {
33 NotificationsHandler,
34 notificationsClassDefinition,
35} from './notifications';
36import {
37 getDisplayMediaSelector,
38 screenShareCss,
39 screenShareJs,
40} from './screenshare';
41import {
42 switchDict,
43 getSpellcheckerLocaleByFuzzyIdentifier,
44} from './spellchecker';
31 45
32import { DEFAULT_APP_SETTINGS, isDevMode } from '../environment'; 46import { DEFAULT_APP_SETTINGS } from '../environment';
33 47
34const debug = require('debug')('Ferdi:Plugin'); 48const debug = require('debug')('Ferdi:Plugin');
35 49
50const badgeHandler = new BadgeHandler();
51
52const notificationsHandler = new NotificationsHandler();
53
54// Patching window.open
55const originalWindowOpen = window.open;
56
57window.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.
101contextBridge.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
110ipcRenderer.sendToHost(
111 'inject-js-unsafe',
112 'window.open = window.ferdi.open;',
113 notificationsClassDefinition,
114 screenShareJs,
115);
116
36class RecipeController { 117class 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 */
328new RecipeController(); 443new RecipeController();
329/* eslint-enable no-new */ 444/* eslint-enable no-new */
330
331// Patching window.open
332const originalWindowOpen = window.open;
333
334window.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
376if (isDevMode) {
377 window.log = console.log;
378}