diff options
author | Stefan Malzner <stefan@adlk.io> | 2018-12-07 22:39:12 +0100 |
---|---|---|
committer | Stefan Malzner <stefan@adlk.io> | 2018-12-07 22:39:12 +0100 |
commit | 65aaac06beac7f070a3a81adeffb8e1887d9f12b (patch) | |
tree | e2b4f452eef8c17198845e7a59c49b4fe28b1823 /src | |
parent | feat(Service): Add option to change spellchecking language by service (diff) | |
download | ferdium-app-65aaac06beac7f070a3a81adeffb8e1887d9f12b.tar.gz ferdium-app-65aaac06beac7f070a3a81adeffb8e1887d9f12b.tar.zst ferdium-app-65aaac06beac7f070a3a81adeffb8e1887d9f12b.zip |
chore(Recipe): Refactor recipe plugin
Diffstat (limited to 'src')
-rw-r--r-- | src/components/services/content/ServiceWebview.js | 2 | ||||
-rw-r--r-- | src/stores/ServicesStore.js | 14 | ||||
-rw-r--r-- | src/webview/contextMenu.js | 57 | ||||
-rw-r--r-- | src/webview/darkmode.js | 12 | ||||
-rw-r--r-- | src/webview/notifications.js | 7 | ||||
-rw-r--r-- | src/webview/plugin.js | 115 | ||||
-rw-r--r-- | src/webview/recipe.js | 124 |
7 files changed, 208 insertions, 123 deletions
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js index 7163209ee..6e56de92f 100644 --- a/src/components/services/content/ServiceWebview.js +++ b/src/components/services/content/ServiceWebview.js | |||
@@ -96,7 +96,7 @@ export default @observer class ServiceWebview extends Component { | |||
96 | ref={(element) => { this.webview = element; }} | 96 | ref={(element) => { this.webview = element; }} |
97 | autosize | 97 | autosize |
98 | src={service.url} | 98 | src={service.url} |
99 | preload="./webview/plugin.js" | 99 | preload="./webview/recipe.js" |
100 | partition={`persist:service-${service.id}`} | 100 | partition={`persist:service-${service.id}`} |
101 | onDidAttach={() => setWebviewReference({ | 101 | onDidAttach={() => setWebviewReference({ |
102 | serviceId: service.id, | 102 | serviceId: service.id, |
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js index 99b091589..ccb4eed04 100644 --- a/src/stores/ServicesStore.js +++ b/src/stores/ServicesStore.js | |||
@@ -400,6 +400,18 @@ export default class ServicesStore extends Store { | |||
400 | const url = args[0]; | 400 | const url = args[0]; |
401 | 401 | ||
402 | this.actions.app.openExternalUrl({ url }); | 402 | this.actions.app.openExternalUrl({ url }); |
403 | } else if (channel === 'set-service-spellchecker-language') { | ||
404 | if (!args) { | ||
405 | console.warn('Did not receive locale'); | ||
406 | } else { | ||
407 | this.actions.service.updateService({ | ||
408 | serviceId, | ||
409 | serviceData: { | ||
410 | spellcheckerLanguage: args[0] === 'reset' ? '' : args[0], | ||
411 | }, | ||
412 | redirect: false, | ||
413 | }); | ||
414 | } | ||
403 | } | 415 | } |
404 | } | 416 | } |
405 | 417 | ||
@@ -625,7 +637,7 @@ export default class ServicesStore extends Store { | |||
625 | const service = this.one(serviceId); | 637 | const service = this.one(serviceId); |
626 | 638 | ||
627 | if (service.webview) { | 639 | if (service.webview) { |
628 | service.webview.send('initializeRecipe', service); | 640 | service.webview.send('initialize-recipe', service); |
629 | } | 641 | } |
630 | } | 642 | } |
631 | 643 | ||
diff --git a/src/webview/contextMenu.js b/src/webview/contextMenu.js index ad156128c..f9afa1913 100644 --- a/src/webview/contextMenu.js +++ b/src/webview/contextMenu.js | |||
@@ -4,6 +4,7 @@ | |||
4 | import { clipboard, remote, ipcRenderer, shell } from 'electron'; | 4 | import { clipboard, remote, ipcRenderer, shell } from 'electron'; |
5 | 5 | ||
6 | import { isDevMode, isMac } from '../environment'; | 6 | import { isDevMode, isMac } from '../environment'; |
7 | import { SPELLCHECKER_LOCALES } from '../i18n/languages'; | ||
7 | 8 | ||
8 | const debug = require('debug')('Franz:contextMenu'); | 9 | const debug = require('debug')('Franz:contextMenu'); |
9 | 10 | ||
@@ -21,7 +22,7 @@ function delUnusedElements(menuTpl) { | |||
21 | }); | 22 | }); |
22 | } | 23 | } |
23 | 24 | ||
24 | const buildMenuTpl = (props, suggestions) => { | 25 | const buildMenuTpl = (props, suggestions, defaultSpellcheckerLanguage, spellcheckerLanguage) => { |
25 | const { editFlags } = props; | 26 | const { editFlags } = props; |
26 | const textSelection = props.selectionText.trim(); | 27 | const textSelection = props.selectionText.trim(); |
27 | const hasText = textSelection.length > 0; | 28 | const hasText = textSelection.length > 0; |
@@ -190,6 +191,49 @@ const buildMenuTpl = (props, suggestions) => { | |||
190 | }); | 191 | }); |
191 | } | 192 | } |
192 | 193 | ||
194 | const spellcheckingLanguages = []; | ||
195 | Object.keys(SPELLCHECKER_LOCALES).sort(Intl.Collator().compare).forEach((key) => { | ||
196 | spellcheckingLanguages.push({ | ||
197 | id: `lang-${key}`, | ||
198 | label: SPELLCHECKER_LOCALES[key], | ||
199 | type: 'radio', | ||
200 | checked: spellcheckerLanguage === key, | ||
201 | click() { | ||
202 | debug('Setting service spellchecker to', key); | ||
203 | ipcRenderer.sendToHost('set-service-spellchecker-language', key); | ||
204 | }, | ||
205 | }); | ||
206 | }); | ||
207 | |||
208 | menuTpl.push({ | ||
209 | type: 'separator', | ||
210 | }, { | ||
211 | id: 'spellchecker', | ||
212 | label: 'Spellchecker', | ||
213 | submenu: [ | ||
214 | { | ||
215 | id: 'spellchecker', | ||
216 | label: 'Available Languages', | ||
217 | enabled: false, | ||
218 | }, { | ||
219 | type: 'separator', | ||
220 | }, | ||
221 | { | ||
222 | id: 'resetToDefault', | ||
223 | label: `Reset to system default (${SPELLCHECKER_LOCALES[defaultSpellcheckerLanguage]})`, | ||
224 | type: 'radio', | ||
225 | click() { | ||
226 | debug('Resetting service spellchecker to system default'); | ||
227 | ipcRenderer.sendToHost('set-service-spellchecker-language', 'reset'); | ||
228 | }, | ||
229 | }, | ||
230 | { | ||
231 | type: 'separator', | ||
232 | }, | ||
233 | ...spellcheckingLanguages], | ||
234 | }); | ||
235 | |||
236 | |||
193 | if (isDevMode) { | 237 | if (isDevMode) { |
194 | menuTpl.push({ | 238 | menuTpl.push({ |
195 | type: 'separator', | 239 | type: 'separator', |
@@ -205,7 +249,7 @@ const buildMenuTpl = (props, suggestions) => { | |||
205 | return delUnusedElements(menuTpl); | 249 | return delUnusedElements(menuTpl); |
206 | }; | 250 | }; |
207 | 251 | ||
208 | export default function contextMenu(spellcheckProvider) { | 252 | export default function contextMenu(spellcheckProvider, getDefaultSpellcheckerLanguage, getSpellcheckerLanguage) { |
209 | webContents.on('context-menu', (e, props) => { | 253 | webContents.on('context-menu', (e, props) => { |
210 | e.preventDefault(); | 254 | e.preventDefault(); |
211 | 255 | ||
@@ -216,7 +260,14 @@ export default function contextMenu(spellcheckProvider) { | |||
216 | debug('Suggestions', suggestions); | 260 | debug('Suggestions', suggestions); |
217 | } | 261 | } |
218 | 262 | ||
219 | const menu = Menu.buildFromTemplate(buildMenuTpl(props, suggestions.slice(0, 5))); | 263 | const menu = Menu.buildFromTemplate( |
264 | buildMenuTpl( | ||
265 | props, | ||
266 | suggestions.slice(0, 5), | ||
267 | getDefaultSpellcheckerLanguage(), | ||
268 | getSpellcheckerLanguage(), | ||
269 | ), | ||
270 | ); | ||
220 | 271 | ||
221 | menu.popup(remote.getCurrentWindow()); | 272 | menu.popup(remote.getCurrentWindow()); |
222 | }); | 273 | }); |
diff --git a/src/webview/darkmode.js b/src/webview/darkmode.js index 9830ef33c..73c7007c6 100644 --- a/src/webview/darkmode.js +++ b/src/webview/darkmode.js | |||
@@ -1,7 +1,13 @@ | |||
1 | /* eslint no-bitwise: ["error", { "int32Hint": true }] */ | ||
2 | |||
1 | import path from 'path'; | 3 | import path from 'path'; |
2 | import fs from 'fs-extra'; | 4 | import fs from 'fs-extra'; |
3 | 5 | ||
4 | const ID = 'franz-theme-dark-mode'; | 6 | const debug = require('debug')('Franz:DarkMode'); |
7 | |||
8 | const chars = [...'abcdefghijklmnopqrstuvwxyz']; | ||
9 | |||
10 | const ID = [...Array(20)].map(() => chars[Math.random() * chars.length | 0]).join``; | ||
5 | 11 | ||
6 | export function injectDarkModeStyle(recipePath) { | 12 | export function injectDarkModeStyle(recipePath) { |
7 | const darkModeStyle = path.join(recipePath, 'darkmode.css'); | 13 | const darkModeStyle = path.join(recipePath, 'darkmode.css'); |
@@ -12,6 +18,8 @@ export function injectDarkModeStyle(recipePath) { | |||
12 | styles.innerHTML = data.toString(); | 18 | styles.innerHTML = data.toString(); |
13 | 19 | ||
14 | document.querySelector('head').appendChild(styles); | 20 | document.querySelector('head').appendChild(styles); |
21 | |||
22 | debug('Injected Dark Mode style with ID', ID); | ||
15 | } | 23 | } |
16 | } | 24 | } |
17 | 25 | ||
@@ -20,6 +28,8 @@ export function removeDarkModeStyle() { | |||
20 | 28 | ||
21 | if (style) { | 29 | if (style) { |
22 | style.remove(); | 30 | style.remove(); |
31 | |||
32 | debug('Removed Dark Mode Style with ID', ID); | ||
23 | } | 33 | } |
24 | } | 34 | } |
25 | 35 | ||
diff --git a/src/webview/notifications.js b/src/webview/notifications.js index 2020bbdc6..f8fe53e1b 100644 --- a/src/webview/notifications.js +++ b/src/webview/notifications.js | |||
@@ -1,10 +1,13 @@ | |||
1 | const { ipcRenderer } = require('electron'); | 1 | import { ipcRenderer } from 'electron'; |
2 | const uuidV1 = require('uuid/v1'); | 2 | import uuidV1 from 'uuid/v1'; |
3 | |||
4 | const debug = require('debug')('Franz:Notifications'); | ||
3 | 5 | ||
4 | class Notification { | 6 | class Notification { |
5 | static permission = 'granted'; | 7 | static permission = 'granted'; |
6 | 8 | ||
7 | constructor(title = '', options = {}) { | 9 | constructor(title = '', options = {}) { |
10 | debug('New notification', title, options); | ||
8 | this.title = title; | 11 | this.title = title; |
9 | this.options = options; | 12 | this.options = options; |
10 | this.notificationId = uuidV1(); | 13 | this.notificationId = uuidV1(); |
diff --git a/src/webview/plugin.js b/src/webview/plugin.js deleted file mode 100644 index 6d4e65062..000000000 --- a/src/webview/plugin.js +++ /dev/null | |||
@@ -1,115 +0,0 @@ | |||
1 | import { ipcRenderer } from 'electron'; | ||
2 | import path from 'path'; | ||
3 | import { observable } from 'mobx'; | ||
4 | |||
5 | import RecipeWebview from './lib/RecipeWebview'; | ||
6 | |||
7 | import spellchecker, { switchDict, disable as disableSpellchecker } from './spellchecker'; | ||
8 | import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode'; | ||
9 | import contextMenu from './contextMenu'; | ||
10 | import './notifications'; | ||
11 | |||
12 | const debug = require('debug')('Franz:Plugin'); | ||
13 | |||
14 | window.franzSettings = {}; | ||
15 | let serviceData; | ||
16 | let overrideSpellcheckerLanguage = false; | ||
17 | |||
18 | |||
19 | ipcRenderer.on('initializeRecipe', (e, data) => { | ||
20 | const modulePath = path.join(data.recipe.path, 'webview.js'); | ||
21 | // Delete module from cache | ||
22 | delete require.cache[require.resolve(modulePath)]; | ||
23 | try { | ||
24 | // eslint-disable-next-line | ||
25 | require(modulePath)(new RecipeWebview(), data); | ||
26 | debug('Initialize Recipe', data); | ||
27 | |||
28 | serviceData = data; | ||
29 | |||
30 | if (data.isDarkModeEnabled) { | ||
31 | injectDarkModeStyle(data.recipe.path); | ||
32 | debug('Add dark theme styles'); | ||
33 | } | ||
34 | |||
35 | if (data.spellcheckerLanguage) { | ||
36 | debug('Overriding spellchecker language to', data.spellcheckerLanguage); | ||
37 | switchDict(data.spellcheckerLanguage); | ||
38 | |||
39 | overrideSpellcheckerLanguage = true; | ||
40 | } | ||
41 | } catch (err) { | ||
42 | debug('Recipe initialization failed', err); | ||
43 | } | ||
44 | }); | ||
45 | |||
46 | // Needs to run asap to intialize dictionaries | ||
47 | (async () => { | ||
48 | const spellcheckingProvider = await spellchecker(); | ||
49 | contextMenu(spellcheckingProvider); | ||
50 | })(); | ||
51 | |||
52 | ipcRenderer.on('settings-update', async (e, data) => { | ||
53 | debug('Settings update received', data); | ||
54 | |||
55 | if (!data.enableSpellchecking) { | ||
56 | disableSpellchecker(); | ||
57 | } else if (!overrideSpellcheckerLanguage) { | ||
58 | debug('Setting spellchecker language based on app settings to', data.spellcheckerLanguage); | ||
59 | switchDict(data.spellcheckerLanguage); | ||
60 | } | ||
61 | |||
62 | window.franzSettings = data; | ||
63 | }); | ||
64 | |||
65 | ipcRenderer.on('service-settings-update', (e, data) => { | ||
66 | debug('Service settings update received', data); | ||
67 | |||
68 | serviceData = data; | ||
69 | |||
70 | if (data.isDarkModeEnabled && !isDarkModeStyleInjected()) { | ||
71 | injectDarkModeStyle(serviceData.recipe.path); | ||
72 | |||
73 | debug('Enable service dark mode'); | ||
74 | } else if (!data.isDarkModeEnabled && isDarkModeStyleInjected()) { | ||
75 | removeDarkModeStyle(); | ||
76 | |||
77 | debug('Disable service dark mode'); | ||
78 | } | ||
79 | |||
80 | if (data.spellcheckerLanguage) { | ||
81 | debug('Overriding spellchecker language to', data.spellcheckerLanguage); | ||
82 | switchDict(data.spellcheckerLanguage); | ||
83 | |||
84 | overrideSpellcheckerLanguage = true; | ||
85 | } else { | ||
86 | debug('Going back to default spellchecker language to', window.franzSettings.spellcheckerLanguage); | ||
87 | switchDict(window.franzSettings.spellcheckerLanguage); | ||
88 | |||
89 | overrideSpellcheckerLanguage = false; | ||
90 | } | ||
91 | }); | ||
92 | |||
93 | // Needed for current implementation of electrons 'login' event 🤦 | ||
94 | ipcRenderer.on('get-service-id', (event) => { | ||
95 | debug('Asking for service id', event); | ||
96 | |||
97 | event.sender.send('service-id', serviceData.id); | ||
98 | }); | ||
99 | |||
100 | |||
101 | document.addEventListener('DOMContentLoaded', () => { | ||
102 | ipcRenderer.sendToHost('hello'); | ||
103 | }, false); | ||
104 | |||
105 | // Patching window.open | ||
106 | const originalWindowOpen = window.open; | ||
107 | |||
108 | window.open = (url, frameName, features) => { | ||
109 | // We need to differentiate if the link should be opened in a popup or in the systems default browser | ||
110 | if (!frameName && !features) { | ||
111 | return ipcRenderer.sendToHost('new-window', url); | ||
112 | } | ||
113 | |||
114 | return originalWindowOpen(url, frameName, features); | ||
115 | }; | ||
diff --git a/src/webview/recipe.js b/src/webview/recipe.js new file mode 100644 index 000000000..a2c157af8 --- /dev/null +++ b/src/webview/recipe.js | |||
@@ -0,0 +1,124 @@ | |||
1 | import { ipcRenderer } from 'electron'; | ||
2 | import path from 'path'; | ||
3 | import { autorun, computed, observable } from 'mobx'; | ||
4 | |||
5 | import RecipeWebview from './lib/RecipeWebview'; | ||
6 | |||
7 | import spellchecker, { switchDict, disable as disableSpellchecker } from './spellchecker'; | ||
8 | import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode'; | ||
9 | import contextMenu from './contextMenu'; | ||
10 | import './notifications'; | ||
11 | |||
12 | const debug = require('debug')('Franz:Plugin'); | ||
13 | |||
14 | class RecipeController { | ||
15 | @observable settings = { | ||
16 | overrideSpellcheckerLanguage: false, | ||
17 | app: {}, | ||
18 | service: {}, | ||
19 | }; | ||
20 | |||
21 | spellcheckProvider = null; | ||
22 | |||
23 | ipcEvents = { | ||
24 | 'initialize-recipe': 'loadRecipeModule', | ||
25 | 'settings-update': 'updateAppSettings', | ||
26 | 'service-settings-update': 'updateServiceSettings', | ||
27 | 'get-service-id': 'serviceIdEcho', | ||
28 | } | ||
29 | |||
30 | constructor() { | ||
31 | this.initialize(); | ||
32 | } | ||
33 | |||
34 | @computed get spellcheckerLanguage() { | ||
35 | return this.settings.service.spellcheckerLanguage || this.settings.app.spellcheckerLanguage; | ||
36 | } | ||
37 | |||
38 | async initialize() { | ||
39 | Object.keys(this.ipcEvents).forEach((channel) => { | ||
40 | ipcRenderer.on(channel, (event, data) => { | ||
41 | debug('Received IPC event for channel', channel, 'with', data); | ||
42 | this[this.ipcEvents[channel]](event, data); | ||
43 | }); | ||
44 | }); | ||
45 | |||
46 | debug('Send "hello" to host'); | ||
47 | setTimeout(() => ipcRenderer.sendToHost('hello'), 100); | ||
48 | |||
49 | this.spellcheckingProvider = await spellchecker(); | ||
50 | contextMenu( | ||
51 | this.spellcheckingProvider, | ||
52 | () => this.settings.app.spellcheckerLanguage, | ||
53 | () => this.spellcheckerLanguage); | ||
54 | |||
55 | autorun(() => this.update()); | ||
56 | } | ||
57 | |||
58 | loadRecipeModule(event, data) { | ||
59 | debug('loadRecipeModule'); | ||
60 | const modulePath = path.join(data.recipe.path, 'webview.js'); | ||
61 | // Delete module from cache | ||
62 | delete require.cache[require.resolve(modulePath)]; | ||
63 | try { | ||
64 | // eslint-disable-next-line | ||
65 | require(modulePath)(new RecipeWebview(), data); | ||
66 | debug('Initialize Recipe', data); | ||
67 | |||
68 | this.settings.service = data; | ||
69 | } catch (err) { | ||
70 | console.error('Recipe initialization failed', err); | ||
71 | } | ||
72 | } | ||
73 | |||
74 | update() { | ||
75 | debug('enableSpellchecking', this.settings.app.enableSpellchecking); | ||
76 | debug('isDarkModeEnabled', this.settings.service.isDarkModeEnabled); | ||
77 | debug('System spellcheckerLanguage', this.settings.app.spellcheckerLanguage); | ||
78 | debug('Service spellcheckerLanguage', this.settings.service.spellcheckerLanguage); | ||
79 | |||
80 | if (this.settings.app.enableSpellchecking) { | ||
81 | debug('Setting spellchecker language to', this.spellcheckerLanguage); | ||
82 | switchDict(this.spellcheckerLanguage); | ||
83 | } else { | ||
84 | disableSpellchecker(); | ||
85 | } | ||
86 | |||
87 | console.log(this.settings.service); | ||
88 | if (this.settings.service.isDarkModeEnabled) { | ||
89 | debug('Enable dark mode'); | ||
90 | injectDarkModeStyle(this.settings.service.recipe.path); | ||
91 | } else if (isDarkModeStyleInjected()) { | ||
92 | debug('Remove dark mode'); | ||
93 | removeDarkModeStyle(); | ||
94 | } | ||
95 | } | ||
96 | |||
97 | updateAppSettings(event, data) { | ||
98 | this.settings.app = Object.assign(this.settings.app, data); | ||
99 | } | ||
100 | |||
101 | updateServiceSettings(event, data) { | ||
102 | this.settings.service = Object.assign(this.settings.service, data); | ||
103 | } | ||
104 | |||
105 | serviceIdEcho(event) { | ||
106 | event.sender.send('service-id', this.settings.service.id); | ||
107 | } | ||
108 | } | ||
109 | |||
110 | /* eslint-disable no-new */ | ||
111 | new RecipeController(); | ||
112 | /* eslint-enable no-new */ | ||
113 | |||
114 | // Patching window.open | ||
115 | const originalWindowOpen = window.open; | ||
116 | |||
117 | window.open = (url, frameName, features) => { | ||
118 | // We need to differentiate if the link should be opened in a popup or in the systems default browser | ||
119 | if (!frameName && !features) { | ||
120 | return ipcRenderer.sendToHost('new-window', url); | ||
121 | } | ||
122 | |||
123 | return originalWindowOpen(url, frameName, features); | ||
124 | }; | ||