From c04ae52680a5293a46d506517b12cf4fc3d6909c Mon Sep 17 00:00:00 2001 From: Balaji Vijayakumar Date: Wed, 2 Nov 2022 19:18:55 +0530 Subject: refactor: migrate recipe.js to typescript --- src/@types/stores.types.ts | 4 +- src/config.ts | 1 + src/webview/find.ts | 4 + src/webview/lib/RecipeWebview.ts | 2 +- src/webview/recipe.js | 473 -------------------------------------- src/webview/recipe.ts | 475 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 483 insertions(+), 476 deletions(-) delete mode 100644 src/webview/recipe.js create mode 100644 src/webview/recipe.ts diff --git a/src/@types/stores.types.ts b/src/@types/stores.types.ts index bf2dc8bd2..bbb37cff9 100644 --- a/src/@types/stores.types.ts +++ b/src/@types/stores.types.ts @@ -78,7 +78,7 @@ interface TypedStore { resetStatus: () => void; } -interface AppStore extends TypedStore { +export interface AppStore extends TypedStore { accentColor: string; adaptableDarkMode: boolean; progressbarAccentColor: string; @@ -102,7 +102,7 @@ interface AppStore extends TypedStore { isOnline: boolean; isSystemDarkModeEnabled: () => void; isSystemMuteOverridden: () => void; - locale: () => void; + locale: string; lockedPassword: string; reloadAfterResume: boolean; reloadAfterResumeTime: number; diff --git a/src/config.ts b/src/config.ts index 4bd1ca155..e4baf27c5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -456,4 +456,5 @@ export const DEFAULT_SERVICE_SETTINGS = { hasHostedOption: false, allowFavoritesDelineationInUnreadCount: false, disablewebsecurity: false, + spellcheckerLanguage: false, }; diff --git a/src/webview/find.ts b/src/webview/find.ts index 0665d9670..ead818b07 100644 --- a/src/webview/find.ts +++ b/src/webview/find.ts @@ -24,4 +24,8 @@ export default class FindInPage extends ElectronFindInPage { constructor(options = {}) { super(webContentsShim, options); } + + openFindWindow() { + super.openFindWindow(); + } } diff --git a/src/webview/lib/RecipeWebview.ts b/src/webview/lib/RecipeWebview.ts index a896f1b6e..20be3f866 100644 --- a/src/webview/lib/RecipeWebview.ts +++ b/src/webview/lib/RecipeWebview.ts @@ -39,7 +39,7 @@ class RecipeWebview { loopFunc = () => null; - darkModeHandler = false; + darkModeHandler: ((darkMode: boolean, config: any) => void) | null = null; // TODO Remove this once we implement a proper wrapper. get ipcRenderer() { 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 @@ -/* eslint-disable global-require */ -/* eslint-disable import/first */ -import { contextBridge, ipcRenderer } from 'electron'; -import { join } from 'path'; -import { autorun, computed, makeObservable, observable } from 'mobx'; -import { pathExistsSync, readFileSync } from 'fs-extra'; -import { debounce } from 'lodash'; - -// For some services darkreader tries to use the chrome extension message API -// This will cause the service to fail loading -// As the message API is not actually needed, we'll add this shim sendMessage -// function in order for darkreader to continue working -window.chrome.runtime.sendMessage = () => {}; -import { - enable as enableDarkMode, - disable as disableDarkMode, -} from 'darkreader'; - -import { existsSync } from 'fs'; -import ignoreList from './darkmode/ignore'; -import customDarkModeCss from './darkmode/custom'; - -import RecipeWebview from './lib/RecipeWebview'; -import Userscript from './lib/Userscript'; - -import BadgeHandler from './badge'; -import DialogTitleHandler from './dialogTitle'; -import SessionHandler from './sessionHandler'; -import contextMenu from './contextMenu'; -import { - darkModeStyleExists, - injectDarkModeStyle, - isDarkModeStyleInjected, - removeDarkModeStyle, -} from './darkmode'; -import FindInPage from './find'; -import { - NotificationsHandler, - notificationsClassDefinition, -} from './notifications'; -import { - getDisplayMediaSelector, - screenShareCss, - screenShareJs, -} from './screenshare'; -import { - switchDict, - getSpellcheckerLocaleByFuzzyIdentifier, -} from './spellchecker'; - -import { DEFAULT_APP_SETTINGS } from '../config'; -import { ifUndefinedString } from '../jsUtils'; - -const debug = require('../preload-safe-debug')('Ferdium:Plugin'); - -const badgeHandler = new BadgeHandler(); - -const dialogTitleHandler = new DialogTitleHandler(); - -const sessionHandler = new SessionHandler(); - -const notificationsHandler = new NotificationsHandler(); - -// Patching window.open -const originalWindowOpen = window.open; - -window.open = (url, frameName, features) => { - debug('window.open', url, frameName, features); - if (!url) { - // The service hasn't yet supplied a URL (as used in Skype). - // Return a new dummy window object and wait for the service to change the properties - const newWindow = { - location: { - href: '', - }, - }; - - const checkInterval = setInterval(() => { - // Has the service changed the URL yet? - if (newWindow.location.href !== '') { - if (features) { - originalWindowOpen(newWindow.location.href, frameName, features); - } else { - // Open the new URL - ipcRenderer.sendToHost('new-window', newWindow.location.href); - } - clearInterval(checkInterval); - } - }, 0); - - setTimeout(() => { - // Stop checking for location changes after 1 second - clearInterval(checkInterval); - }, 1000); - - return newWindow; - } - - // We need to differentiate if the link should be opened in a popup or in the systems default browser - if (!frameName && !features && typeof features !== 'string') { - return ipcRenderer.sendToHost('new-window', url); - } - - if (url) { - return originalWindowOpen(url, frameName, features); - } -}; - -// We can't override APIs here, so we first expose functions via 'window.ferdium', -// then overwrite the corresponding field of the window object by injected JS. -contextBridge.exposeInMainWorld('ferdium', { - open: window.open, - setBadge: (direct, indirect) => badgeHandler.setBadge(direct, indirect), - safeParseInt: text => badgeHandler.safeParseInt(text), - setDialogTitle: title => dialogTitleHandler.setDialogTitle(title), - displayNotification: (title, options) => - notificationsHandler.displayNotification(title, options), - getDisplayMediaSelector, -}); - -ipcRenderer.sendToHost( - 'inject-js-unsafe', - 'window.open = window.ferdium.open;', - notificationsClassDefinition, - screenShareJs, -); - -class RecipeController { - @observable settings = { - overrideSpellcheckerLanguage: false, - app: DEFAULT_APP_SETTINGS, - service: { - isDarkModeEnabled: false, - spellcheckerLanguage: '', - }, - }; - - spellcheckProvider = null; - - ipcEvents = { - 'initialize-recipe': 'loadRecipeModule', - 'settings-update': 'updateAppSettings', - 'service-settings-update': 'updateServiceSettings', - 'get-service-id': 'serviceIdEcho', - 'find-in-page': 'openFindInPage', - }; - - universalDarkModeInjected = false; - - recipe = null; - - userscript = null; - - hasUpdatedBeforeRecipeLoaded = false; - - constructor() { - makeObservable(this); - - this.initialize(); - } - - @computed get spellcheckerLanguage() { - return ifUndefinedString( - this.settings.service.spellcheckerLanguage, - this.settings.app.spellcheckerLanguage, - ); - } - - cldIdentifier = null; - - findInPage = null; - - async initialize() { - for (const channel of Object.keys(this.ipcEvents)) { - ipcRenderer.on(channel, (...args) => { - debug('Received IPC event for channel', channel, 'with', ...args); - this[this.ipcEvents[channel]](...args); - }); - } - - debug('Send "hello" to host'); - setTimeout(() => ipcRenderer.sendToHost('hello'), 100); - - this.spellcheckingProvider = null; - contextMenu( - () => this.settings.app.enableSpellchecking, - () => this.settings.app.spellcheckerLanguage, - () => this.spellcheckerLanguage, - () => this.settings.app.searchEngine, - () => this.settings.app.clipboardNotifications, - () => this.settings.app.enableTranslator, - () => this.settings.app.translatorEngine, - () => this.settings.app.translatorLanguage, - ); - - autorun(() => this.update()); - - document.addEventListener('DOMContentLoaded', () => { - this.findInPage = new FindInPage({ - inputFocusColor: '#CE9FFC', - textColor: '#212121', - }); - }); - - // Add ability to go forward or back with mouse buttons (inside the recipe) - window.addEventListener('mouseup', e => { - if (e.button === 3) { - e.preventDefault(); - e.stopPropagation(); - window.history.back(); - } else if (e.button === 4) { - e.preventDefault(); - e.stopPropagation(); - window.history.forward(); - } - }); - } - - loadRecipeModule(event, config, recipe) { - debug('loadRecipeModule'); - const modulePath = join(recipe.path, 'webview.js'); - debug('module path', modulePath); - // Delete module from cache - delete require.cache[require.resolve(modulePath)]; - try { - this.recipe = new RecipeWebview( - badgeHandler, - dialogTitleHandler, - notificationsHandler, - sessionHandler, - ); - if (existsSync(modulePath)) { - // eslint-disable-next-line import/no-dynamic-require - require(modulePath)(this.recipe, { ...config, recipe }); - debug('Initialize Recipe', config, recipe); - } - - this.settings.service = Object.assign(config, { recipe }); - - // Make sure to update the WebView, otherwise the custom darkmode handler may not be used - this.update(); - } catch (error) { - console.error('Recipe initialization failed', error); - } - - this.loadUserFiles(recipe, config); - } - - async loadUserFiles(recipe, config) { - const styles = document.createElement('style'); - styles.innerHTML = screenShareCss; - - const userCss = join(recipe.path, 'user.css'); - if (pathExistsSync(userCss)) { - const data = readFileSync(userCss); - styles.innerHTML += data.toString(); - } - document.querySelector('head').append(styles); - - const userJs = join(recipe.path, 'user.js'); - if (pathExistsSync(userJs)) { - const loadUserJs = () => { - // eslint-disable-next-line import/no-dynamic-require - const userJsModule = require(userJs); - - if (typeof userJsModule === 'function') { - this.userscript = new Userscript(this.recipe, this, config); - userJsModule(config, this.userscript); - } - }; - - if (document.readyState !== 'loading') { - loadUserJs(); - } else { - document.addEventListener('DOMContentLoaded', () => { - loadUserJs(); - }); - } - } - } - - openFindInPage() { - this.findInPage.openFindWindow(); - } - - update() { - debug('enableSpellchecking', this.settings.app.enableSpellchecking); - debug('isDarkModeEnabled', this.settings.service.isDarkModeEnabled); - debug( - 'System spellcheckerLanguage', - this.settings.app.spellcheckerLanguage, - ); - debug( - 'Service spellcheckerLanguage', - this.settings.service.spellcheckerLanguage, - ); - debug('darkReaderSettigs', this.settings.service.darkReaderSettings); - debug('searchEngine', this.settings.app.searchEngine); - debug('enableTranslator', this.settings.app.enableTranslator); - debug('translatorEngine', this.settings.app.translatorEngine); - debug('translatorLanguage', this.settings.app.translatorLanguage); - - if (this.userscript && this.userscript.internal_setSettings) { - this.userscript.internal_setSettings(this.settings); - } - - if (this.settings.app.enableSpellchecking) { - let { spellcheckerLanguage } = this; - debug(`Setting spellchecker language to ${spellcheckerLanguage}`); - if (spellcheckerLanguage.includes('automatic')) { - this.automaticLanguageDetection(); - debug( - 'Found `automatic` locale, falling back to user locale until detected', - this.settings.app.locale, - ); - spellcheckerLanguage = this.settings.app.locale; - } - switchDict(spellcheckerLanguage, this.settings.service.id); - } else { - debug('Disable spellchecker'); - } - - if (!this.recipe) { - this.hasUpdatedBeforeRecipeLoaded = true; - } - - debug( - 'Darkmode enabled?', - this.settings.service.isDarkModeEnabled, - 'Dark theme active?', - this.settings.app.isDarkThemeActive, - ); - - const handlerConfig = { - removeDarkModeStyle, - disableDarkMode, - enableDarkMode, - injectDarkModeStyle: () => - injectDarkModeStyle(this.settings.service.recipe.path), - isDarkModeStyleInjected, - }; - - if (this.settings.service.isDarkModeEnabled) { - debug('Enable dark mode'); - - // Check if recipe has a custom dark mode handler - if (this.recipe && this.recipe.darkModeHandler) { - debug('Using custom dark mode handler'); - - // Remove other dark mode styles if they were already loaded - if (this.hasUpdatedBeforeRecipeLoaded) { - this.hasUpdatedBeforeRecipeLoaded = false; - removeDarkModeStyle(); - disableDarkMode(); - } - - this.recipe.darkModeHandler(true, handlerConfig); - } else if (darkModeStyleExists(this.settings.service.recipe.path)) { - debug('Injecting darkmode from recipe'); - injectDarkModeStyle(this.settings.service.recipe.path); - - // Make sure universal dark mode is disabled - disableDarkMode(); - this.universalDarkModeInjected = false; - } else if ( - this.settings.app.universalDarkMode && - !ignoreList.includes(window.location.host) - ) { - debug('Injecting Dark Reader'); - - // Use Dark Reader instead - const { brightness, contrast, sepia } = - this.settings.service.darkReaderSettings; - enableDarkMode( - { brightness, contrast, sepia }, - { - css: customDarkModeCss[window.location.host] || '', - }, - ); - this.universalDarkModeInjected = true; - } - } else { - debug('Remove dark mode'); - debug('DarkMode disabled - removing remaining styles'); - - if (this.recipe && this.recipe.darkModeHandler) { - // Remove other dark mode styles if they were already loaded - if (this.hasUpdatedBeforeRecipeLoaded) { - this.hasUpdatedBeforeRecipeLoaded = false; - removeDarkModeStyle(); - disableDarkMode(); - } - - this.recipe.darkModeHandler(false, handlerConfig); - } else if (isDarkModeStyleInjected()) { - debug('Removing injected darkmode from recipe'); - removeDarkModeStyle(); - } else { - debug('Removing Dark Reader'); - - disableDarkMode(); - this.universalDarkModeInjected = false; - } - } - - // Remove dark reader if (universal) dark mode was just disabled - if ( - this.universalDarkModeInjected && - (!this.settings.app.darkMode || - !this.settings.service.isDarkModeEnabled || - !this.settings.app.universalDarkMode) - ) { - disableDarkMode(); - this.universalDarkModeInjected = false; - } - } - - updateAppSettings(event, data) { - this.settings.app = Object.assign(this.settings.app, data); - } - - updateServiceSettings(event, data) { - this.settings.service = Object.assign(this.settings.service, data); - } - - serviceIdEcho(event) { - debug('Received a service echo ping'); - event.sender.send('service-id', this.settings.service.id); - } - - async automaticLanguageDetection() { - window.addEventListener( - 'keyup', - debounce(async e => { - const element = e.target; - - if (!element) return; - - let value = ''; - if (element.isContentEditable) { - value = element.textContent; - } else if (element.value) { - value = element.value; - } - - // Force a minimum length to get better detection results - if (value.length < 25) return; - - debug('Detecting language for', value); - const locale = await ipcRenderer.invoke('detect-language', { - sample: value, - }); - if (!locale) { - return; - } - - const spellcheckerLocale = - getSpellcheckerLocaleByFuzzyIdentifier(locale); - debug( - 'Language detected reliably, setting spellchecker language to', - spellcheckerLocale, - ); - if (spellcheckerLocale) { - switchDict(spellcheckerLocale, this.settings.service.id); - } - }, 225), - ); - } -} - -/* eslint-disable no-new */ -new RecipeController(); -/* eslint-enable no-new */ diff --git a/src/webview/recipe.ts b/src/webview/recipe.ts new file mode 100644 index 000000000..887d9c367 --- /dev/null +++ b/src/webview/recipe.ts @@ -0,0 +1,475 @@ +/* eslint-disable global-require */ +/* eslint-disable import/no-dynamic-require */ +/* eslint-disable import/first */ +import { contextBridge, ipcRenderer } from 'electron'; +import { join } from 'path'; +import { autorun, computed, makeObservable, observable } from 'mobx'; +import { pathExistsSync, readFileSync } from 'fs-extra'; +import { debounce } from 'lodash'; +import { + disable as disableDarkMode, + enable as enableDarkMode, +} from 'darkreader'; + +import { existsSync } from 'fs'; +import ignoreList from './darkmode/ignore'; +import customDarkModeCss from './darkmode/custom'; + +import RecipeWebview from './lib/RecipeWebview'; +import Userscript from './lib/Userscript'; + +import BadgeHandler from './badge'; +import DialogTitleHandler from './dialogTitle'; +import SessionHandler from './sessionHandler'; +import contextMenu from './contextMenu'; +import { + darkModeStyleExists, + injectDarkModeStyle, + isDarkModeStyleInjected, + removeDarkModeStyle, +} from './darkmode'; +import FindInPage from './find'; +import { + notificationsClassDefinition, + NotificationsHandler, +} from './notifications'; +import { + getDisplayMediaSelector, + screenShareCss, + screenShareJs, +} from './screenshare'; +import { + getSpellcheckerLocaleByFuzzyIdentifier, + switchDict, +} from './spellchecker'; + +import { ifUndefinedString } from '../jsUtils'; +import { AppStore } from '../@types/stores.types'; +import Service from '../models/Service'; + +// For some services darkreader tries to use the chrome extension message API +// This will cause the service to fail loading +// As the message API is not actually needed, we'll add this shim sendMessage +// function in order for darkreader to continue working +// @ts-ignore +window.chrome.runtime.sendMessage = () => {}; + +const debug = require('../preload-safe-debug')('Ferdium:Plugin'); + +const badgeHandler = new BadgeHandler(); + +const dialogTitleHandler = new DialogTitleHandler(); + +const sessionHandler = new SessionHandler(); + +const notificationsHandler = new NotificationsHandler(); + +// Patching window.open +const originalWindowOpen = window.open; + +window.open = (url, frameName, features): WindowProxy | null => { + debug('window.open', url, frameName, features); + if (!url) { + // The service hasn't yet supplied a URL (as used in Skype). + // Return a new dummy window object and wait for the service to change the properties + const newWindow = { + location: { + href: '', + }, + }; + + const checkInterval = setInterval(() => { + // Has the service changed the URL yet? + if (newWindow.location.href !== '') { + if (features) { + originalWindowOpen(newWindow.location.href, frameName, features); + } else { + // Open the new URL + ipcRenderer.sendToHost('new-window', newWindow.location.href); + } + clearInterval(checkInterval); + } + }, 0); + + setTimeout(() => { + // Stop checking for location changes after 1 second + clearInterval(checkInterval); + }, 1000); + + return newWindow as Window; + } + + // We need to differentiate if the link should be opened in a popup or in the systems default browser + if (!frameName && !features && typeof features !== 'string') { + ipcRenderer.sendToHost('new-window', url); + return null; + } + + if (url) { + return originalWindowOpen(url, frameName, features); + } + return null; +}; + +// We can't override APIs here, so we first expose functions via 'window.ferdium', +// then overwrite the corresponding field of the window object by injected JS. +contextBridge.exposeInMainWorld('ferdium', { + open: window.open, + setBadge: (direct, indirect) => badgeHandler.setBadge(direct, indirect), + safeParseInt: text => badgeHandler.safeParseInt(text), + setDialogTitle: title => dialogTitleHandler.setDialogTitle(title), + displayNotification: (title, options) => + notificationsHandler.displayNotification(title, options), + getDisplayMediaSelector, +}); + +ipcRenderer.sendToHost( + 'inject-js-unsafe', + 'window.open = window.ferdium.open;', + notificationsClassDefinition, + screenShareJs, +); + +class RecipeController { + // @ts-ignore + @observable settings: { + overrideSpellcheckerLanguage: boolean; + app: AppStore; + service: Service; + } = { + overrideSpellcheckerLanguage: false, + }; + + ipcEvents = { + 'initialize-recipe': 'loadRecipeModule', + 'settings-update': 'updateAppSettings', + 'service-settings-update': 'updateServiceSettings', + 'get-service-id': 'serviceIdEcho', + 'find-in-page': 'openFindInPage', + }; + + universalDarkModeInjected = false; + + recipe: RecipeWebview | null = null; + + userscript: Userscript | null = null; + + hasUpdatedBeforeRecipeLoaded = false; + + constructor() { + makeObservable(this); + + this.initialize(); + } + + @computed get spellcheckerLanguage() { + return ifUndefinedString( + this.settings.service.spellcheckerLanguage, + this.settings.app.spellcheckerLanguage, + ); + } + + findInPage: FindInPage | null = null; + + async initialize() { + for (const channel of Object.keys(this.ipcEvents)) { + ipcRenderer.on(channel, (...args) => { + debug('Received IPC event for channel', channel, 'with', ...args); + this[this.ipcEvents[channel]](...args); + }); + } + + debug('Send "hello" to host'); + setTimeout(() => ipcRenderer.sendToHost('hello'), 100); + + contextMenu( + () => this.settings.app.enableSpellchecking, + () => this.settings.app.spellcheckerLanguage, + () => this.spellcheckerLanguage, + () => this.settings.app.searchEngine, + () => this.settings.app.clipboardNotifications, + () => this.settings.app.enableTranslator, + () => this.settings.app.translatorEngine, + () => this.settings.app.translatorLanguage, + ); + + autorun(() => this.update()); + + document.addEventListener('DOMContentLoaded', () => { + this.findInPage = new FindInPage({ + inputFocusColor: '#CE9FFC', + textColor: '#212121', + }); + }); + + // Add ability to go forward or back with mouse buttons (inside the recipe) + window.addEventListener('mouseup', e => { + if (e.button === 3) { + e.preventDefault(); + e.stopPropagation(); + window.history.back(); + } else if (e.button === 4) { + e.preventDefault(); + e.stopPropagation(); + window.history.forward(); + } + }); + } + + loadRecipeModule(_event, config, recipe) { + debug('loadRecipeModule'); + const modulePath = join(recipe.path, 'webview.js'); + debug('module path', modulePath); + // Delete module from cache + delete require.cache[require.resolve(modulePath)]; + try { + this.recipe = new RecipeWebview( + badgeHandler, + dialogTitleHandler, + notificationsHandler, + sessionHandler, + ); + if (existsSync(modulePath)) { + require(modulePath)(this.recipe, { ...config, recipe }); + debug('Initialize Recipe', config, recipe); + } + + this.settings.service = Object.assign(config, { recipe }); + + // Make sure to update the WebView, otherwise the custom darkmode handler may not be used + this.update(); + } catch (error) { + console.error('Recipe initialization failed', error); + } + + this.loadUserFiles(recipe, config); + } + + async loadUserFiles(recipe, config) { + const styles = document.createElement('style'); + styles.innerHTML = screenShareCss; + + const userCss = join(recipe.path, 'user.css'); + if (pathExistsSync(userCss)) { + const data = readFileSync(userCss); + styles.innerHTML += data.toString(); + } + document.querySelector('head')?.append(styles); + + const userJs = join(recipe.path, 'user.js'); + if (pathExistsSync(userJs)) { + const loadUserJs = () => { + const userJsModule = require(userJs); + + if (typeof userJsModule === 'function') { + this.userscript = new Userscript(this.recipe, this, config); + userJsModule(config, this.userscript); + } + }; + + if (document.readyState !== 'loading') { + loadUserJs(); + } else { + document.addEventListener('DOMContentLoaded', () => { + loadUserJs(); + }); + } + } + } + + openFindInPage() { + this.findInPage?.openFindWindow(); + } + + update() { + debug('enableSpellchecking', this.settings.app.enableSpellchecking); + debug('isDarkModeEnabled', this.settings.service.isDarkModeEnabled); + debug( + 'System spellcheckerLanguage', + this.settings.app.spellcheckerLanguage, + ); + debug( + 'Service spellcheckerLanguage', + this.settings.service.spellcheckerLanguage, + ); + debug('darkReaderSettigs', this.settings.service.darkReaderSettings); + debug('searchEngine', this.settings.app.searchEngine); + debug('enableTranslator', this.settings.app.enableTranslator); + debug('translatorEngine', this.settings.app.translatorEngine); + debug('translatorLanguage', this.settings.app.translatorLanguage); + + if (this.userscript && this.userscript.internal_setSettings) { + this.userscript.internal_setSettings(this.settings); + } + + if (this.settings.app.enableSpellchecking) { + let { spellcheckerLanguage } = this; + debug(`Setting spellchecker language to ${spellcheckerLanguage}`); + if (spellcheckerLanguage.includes('automatic')) { + this.automaticLanguageDetection(); + debug( + 'Found `automatic` locale, falling back to user locale until detected', + this.settings.app.locale, + ); + spellcheckerLanguage = this.settings.app.locale; + } + switchDict(spellcheckerLanguage, this.settings.service.id); + } else { + debug('Disable spellchecker'); + } + + if (!this.recipe) { + this.hasUpdatedBeforeRecipeLoaded = true; + } + + debug( + 'Darkmode enabled?', + this.settings.service.isDarkModeEnabled, + 'Dark theme active?', + // @ts-ignore + this.settings.app.isDarkThemeActive, + ); + + const handlerConfig = { + removeDarkModeStyle, + disableDarkMode, + enableDarkMode, + injectDarkModeStyle: () => + injectDarkModeStyle(this.settings.service.recipe.path), + isDarkModeStyleInjected, + }; + + if (this.settings.service.isDarkModeEnabled) { + debug('Enable dark mode'); + + // Check if recipe has a custom dark mode handler + if (this.recipe && this.recipe.darkModeHandler) { + debug('Using custom dark mode handler'); + + // Remove other dark mode styles if they were already loaded + if (this.hasUpdatedBeforeRecipeLoaded) { + this.hasUpdatedBeforeRecipeLoaded = false; + removeDarkModeStyle(); + disableDarkMode(); + } + + this.recipe.darkModeHandler(true, handlerConfig); + } else if (darkModeStyleExists(this.settings.service.recipe.path)) { + debug('Injecting darkmode from recipe'); + injectDarkModeStyle(this.settings.service.recipe.path); + + // Make sure universal dark mode is disabled + disableDarkMode(); + this.universalDarkModeInjected = false; + } else if ( + this.settings.app.universalDarkMode && + !ignoreList.includes(window.location.host) + ) { + debug('Injecting Dark Reader'); + + // Use Dark Reader instead + const { brightness, contrast, sepia } = + this.settings.service.darkReaderSettings; + enableDarkMode( + { brightness, contrast, sepia }, + { + css: customDarkModeCss[window.location.host] || '', + invert: [], + ignoreImageAnalysis: [], + ignoreInlineStyle: [], + disableStyleSheetsProxy: false, + }, + ); + this.universalDarkModeInjected = true; + } + } else { + debug('Remove dark mode'); + debug('DarkMode disabled - removing remaining styles'); + + if (this.recipe && this.recipe.darkModeHandler) { + // Remove other dark mode styles if they were already loaded + if (this.hasUpdatedBeforeRecipeLoaded) { + this.hasUpdatedBeforeRecipeLoaded = false; + removeDarkModeStyle(); + disableDarkMode(); + } + + this.recipe.darkModeHandler(false, handlerConfig); + } else if (isDarkModeStyleInjected()) { + debug('Removing injected darkmode from recipe'); + removeDarkModeStyle(); + } else { + debug('Removing Dark Reader'); + + disableDarkMode(); + this.universalDarkModeInjected = false; + } + } + + // Remove dark reader if (universal) dark mode was just disabled + if ( + this.universalDarkModeInjected && + (!this.settings.app.darkMode || + !this.settings.service.isDarkModeEnabled || + !this.settings.app.universalDarkMode) + ) { + disableDarkMode(); + this.universalDarkModeInjected = false; + } + } + + updateAppSettings(_event, data) { + this.settings.app = Object.assign(this.settings.app, data); + } + + updateServiceSettings(_event, data) { + this.settings.service = Object.assign(this.settings.service, data); + } + + serviceIdEcho(event) { + debug('Received a service echo ping'); + event.sender.send('service-id', this.settings.service.id); + } + + async automaticLanguageDetection() { + window.addEventListener( + 'keyup', + debounce(async e => { + const element = e.target; + + if (!element) return; + + let value = ''; + if (element.isContentEditable) { + value = element.textContent; + } else if (element.value) { + value = element.value; + } + + // Force a minimum length to get better detection results + if (value.length < 25) return; + + debug('Detecting language for', value); + const locale = await ipcRenderer.invoke('detect-language', { + sample: value, + }); + if (!locale) { + return; + } + + const spellcheckerLocale = + getSpellcheckerLocaleByFuzzyIdentifier(locale); + debug( + 'Language detected reliably, setting spellchecker language to', + spellcheckerLocale, + ); + if (spellcheckerLocale) { + switchDict(spellcheckerLocale, this.settings.service.id); + } + }, 225), + ); + } +} + +/* eslint-disable no-new */ +new RecipeController(); -- cgit v1.2.3-70-g09d2