aboutsummaryrefslogtreecommitdiffstats
path: root/src/webview/recipe.js
diff options
context:
space:
mode:
authorLibravatar Balaji Vijayakumar <kuttibalaji.v6@gmail.com>2022-11-02 19:18:55 +0530
committerLibravatar Vijay Aravamudhan <vraravam@users.noreply.github.com>2022-11-02 20:07:36 +0530
commitc04ae52680a5293a46d506517b12cf4fc3d6909c (patch)
tree2d1e4849f0f80fb7abdbc9c5a2997320dc4f5275 /src/webview/recipe.js
parent6.2.1-nightly.36 [skip ci] (diff)
downloadferdium-app-c04ae52680a5293a46d506517b12cf4fc3d6909c.tar.gz
ferdium-app-c04ae52680a5293a46d506517b12cf4fc3d6909c.tar.zst
ferdium-app-c04ae52680a5293a46d506517b12cf4fc3d6909c.zip
refactor: migrate recipe.js to typescript
Diffstat (limited to 'src/webview/recipe.js')
-rw-r--r--src/webview/recipe.js473
1 files changed, 0 insertions, 473 deletions
diff --git a/src/webview/recipe.js b/src/webview/recipe.js
deleted file mode 100644
index acf4f9f31..000000000
--- a/src/webview/recipe.js
+++ /dev/null
@@ -1,473 +0,0 @@
1/* eslint-disable global-require */
2/* eslint-disable import/first */
3import { contextBridge, ipcRenderer } from 'electron';
4import { join } from 'path';
5import { autorun, computed, makeObservable, observable } from 'mobx';
6import { pathExistsSync, readFileSync } from 'fs-extra';
7import { debounce } from 'lodash';
8
9// For some services darkreader tries to use the chrome extension message API
10// This will cause the service to fail loading
11// As the message API is not actually needed, we'll add this shim sendMessage
12// function in order for darkreader to continue working
13window.chrome.runtime.sendMessage = () => {};
14import {
15 enable as enableDarkMode,
16 disable as disableDarkMode,
17} from 'darkreader';
18
19import { existsSync } from 'fs';
20import ignoreList from './darkmode/ignore';
21import customDarkModeCss from './darkmode/custom';
22
23import RecipeWebview from './lib/RecipeWebview';
24import Userscript from './lib/Userscript';
25
26import BadgeHandler from './badge';
27import DialogTitleHandler from './dialogTitle';
28import SessionHandler from './sessionHandler';
29import contextMenu from './contextMenu';
30import {
31 darkModeStyleExists,
32 injectDarkModeStyle,
33 isDarkModeStyleInjected,
34 removeDarkModeStyle,
35} from './darkmode';
36import FindInPage from './find';
37import {
38 NotificationsHandler,
39 notificationsClassDefinition,
40} from './notifications';
41import {
42 getDisplayMediaSelector,
43 screenShareCss,
44 screenShareJs,
45} from './screenshare';
46import {
47 switchDict,
48 getSpellcheckerLocaleByFuzzyIdentifier,
49} from './spellchecker';
50
51import { DEFAULT_APP_SETTINGS } from '../config';
52import { ifUndefinedString } from '../jsUtils';
53
54const debug = require('../preload-safe-debug')('Ferdium:Plugin');
55
56const badgeHandler = new BadgeHandler();
57
58const dialogTitleHandler = new DialogTitleHandler();
59
60const sessionHandler = new SessionHandler();
61
62const notificationsHandler = new NotificationsHandler();
63
64// Patching window.open
65const originalWindowOpen = window.open;
66
67window.open = (url, frameName, features) => {
68 debug('window.open', url, frameName, features);
69 if (!url) {
70 // The service hasn't yet supplied a URL (as used in Skype).
71 // Return a new dummy window object and wait for the service to change the properties
72 const newWindow = {
73 location: {
74 href: '',
75 },
76 };
77
78 const checkInterval = setInterval(() => {
79 // Has the service changed the URL yet?
80 if (newWindow.location.href !== '') {
81 if (features) {
82 originalWindowOpen(newWindow.location.href, frameName, features);
83 } else {
84 // Open the new URL
85 ipcRenderer.sendToHost('new-window', newWindow.location.href);
86 }
87 clearInterval(checkInterval);
88 }
89 }, 0);
90
91 setTimeout(() => {
92 // Stop checking for location changes after 1 second
93 clearInterval(checkInterval);
94 }, 1000);
95
96 return newWindow;
97 }
98
99 // We need to differentiate if the link should be opened in a popup or in the systems default browser
100 if (!frameName && !features && typeof features !== 'string') {
101 return ipcRenderer.sendToHost('new-window', url);
102 }
103
104 if (url) {
105 return originalWindowOpen(url, frameName, features);
106 }
107};
108
109// We can't override APIs here, so we first expose functions via 'window.ferdium',
110// then overwrite the corresponding field of the window object by injected JS.
111contextBridge.exposeInMainWorld('ferdium', {
112 open: window.open,
113 setBadge: (direct, indirect) => badgeHandler.setBadge(direct, indirect),
114 safeParseInt: text => badgeHandler.safeParseInt(text),
115 setDialogTitle: title => dialogTitleHandler.setDialogTitle(title),
116 displayNotification: (title, options) =>
117 notificationsHandler.displayNotification(title, options),
118 getDisplayMediaSelector,
119});
120
121ipcRenderer.sendToHost(
122 'inject-js-unsafe',
123 'window.open = window.ferdium.open;',
124 notificationsClassDefinition,
125 screenShareJs,
126);
127
128class RecipeController {
129 @observable settings = {
130 overrideSpellcheckerLanguage: false,
131 app: DEFAULT_APP_SETTINGS,
132 service: {
133 isDarkModeEnabled: false,
134 spellcheckerLanguage: '',
135 },
136 };
137
138 spellcheckProvider = null;
139
140 ipcEvents = {
141 'initialize-recipe': 'loadRecipeModule',
142 'settings-update': 'updateAppSettings',
143 'service-settings-update': 'updateServiceSettings',
144 'get-service-id': 'serviceIdEcho',
145 'find-in-page': 'openFindInPage',
146 };
147
148 universalDarkModeInjected = false;
149
150 recipe = null;
151
152 userscript = null;
153
154 hasUpdatedBeforeRecipeLoaded = false;
155
156 constructor() {
157 makeObservable(this);
158
159 this.initialize();
160 }
161
162 @computed get spellcheckerLanguage() {
163 return ifUndefinedString(
164 this.settings.service.spellcheckerLanguage,
165 this.settings.app.spellcheckerLanguage,
166 );
167 }
168
169 cldIdentifier = null;
170
171 findInPage = null;
172
173 async initialize() {
174 for (const channel of Object.keys(this.ipcEvents)) {
175 ipcRenderer.on(channel, (...args) => {
176 debug('Received IPC event for channel', channel, 'with', ...args);
177 this[this.ipcEvents[channel]](...args);
178 });
179 }
180
181 debug('Send "hello" to host');
182 setTimeout(() => ipcRenderer.sendToHost('hello'), 100);
183
184 this.spellcheckingProvider = null;
185 contextMenu(
186 () => this.settings.app.enableSpellchecking,
187 () => this.settings.app.spellcheckerLanguage,
188 () => this.spellcheckerLanguage,
189 () => this.settings.app.searchEngine,
190 () => this.settings.app.clipboardNotifications,
191 () => this.settings.app.enableTranslator,
192 () => this.settings.app.translatorEngine,
193 () => this.settings.app.translatorLanguage,
194 );
195
196 autorun(() => this.update());
197
198 document.addEventListener('DOMContentLoaded', () => {
199 this.findInPage = new FindInPage({
200 inputFocusColor: '#CE9FFC',
201 textColor: '#212121',
202 });
203 });
204
205 // Add ability to go forward or back with mouse buttons (inside the recipe)
206 window.addEventListener('mouseup', e => {
207 if (e.button === 3) {
208 e.preventDefault();
209 e.stopPropagation();
210 window.history.back();
211 } else if (e.button === 4) {
212 e.preventDefault();
213 e.stopPropagation();
214 window.history.forward();
215 }
216 });
217 }
218
219 loadRecipeModule(event, config, recipe) {
220 debug('loadRecipeModule');
221 const modulePath = join(recipe.path, 'webview.js');
222 debug('module path', modulePath);
223 // Delete module from cache
224 delete require.cache[require.resolve(modulePath)];
225 try {
226 this.recipe = new RecipeWebview(
227 badgeHandler,
228 dialogTitleHandler,
229 notificationsHandler,
230 sessionHandler,
231 );
232 if (existsSync(modulePath)) {
233 // eslint-disable-next-line import/no-dynamic-require
234 require(modulePath)(this.recipe, { ...config, recipe });
235 debug('Initialize Recipe', config, recipe);
236 }
237
238 this.settings.service = Object.assign(config, { recipe });
239
240 // Make sure to update the WebView, otherwise the custom darkmode handler may not be used
241 this.update();
242 } catch (error) {
243 console.error('Recipe initialization failed', error);
244 }
245
246 this.loadUserFiles(recipe, config);
247 }
248
249 async loadUserFiles(recipe, config) {
250 const styles = document.createElement('style');
251 styles.innerHTML = screenShareCss;
252
253 const userCss = join(recipe.path, 'user.css');
254 if (pathExistsSync(userCss)) {
255 const data = readFileSync(userCss);
256 styles.innerHTML += data.toString();
257 }
258 document.querySelector('head').append(styles);
259
260 const userJs = join(recipe.path, 'user.js');
261 if (pathExistsSync(userJs)) {
262 const loadUserJs = () => {
263 // eslint-disable-next-line import/no-dynamic-require
264 const userJsModule = require(userJs);
265
266 if (typeof userJsModule === 'function') {
267 this.userscript = new Userscript(this.recipe, this, config);
268 userJsModule(config, this.userscript);
269 }
270 };
271
272 if (document.readyState !== 'loading') {
273 loadUserJs();
274 } else {
275 document.addEventListener('DOMContentLoaded', () => {
276 loadUserJs();
277 });
278 }
279 }
280 }
281
282 openFindInPage() {
283 this.findInPage.openFindWindow();
284 }
285
286 update() {
287 debug('enableSpellchecking', this.settings.app.enableSpellchecking);
288 debug('isDarkModeEnabled', this.settings.service.isDarkModeEnabled);
289 debug(
290 'System spellcheckerLanguage',
291 this.settings.app.spellcheckerLanguage,
292 );
293 debug(
294 'Service spellcheckerLanguage',
295 this.settings.service.spellcheckerLanguage,
296 );
297 debug('darkReaderSettigs', this.settings.service.darkReaderSettings);
298 debug('searchEngine', this.settings.app.searchEngine);
299 debug('enableTranslator', this.settings.app.enableTranslator);
300 debug('translatorEngine', this.settings.app.translatorEngine);
301 debug('translatorLanguage', this.settings.app.translatorLanguage);
302
303 if (this.userscript && this.userscript.internal_setSettings) {
304 this.userscript.internal_setSettings(this.settings);
305 }
306
307 if (this.settings.app.enableSpellchecking) {
308 let { spellcheckerLanguage } = this;
309 debug(`Setting spellchecker language to ${spellcheckerLanguage}`);
310 if (spellcheckerLanguage.includes('automatic')) {
311 this.automaticLanguageDetection();
312 debug(
313 'Found `automatic` locale, falling back to user locale until detected',
314 this.settings.app.locale,
315 );
316 spellcheckerLanguage = this.settings.app.locale;
317 }
318 switchDict(spellcheckerLanguage, this.settings.service.id);
319 } else {
320 debug('Disable spellchecker');
321 }
322
323 if (!this.recipe) {
324 this.hasUpdatedBeforeRecipeLoaded = true;
325 }
326
327 debug(
328 'Darkmode enabled?',
329 this.settings.service.isDarkModeEnabled,
330 'Dark theme active?',
331 this.settings.app.isDarkThemeActive,
332 );
333
334 const handlerConfig = {
335 removeDarkModeStyle,
336 disableDarkMode,
337 enableDarkMode,
338 injectDarkModeStyle: () =>
339 injectDarkModeStyle(this.settings.service.recipe.path),
340 isDarkModeStyleInjected,
341 };
342
343 if (this.settings.service.isDarkModeEnabled) {
344 debug('Enable dark mode');
345
346 // Check if recipe has a custom dark mode handler
347 if (this.recipe && this.recipe.darkModeHandler) {
348 debug('Using custom dark mode handler');
349
350 // Remove other dark mode styles if they were already loaded
351 if (this.hasUpdatedBeforeRecipeLoaded) {
352 this.hasUpdatedBeforeRecipeLoaded = false;
353 removeDarkModeStyle();
354 disableDarkMode();
355 }
356
357 this.recipe.darkModeHandler(true, handlerConfig);
358 } else if (darkModeStyleExists(this.settings.service.recipe.path)) {
359 debug('Injecting darkmode from recipe');
360 injectDarkModeStyle(this.settings.service.recipe.path);
361
362 // Make sure universal dark mode is disabled
363 disableDarkMode();
364 this.universalDarkModeInjected = false;
365 } else if (
366 this.settings.app.universalDarkMode &&
367 !ignoreList.includes(window.location.host)
368 ) {
369 debug('Injecting Dark Reader');
370
371 // Use Dark Reader instead
372 const { brightness, contrast, sepia } =
373 this.settings.service.darkReaderSettings;
374 enableDarkMode(
375 { brightness, contrast, sepia },
376 {
377 css: customDarkModeCss[window.location.host] || '',
378 },
379 );
380 this.universalDarkModeInjected = true;
381 }
382 } else {
383 debug('Remove dark mode');
384 debug('DarkMode disabled - removing remaining styles');
385
386 if (this.recipe && this.recipe.darkModeHandler) {
387 // Remove other dark mode styles if they were already loaded
388 if (this.hasUpdatedBeforeRecipeLoaded) {
389 this.hasUpdatedBeforeRecipeLoaded = false;
390 removeDarkModeStyle();
391 disableDarkMode();
392 }
393
394 this.recipe.darkModeHandler(false, handlerConfig);
395 } else if (isDarkModeStyleInjected()) {
396 debug('Removing injected darkmode from recipe');
397 removeDarkModeStyle();
398 } else {
399 debug('Removing Dark Reader');
400
401 disableDarkMode();
402 this.universalDarkModeInjected = false;
403 }
404 }
405
406 // Remove dark reader if (universal) dark mode was just disabled
407 if (
408 this.universalDarkModeInjected &&
409 (!this.settings.app.darkMode ||
410 !this.settings.service.isDarkModeEnabled ||
411 !this.settings.app.universalDarkMode)
412 ) {
413 disableDarkMode();
414 this.universalDarkModeInjected = false;
415 }
416 }
417
418 updateAppSettings(event, data) {
419 this.settings.app = Object.assign(this.settings.app, data);
420 }
421
422 updateServiceSettings(event, data) {
423 this.settings.service = Object.assign(this.settings.service, data);
424 }
425
426 serviceIdEcho(event) {
427 debug('Received a service echo ping');
428 event.sender.send('service-id', this.settings.service.id);
429 }
430
431 async automaticLanguageDetection() {
432 window.addEventListener(
433 'keyup',
434 debounce(async e => {
435 const element = e.target;
436
437 if (!element) return;
438
439 let value = '';
440 if (element.isContentEditable) {
441 value = element.textContent;
442 } else if (element.value) {
443 value = element.value;
444 }
445
446 // Force a minimum length to get better detection results
447 if (value.length < 25) return;
448
449 debug('Detecting language for', value);
450 const locale = await ipcRenderer.invoke('detect-language', {
451 sample: value,
452 });
453 if (!locale) {
454 return;
455 }
456
457 const spellcheckerLocale =
458 getSpellcheckerLocaleByFuzzyIdentifier(locale);
459 debug(
460 'Language detected reliably, setting spellchecker language to',
461 spellcheckerLocale,
462 );
463 if (spellcheckerLocale) {
464 switchDict(spellcheckerLocale, this.settings.service.id);
465 }
466 }, 225),
467 );
468 }
469}
470
471/* eslint-disable no-new */
472new RecipeController();
473/* eslint-enable no-new */