diff options
Diffstat (limited to 'src/webview')
-rw-r--r-- | src/webview/contextMenu.js | 355 | ||||
-rw-r--r-- | src/webview/lib/RecipeWebview.js | 25 | ||||
-rw-r--r-- | src/webview/recipe.js | 76 | ||||
-rw-r--r-- | src/webview/spellchecker.js | 12 |
4 files changed, 125 insertions, 343 deletions
diff --git a/src/webview/contextMenu.js b/src/webview/contextMenu.js index acd62d675..eeb825ece 100644 --- a/src/webview/contextMenu.js +++ b/src/webview/contextMenu.js | |||
@@ -1,323 +1,50 @@ | |||
1 | // This is heavily based on https://github.com/sindresorhus/electron-context-menu | 1 | import { remote } from 'electron'; |
2 | // ❤ @sindresorhus | 2 | import { ContextMenuBuilder, ContextMenuListener } from 'electron-spellchecker'; |
3 | 3 | ||
4 | import { | ||
5 | clipboard, remote, ipcRenderer, shell, | ||
6 | } from 'electron'; | ||
7 | |||
8 | import { isDevMode, isMac } from '../environment'; | ||
9 | import { SPELLCHECKER_LOCALES } from '../i18n/languages'; | ||
10 | |||
11 | const debug = require('debug')('Ferdi:contextMenu'); | ||
12 | |||
13 | const { Menu } = remote; | ||
14 | |||
15 | // const win = remote.getCurrentWindow(); | ||
16 | const webContents = remote.getCurrentWebContents(); | 4 | const webContents = remote.getCurrentWebContents(); |
17 | 5 | ||
18 | function delUnusedElements(menuTpl) { | 6 | export default async function setupContextMenu(handler) { |
19 | let notDeletedPrevEl; | 7 | const addCustomMenuItems = (menu, menuInfo) => { |
20 | return menuTpl.filter(el => el.visible !== false).filter((el, i, array) => { | 8 | // Add "Paste as plain text" item when right-clicking editable content |
21 | const toDelete = el.type === 'separator' && (!notDeletedPrevEl || i === array.length - 1 || array[i + 1].type === 'separator'); | 9 | if ( |
22 | notDeletedPrevEl = toDelete ? notDeletedPrevEl : el; | 10 | menuInfo.editFlags.canPaste |
23 | return !toDelete; | 11 | && !menuInfo.linkText |
24 | }); | 12 | && !menuInfo.hasImageContents |
25 | } | 13 | ) { |
26 | 14 | menu.insert( | |
27 | const buildMenuTpl = (props, suggestions, isSpellcheckEnabled, defaultSpellcheckerLanguage, spellcheckerLanguage) => { | 15 | 3, |
28 | const { editFlags } = props; | 16 | new remote.MenuItem({ |
29 | const textSelection = props.selectionText.trim(); | 17 | label: 'Paste as plain text', |
30 | const hasText = textSelection.length > 0; | 18 | accelerator: 'CommandOrControl+Shift+V', |
31 | const can = type => editFlags[`can${type}`] && hasText; | 19 | click: () => webContents.pasteAndMatchStyle(), |
32 | 20 | }), | |
33 | const canGoBack = webContents.canGoBack(); | 21 | ); |
34 | const canGoForward = webContents.canGoForward(); | 22 | } |
35 | 23 | ||
36 | // @adlk: we can't use roles here due to a bug with electron where electron.remote.webContents.getFocusedWebContents() returns the first webview in DOM instead of the focused one | 24 | // Add "Open Link in Ferdi" item for links |
37 | // Github issue creation is pending | 25 | if (menuInfo.linkURL) { |
38 | let menuTpl = [ | 26 | menu.insert( |
39 | { | 27 | 2, |
40 | type: 'separator', | 28 | new remote.MenuItem({ |
41 | }, { | 29 | label: 'Open Link in Ferdi', |
42 | id: 'createTodo', | 30 | click: () => { |
43 | label: `Create todo: "${textSelection.length > 15 ? `${textSelection.slice(0, 15)}...` : textSelection}"`, | 31 | window.location.href = menuInfo.linkURL; |
44 | visible: hasText, | ||
45 | click() { | ||
46 | debug('Create todo from selected text', textSelection); | ||
47 | ipcRenderer.sendToHost('feature:todos', { | ||
48 | action: 'todos:create', | ||
49 | data: { | ||
50 | title: textSelection, | ||
51 | url: window.location.href, | ||
52 | }, | 32 | }, |
53 | }); | 33 | }), |
54 | }, | 34 | ); |
55 | }, | ||
56 | { | ||
57 | type: 'separator', | ||
58 | }, { | ||
59 | id: 'lookup', | ||
60 | label: `Look Up "${textSelection.length > 15 ? `${textSelection.slice(0, 15)}...` : textSelection}"`, | ||
61 | visible: isMac && props.mediaType === 'none' && hasText, | ||
62 | click() { | ||
63 | debug('Show definition for selection', textSelection); | ||
64 | webContents.showDefinitionForSelection(); | ||
65 | }, | ||
66 | }, { | ||
67 | type: 'separator', | ||
68 | }, { | ||
69 | id: 'cut', | ||
70 | label: 'Cut', | ||
71 | click() { | ||
72 | if (can('Cut')) { | ||
73 | webContents.cut(); | ||
74 | } | ||
75 | }, | ||
76 | enabled: can('Cut'), | ||
77 | visible: hasText && props.isEditable, | ||
78 | }, { | ||
79 | id: 'copy', | ||
80 | label: 'Copy', | ||
81 | click() { | ||
82 | if (can('Copy')) { | ||
83 | webContents.copy(); | ||
84 | } | ||
85 | }, | ||
86 | enabled: can('Copy'), | ||
87 | visible: props.isEditable || hasText, | ||
88 | }, { | ||
89 | id: 'paste', | ||
90 | label: 'Paste', | ||
91 | click() { | ||
92 | if (editFlags.canPaste) { | ||
93 | webContents.paste(); | ||
94 | } | ||
95 | }, | ||
96 | enabled: editFlags.canPaste, | ||
97 | visible: props.isEditable, | ||
98 | }, { | ||
99 | type: 'separator', | ||
100 | visible: props.isEditable && hasText, | ||
101 | }, { | ||
102 | id: 'searchTextSelection', | ||
103 | label: `Search Google for "${textSelection.length > 15 ? `${textSelection.slice(0, 15)}...` : textSelection}"`, | ||
104 | visible: hasText, | ||
105 | click() { | ||
106 | const url = `https://www.google.com/search?q=${textSelection}`; | ||
107 | debug('Search on Google', url); | ||
108 | shell.openExternal(url); | ||
109 | }, | ||
110 | }, { | ||
111 | type: 'separator', | ||
112 | }, | ||
113 | ]; | ||
114 | |||
115 | if (props.linkURL && props.mediaType === 'none') { | ||
116 | menuTpl = [{ | ||
117 | type: 'separator', | ||
118 | }, { | ||
119 | id: 'openLink', | ||
120 | label: 'Open Link in Browser', | ||
121 | click() { | ||
122 | debug('Open link in Browser', props.linkURL); | ||
123 | shell.openExternal(props.linkURL); | ||
124 | }, | ||
125 | }, { | ||
126 | id: 'copyLink', | ||
127 | label: 'Copy Link', | ||
128 | click() { | ||
129 | clipboard.write({ | ||
130 | bookmark: props.linkText, | ||
131 | text: props.linkURL, | ||
132 | }); | ||
133 | }, | ||
134 | }, { | ||
135 | type: 'separator', | ||
136 | }]; | ||
137 | } | ||
138 | |||
139 | if (props.mediaType === 'image') { | ||
140 | menuTpl.push({ | ||
141 | type: 'separator', | ||
142 | }, { | ||
143 | id: 'openImage', | ||
144 | label: 'Open Image in Browser', | ||
145 | click() { | ||
146 | debug('Open image in Browser', props.srcURL); | ||
147 | shell.openExternal(props.srcURL); | ||
148 | }, | ||
149 | }, { | ||
150 | id: 'copyImageAddress', | ||
151 | label: 'Copy Image Address', | ||
152 | click() { | ||
153 | clipboard.write({ | ||
154 | bookmark: props.srcURL, | ||
155 | text: props.srcURL, | ||
156 | }); | ||
157 | }, | ||
158 | }, { | ||
159 | type: 'separator', | ||
160 | }); | ||
161 | } | ||
162 | |||
163 | if (props.mediaType === 'image') { | ||
164 | menuTpl.push({ | ||
165 | id: 'saveImageAs', | ||
166 | label: 'Save Image As…', | ||
167 | async click() { | ||
168 | if (props.srcURL.startsWith('blob:')) { | ||
169 | const url = new window.URL(props.srcURL.substr(5)); | ||
170 | const fileName = url.pathname.substr(1); | ||
171 | const resp = await window.fetch(props.srcURL); | ||
172 | const blob = await resp.blob(); | ||
173 | const reader = new window.FileReader(); | ||
174 | reader.readAsDataURL(blob); | ||
175 | reader.onloadend = () => { | ||
176 | const base64data = reader.result; | ||
177 | |||
178 | ipcRenderer.send('download-file', { | ||
179 | content: base64data, | ||
180 | fileOptions: { | ||
181 | name: fileName, | ||
182 | mime: blob.type, | ||
183 | }, | ||
184 | }); | ||
185 | }; | ||
186 | debug('binary string', blob); | ||
187 | } else { | ||
188 | ipcRenderer.send('download-file', { url: props.srcURL }); | ||
189 | } | ||
190 | }, | ||
191 | }, { | ||
192 | type: 'separator', | ||
193 | }); | ||
194 | } | ||
195 | |||
196 | if (suggestions.length > 0) { | ||
197 | suggestions.reverse().map(suggestion => menuTpl.unshift({ | ||
198 | id: `suggestion-${suggestion}`, | ||
199 | label: suggestion, | ||
200 | click() { | ||
201 | webContents.replaceMisspelling(suggestion); | ||
202 | }, | ||
203 | })); | ||
204 | } | ||
205 | |||
206 | if (canGoBack || canGoForward) { | ||
207 | menuTpl.push({ | ||
208 | type: 'separator', | ||
209 | }, { | ||
210 | id: 'goBack', | ||
211 | label: 'Go Back', | ||
212 | enabled: canGoBack, | ||
213 | click() { | ||
214 | webContents.goBack(); | ||
215 | }, | ||
216 | }, { | ||
217 | id: 'goForward', | ||
218 | label: 'Go Forward', | ||
219 | enabled: canGoForward, | ||
220 | click() { | ||
221 | webContents.goForward(); | ||
222 | }, | ||
223 | }, { | ||
224 | type: 'separator', | ||
225 | }); | ||
226 | } | ||
227 | |||
228 | const spellcheckingLanguages = []; | ||
229 | Object.keys(SPELLCHECKER_LOCALES).sort(Intl.Collator().compare).forEach((key) => { | ||
230 | spellcheckingLanguages.push({ | ||
231 | id: `lang-${key}`, | ||
232 | label: SPELLCHECKER_LOCALES[key], | ||
233 | type: 'radio', | ||
234 | checked: spellcheckerLanguage === key, | ||
235 | click() { | ||
236 | debug('Setting service spellchecker to', key); | ||
237 | ipcRenderer.sendToHost('set-service-spellchecker-language', key); | ||
238 | }, | ||
239 | }); | ||
240 | }); | ||
241 | |||
242 | menuTpl.push({ | ||
243 | type: 'separator', | ||
244 | }, { | ||
245 | id: 'spellchecker', | ||
246 | label: 'Spell Checking', | ||
247 | visible: isSpellcheckEnabled, | ||
248 | submenu: [ | ||
249 | { | ||
250 | id: 'spellchecker', | ||
251 | label: 'Available Languages', | ||
252 | enabled: false, | ||
253 | }, { | ||
254 | type: 'separator', | ||
255 | }, | ||
256 | { | ||
257 | id: 'resetToDefault', | ||
258 | label: `Reset to system default (${defaultSpellcheckerLanguage === 'automatic' ? 'Automatic' : SPELLCHECKER_LOCALES[defaultSpellcheckerLanguage]})`, | ||
259 | type: 'radio', | ||
260 | visible: defaultSpellcheckerLanguage !== spellcheckerLanguage || (defaultSpellcheckerLanguage !== 'automatic' && spellcheckerLanguage === 'automatic'), | ||
261 | click() { | ||
262 | debug('Resetting service spellchecker to system default'); | ||
263 | ipcRenderer.sendToHost('set-service-spellchecker-language', 'reset'); | ||
264 | }, | ||
265 | }, | ||
266 | { | ||
267 | id: 'automaticDetection', | ||
268 | label: 'Automatic language detection', | ||
269 | type: 'radio', | ||
270 | checked: spellcheckerLanguage === 'automatic', | ||
271 | click() { | ||
272 | debug('Detect language automatically'); | ||
273 | ipcRenderer.sendToHost('set-service-spellchecker-language', 'automatic'); | ||
274 | }, | ||
275 | }, | ||
276 | { | ||
277 | type: 'separator', | ||
278 | visible: defaultSpellcheckerLanguage !== spellcheckerLanguage, | ||
279 | }, | ||
280 | ...spellcheckingLanguages], | ||
281 | }); | ||
282 | |||
283 | |||
284 | if (isDevMode) { | ||
285 | menuTpl.push({ | ||
286 | type: 'separator', | ||
287 | }, { | ||
288 | id: 'inspect', | ||
289 | label: 'Inspect Element', | ||
290 | click() { | ||
291 | webContents.inspectElement(props.x, props.y); | ||
292 | }, | ||
293 | }); | ||
294 | } | ||
295 | |||
296 | return delUnusedElements(menuTpl); | ||
297 | }; | ||
298 | |||
299 | export default function contextMenu(spellcheckProvider, isSpellcheckEnabled, getDefaultSpellcheckerLanguage, getSpellcheckerLanguage) { | ||
300 | webContents.on('context-menu', async (e, props) => { | ||
301 | e.preventDefault(); | ||
302 | |||
303 | let suggestions = []; | ||
304 | if (spellcheckProvider && props.misspelledWord) { | ||
305 | debug('Mispelled word', props.misspelledWord); | ||
306 | suggestions = await spellcheckProvider.getSuggestion(props.misspelledWord); | ||
307 | |||
308 | debug('Suggestions', suggestions); | ||
309 | } | 35 | } |
310 | 36 | ||
311 | const menu = Menu.buildFromTemplate( | 37 | return menu; |
312 | buildMenuTpl( | 38 | }; |
313 | props, | 39 | |
314 | suggestions.slice(0, 5), | 40 | const contextMenuBuilder = new ContextMenuBuilder( |
315 | isSpellcheckEnabled(), | 41 | handler, |
316 | getDefaultSpellcheckerLanguage(), | 42 | null, |
317 | getSpellcheckerLanguage(), | 43 | true, |
318 | ), | 44 | addCustomMenuItems, |
319 | ); | 45 | ); |
320 | 46 | // eslint-disable-next-line no-new | |
321 | menu.popup(); | 47 | new ContextMenuListener((info) => { |
48 | contextMenuBuilder.showPopupMenu(info); | ||
322 | }); | 49 | }); |
323 | } | 50 | } |
diff --git a/src/webview/lib/RecipeWebview.js b/src/webview/lib/RecipeWebview.js index 74d05fc2d..4fac21c55 100644 --- a/src/webview/lib/RecipeWebview.js +++ b/src/webview/lib/RecipeWebview.js | |||
@@ -19,6 +19,8 @@ class RecipeWebview { | |||
19 | 19 | ||
20 | loopFunc = () => null; | 20 | loopFunc = () => null; |
21 | 21 | ||
22 | darkModeHandler = false; | ||
23 | |||
22 | /** | 24 | /** |
23 | * Initialize the loop | 25 | * Initialize the loop |
24 | * | 26 | * |
@@ -67,17 +69,28 @@ class RecipeWebview { | |||
67 | * be an absolute path to the file | 69 | * be an absolute path to the file |
68 | */ | 70 | */ |
69 | injectCSS(...files) { | 71 | injectCSS(...files) { |
70 | files.forEach((file) => { | 72 | files.forEach(async (file) => { |
71 | const data = fs.readFileSync(file); | 73 | if (await fs.exists(file)) { |
72 | const styles = document.createElement('style'); | 74 | const data = await fs.readFile(file); |
73 | styles.innerHTML = data.toString(); | 75 | const styles = document.createElement('style'); |
76 | styles.innerHTML = data.toString(); | ||
74 | 77 | ||
75 | document.querySelector('head').appendChild(styles); | 78 | document.querySelector('head').appendChild(styles); |
76 | 79 | ||
77 | debug('Append styles', styles); | 80 | debug('Append styles', styles); |
81 | } | ||
78 | }); | 82 | }); |
79 | } | 83 | } |
80 | 84 | ||
85 | /** | ||
86 | * Set a custom handler for turning on and off dark mode | ||
87 | * | ||
88 | * @param {function} handler | ||
89 | */ | ||
90 | handleDarkMode(handler) { | ||
91 | this.darkModeHandler = handler; | ||
92 | } | ||
93 | |||
81 | onNotify(fn) { | 94 | onNotify(fn) { |
82 | if (typeof fn === 'function') { | 95 | if (typeof fn === 'function') { |
83 | window.Notification.prototype.onNotify = fn; | 96 | window.Notification.prototype.onNotify = fn; |
diff --git a/src/webview/recipe.js b/src/webview/recipe.js index 1a22542d8..bad5a93b2 100644 --- a/src/webview/recipe.js +++ b/src/webview/recipe.js | |||
@@ -1,10 +1,11 @@ | |||
1 | /* eslint-disable import/first */ | 1 | /* eslint-disable import/first */ |
2 | import { ipcRenderer } from 'electron'; | 2 | import { ipcRenderer, remote } from 'electron'; |
3 | import path from 'path'; | 3 | import path from 'path'; |
4 | import { autorun, computed, observable } from 'mobx'; | 4 | import { autorun, computed, observable } from 'mobx'; |
5 | import fs from 'fs-extra'; | 5 | import fs from 'fs-extra'; |
6 | import { loadModule } from 'cld3-asm'; | 6 | import { loadModule } from 'cld3-asm'; |
7 | import { debounce } from 'lodash'; | 7 | import { debounce } from 'lodash'; |
8 | import { FindInPage } from 'electron-find'; | ||
8 | 9 | ||
9 | // For some services darkreader tries to use the chrome extension message API | 10 | // For some services darkreader tries to use the chrome extension message API |
10 | // This will cause the service to fail loading | 11 | // This will cause the service to fail loading |
@@ -23,7 +24,6 @@ import RecipeWebview from './lib/RecipeWebview'; | |||
23 | 24 | ||
24 | import spellchecker, { switchDict, disable as disableSpellchecker, getSpellcheckerLocaleByFuzzyIdentifier } from './spellchecker'; | 25 | import spellchecker, { switchDict, disable as disableSpellchecker, getSpellcheckerLocaleByFuzzyIdentifier } from './spellchecker'; |
25 | import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode'; | 26 | import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode'; |
26 | import contextMenu from './contextMenu'; | ||
27 | import './notifications'; | 27 | import './notifications'; |
28 | 28 | ||
29 | import { DEFAULT_APP_SETTINGS } from '../config'; | 29 | import { DEFAULT_APP_SETTINGS } from '../config'; |
@@ -48,10 +48,15 @@ class RecipeController { | |||
48 | 'settings-update': 'updateAppSettings', | 48 | 'settings-update': 'updateAppSettings', |
49 | 'service-settings-update': 'updateServiceSettings', | 49 | 'service-settings-update': 'updateServiceSettings', |
50 | 'get-service-id': 'serviceIdEcho', | 50 | 'get-service-id': 'serviceIdEcho', |
51 | 'find-in-page': 'openFindInPage', | ||
51 | }; | 52 | }; |
52 | 53 | ||
53 | universalDarkModeInjected = false; | 54 | universalDarkModeInjected = false; |
54 | 55 | ||
56 | recipe = null; | ||
57 | |||
58 | hasUpdatedBeforeRecipeLoaded = false; | ||
59 | |||
55 | constructor() { | 60 | constructor() { |
56 | this.initialize(); | 61 | this.initialize(); |
57 | } | 62 | } |
@@ -62,6 +67,8 @@ class RecipeController { | |||
62 | 67 | ||
63 | cldIdentifier = null; | 68 | cldIdentifier = null; |
64 | 69 | ||
70 | findInPage = null; | ||
71 | |||
65 | async initialize() { | 72 | async initialize() { |
66 | Object.keys(this.ipcEvents).forEach((channel) => { | 73 | Object.keys(this.ipcEvents).forEach((channel) => { |
67 | ipcRenderer.on(channel, (...args) => { | 74 | ipcRenderer.on(channel, (...args) => { |
@@ -72,16 +79,15 @@ class RecipeController { | |||
72 | 79 | ||
73 | debug('Send "hello" to host'); | 80 | debug('Send "hello" to host'); |
74 | setTimeout(() => ipcRenderer.sendToHost('hello'), 100); | 81 | setTimeout(() => ipcRenderer.sendToHost('hello'), 100); |
75 | 82 | await spellchecker(); | |
76 | this.spellcheckingProvider = await spellchecker(); | ||
77 | contextMenu( | ||
78 | this.spellcheckingProvider, | ||
79 | () => this.settings.app.enableSpellchecking, | ||
80 | () => this.settings.app.spellcheckerLanguage, | ||
81 | () => this.spellcheckerLanguage, | ||
82 | ); | ||
83 | |||
84 | autorun(() => this.update()); | 83 | autorun(() => this.update()); |
84 | |||
85 | document.addEventListener('DOMContentLoaded', () => { | ||
86 | this.findInPage = new FindInPage(remote.getCurrentWebContents(), { | ||
87 | inputFocusColor: '#CE9FFC', | ||
88 | textColor: '#212121', | ||
89 | }); | ||
90 | }); | ||
85 | } | 91 | } |
86 | 92 | ||
87 | loadRecipeModule(event, config, recipe) { | 93 | loadRecipeModule(event, config, recipe) { |
@@ -91,11 +97,15 @@ class RecipeController { | |||
91 | // Delete module from cache | 97 | // Delete module from cache |
92 | delete require.cache[require.resolve(modulePath)]; | 98 | delete require.cache[require.resolve(modulePath)]; |
93 | try { | 99 | try { |
100 | this.recipe = new RecipeWebview(); | ||
94 | // eslint-disable-next-line | 101 | // eslint-disable-next-line |
95 | require(modulePath)(new RecipeWebview(), {...config, recipe,}); | 102 | require(modulePath)(this.recipe, {...config, recipe,}); |
96 | debug('Initialize Recipe', config, recipe); | 103 | debug('Initialize Recipe', config, recipe); |
97 | 104 | ||
98 | this.settings.service = Object.assign(config, { recipe }); | 105 | this.settings.service = Object.assign(config, { recipe }); |
106 | |||
107 | // Make sure to update the WebView, otherwise the custom darkmode handler may not be used | ||
108 | this.update(); | ||
99 | } catch (err) { | 109 | } catch (err) { |
100 | console.error('Recipe initialization failed', err); | 110 | console.error('Recipe initialization failed', err); |
101 | } | 111 | } |
@@ -134,6 +144,10 @@ class RecipeController { | |||
134 | } | 144 | } |
135 | } | 145 | } |
136 | 146 | ||
147 | openFindInPage() { | ||
148 | this.findInPage.openFindWindow(); | ||
149 | } | ||
150 | |||
137 | update() { | 151 | update() { |
138 | debug('enableSpellchecking', this.settings.app.enableSpellchecking); | 152 | debug('enableSpellchecking', this.settings.app.enableSpellchecking); |
139 | debug('isDarkModeEnabled', this.settings.service.isDarkModeEnabled); | 153 | debug('isDarkModeEnabled', this.settings.service.isDarkModeEnabled); |
@@ -160,12 +174,25 @@ class RecipeController { | |||
160 | } | 174 | } |
161 | } | 175 | } |
162 | 176 | ||
177 | if (!this.recipe) { | ||
178 | this.hasUpdatedBeforeRecipeLoaded = true; | ||
179 | } | ||
180 | |||
163 | console.log( | 181 | console.log( |
164 | 'Darkmode enabled?', | 182 | 'Darkmode enabled?', |
165 | this.settings.service.isDarkModeEnabled, | 183 | this.settings.service.isDarkModeEnabled, |
166 | 'Dark theme active?', | 184 | 'Dark theme active?', |
167 | this.settings.app.isDarkThemeActive, | 185 | this.settings.app.isDarkThemeActive, |
168 | ); | 186 | ); |
187 | |||
188 | const handlerConfig = { | ||
189 | removeDarkModeStyle, | ||
190 | disableDarkMode, | ||
191 | enableDarkMode, | ||
192 | injectDarkModeStyle: () => injectDarkModeStyle(this.settings.service.recipe.path), | ||
193 | isDarkModeStyleInjected, | ||
194 | }; | ||
195 | |||
169 | if (this.settings.service.isDarkModeEnabled && this.settings.app.isDarkThemeActive !== false) { | 196 | if (this.settings.service.isDarkModeEnabled && this.settings.app.isDarkThemeActive !== false) { |
170 | debug('Enable dark mode'); | 197 | debug('Enable dark mode'); |
171 | 198 | ||
@@ -175,7 +202,19 @@ class RecipeController { | |||
175 | 202 | ||
176 | console.log('darkmode.css exists? ', darkModeExists ? 'Yes' : 'No'); | 203 | console.log('darkmode.css exists? ', darkModeExists ? 'Yes' : 'No'); |
177 | 204 | ||
178 | if (darkModeExists) { | 205 | // Check if recipe has a custom dark mode handler |
206 | if (this.recipe && this.recipe.darkModeHandler) { | ||
207 | console.log('Using custom dark mode handler'); | ||
208 | |||
209 | // Remove other dark mode styles if they were already loaded | ||
210 | if (this.hasUpdatedBeforeRecipeLoaded) { | ||
211 | this.hasUpdatedBeforeRecipeLoaded = false; | ||
212 | removeDarkModeStyle(); | ||
213 | disableDarkMode(); | ||
214 | } | ||
215 | |||
216 | this.recipe.darkModeHandler(true, handlerConfig); | ||
217 | } else if (darkModeExists) { | ||
179 | console.log('Injecting darkmode.css'); | 218 | console.log('Injecting darkmode.css'); |
180 | injectDarkModeStyle(this.settings.service.recipe.path); | 219 | injectDarkModeStyle(this.settings.service.recipe.path); |
181 | 220 | ||
@@ -195,7 +234,16 @@ class RecipeController { | |||
195 | debug('Remove dark mode'); | 234 | debug('Remove dark mode'); |
196 | console.log('DarkMode disabled - removing remaining styles'); | 235 | console.log('DarkMode disabled - removing remaining styles'); |
197 | 236 | ||
198 | if (isDarkModeStyleInjected()) { | 237 | if (this.recipe && this.recipe.darkModeHandler) { |
238 | // Remove other dark mode styles if they were already loaded | ||
239 | if (this.hasUpdatedBeforeRecipeLoaded) { | ||
240 | this.hasUpdatedBeforeRecipeLoaded = false; | ||
241 | removeDarkModeStyle(); | ||
242 | disableDarkMode(); | ||
243 | } | ||
244 | |||
245 | this.recipe.darkModeHandler(false, handlerConfig); | ||
246 | } else if (isDarkModeStyleInjected()) { | ||
199 | console.log('Removing injected darkmode.css'); | 247 | console.log('Removing injected darkmode.css'); |
200 | removeDarkModeStyle(); | 248 | removeDarkModeStyle(); |
201 | } else { | 249 | } else { |
diff --git a/src/webview/spellchecker.js b/src/webview/spellchecker.js index 8a1c8782b..a33a506b2 100644 --- a/src/webview/spellchecker.js +++ b/src/webview/spellchecker.js | |||
@@ -1,13 +1,12 @@ | |||
1 | import { webFrame } from 'electron'; | 1 | import { webFrame } from 'electron'; |
2 | import { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker'; | 2 | import { SpellCheckHandler } from 'electron-spellchecker'; |
3 | |||
4 | import { SPELLCHECKER_LOCALES } from '../i18n/languages'; | 3 | import { SPELLCHECKER_LOCALES } from '../i18n/languages'; |
4 | import setupContextMenu from './contextMenu'; | ||
5 | 5 | ||
6 | const debug = require('debug')('Franz:spellchecker'); | 6 | const debug = require('debug')('Franz:spellchecker'); |
7 | 7 | ||
8 | let handler; | 8 | let handler; |
9 | let currentDict; | 9 | let currentDict; |
10 | let contextMenuBuilder; | ||
11 | let _isEnabled = false; | 10 | let _isEnabled = false; |
12 | 11 | ||
13 | export async function switchDict(locale) { | 12 | export async function switchDict(locale) { |
@@ -46,12 +45,7 @@ export default async function initialize(languageCode = 'en-us') { | |||
46 | debug('Init spellchecker'); | 45 | debug('Init spellchecker'); |
47 | 46 | ||
48 | switchDict(locale); | 47 | switchDict(locale); |
49 | 48 | setupContextMenu(handler); | |
50 | contextMenuBuilder = new ContextMenuBuilder(handler); | ||
51 | // eslint-disable-next-line no-new | ||
52 | new ContextMenuListener((info) => { | ||
53 | contextMenuBuilder.showPopupMenu(info); | ||
54 | }); | ||
55 | 49 | ||
56 | return handler; | 50 | return handler; |
57 | } catch (err) { | 51 | } catch (err) { |