diff options
author | Balaji Vijayakumar <kuttibalaji.v6@gmail.com> | 2022-11-02 19:18:55 +0530 |
---|---|---|
committer | Vijay Aravamudhan <vraravam@users.noreply.github.com> | 2022-11-02 20:07:36 +0530 |
commit | c04ae52680a5293a46d506517b12cf4fc3d6909c (patch) | |
tree | 2d1e4849f0f80fb7abdbc9c5a2997320dc4f5275 /src/webview/recipe.js | |
parent | 6.2.1-nightly.36 [skip ci] (diff) | |
download | ferdium-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.js | 473 |
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 */ | ||
3 | import { contextBridge, ipcRenderer } from 'electron'; | ||
4 | import { join } from 'path'; | ||
5 | import { autorun, computed, makeObservable, observable } from 'mobx'; | ||
6 | import { pathExistsSync, readFileSync } from 'fs-extra'; | ||
7 | import { 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 | ||
13 | window.chrome.runtime.sendMessage = () => {}; | ||
14 | import { | ||
15 | enable as enableDarkMode, | ||
16 | disable as disableDarkMode, | ||
17 | } from 'darkreader'; | ||
18 | |||
19 | import { existsSync } from 'fs'; | ||
20 | import ignoreList from './darkmode/ignore'; | ||
21 | import customDarkModeCss from './darkmode/custom'; | ||
22 | |||
23 | import RecipeWebview from './lib/RecipeWebview'; | ||
24 | import Userscript from './lib/Userscript'; | ||
25 | |||
26 | import BadgeHandler from './badge'; | ||
27 | import DialogTitleHandler from './dialogTitle'; | ||
28 | import SessionHandler from './sessionHandler'; | ||
29 | import contextMenu from './contextMenu'; | ||
30 | import { | ||
31 | darkModeStyleExists, | ||
32 | injectDarkModeStyle, | ||
33 | isDarkModeStyleInjected, | ||
34 | removeDarkModeStyle, | ||
35 | } from './darkmode'; | ||
36 | import FindInPage from './find'; | ||
37 | import { | ||
38 | NotificationsHandler, | ||
39 | notificationsClassDefinition, | ||
40 | } from './notifications'; | ||
41 | import { | ||
42 | getDisplayMediaSelector, | ||
43 | screenShareCss, | ||
44 | screenShareJs, | ||
45 | } from './screenshare'; | ||
46 | import { | ||
47 | switchDict, | ||
48 | getSpellcheckerLocaleByFuzzyIdentifier, | ||
49 | } from './spellchecker'; | ||
50 | |||
51 | import { DEFAULT_APP_SETTINGS } from '../config'; | ||
52 | import { ifUndefinedString } from '../jsUtils'; | ||
53 | |||
54 | const debug = require('../preload-safe-debug')('Ferdium:Plugin'); | ||
55 | |||
56 | const badgeHandler = new BadgeHandler(); | ||
57 | |||
58 | const dialogTitleHandler = new DialogTitleHandler(); | ||
59 | |||
60 | const sessionHandler = new SessionHandler(); | ||
61 | |||
62 | const notificationsHandler = new NotificationsHandler(); | ||
63 | |||
64 | // Patching window.open | ||
65 | const originalWindowOpen = window.open; | ||
66 | |||
67 | window.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. | ||
111 | contextBridge.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 | |||
121 | ipcRenderer.sendToHost( | ||
122 | 'inject-js-unsafe', | ||
123 | 'window.open = window.ferdium.open;', | ||
124 | notificationsClassDefinition, | ||
125 | screenShareJs, | ||
126 | ); | ||
127 | |||
128 | class 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 */ | ||
472 | new RecipeController(); | ||
473 | /* eslint-enable no-new */ | ||