aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar Stefan Malzner <stefan@adlk.io>2018-12-07 22:39:12 +0100
committerLibravatar Stefan Malzner <stefan@adlk.io>2018-12-07 22:39:12 +0100
commit65aaac06beac7f070a3a81adeffb8e1887d9f12b (patch)
treee2b4f452eef8c17198845e7a59c49b4fe28b1823 /src
parentfeat(Service): Add option to change spellchecking language by service (diff)
downloadferdium-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.js2
-rw-r--r--src/stores/ServicesStore.js14
-rw-r--r--src/webview/contextMenu.js57
-rw-r--r--src/webview/darkmode.js12
-rw-r--r--src/webview/notifications.js7
-rw-r--r--src/webview/plugin.js115
-rw-r--r--src/webview/recipe.js124
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 @@
4import { clipboard, remote, ipcRenderer, shell } from 'electron'; 4import { clipboard, remote, ipcRenderer, shell } from 'electron';
5 5
6import { isDevMode, isMac } from '../environment'; 6import { isDevMode, isMac } from '../environment';
7import { SPELLCHECKER_LOCALES } from '../i18n/languages';
7 8
8const debug = require('debug')('Franz:contextMenu'); 9const debug = require('debug')('Franz:contextMenu');
9 10
@@ -21,7 +22,7 @@ function delUnusedElements(menuTpl) {
21 }); 22 });
22} 23}
23 24
24const buildMenuTpl = (props, suggestions) => { 25const 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
208export default function contextMenu(spellcheckProvider) { 252export 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
1import path from 'path'; 3import path from 'path';
2import fs from 'fs-extra'; 4import fs from 'fs-extra';
3 5
4const ID = 'franz-theme-dark-mode'; 6const debug = require('debug')('Franz:DarkMode');
7
8const chars = [...'abcdefghijklmnopqrstuvwxyz'];
9
10const ID = [...Array(20)].map(() => chars[Math.random() * chars.length | 0]).join``;
5 11
6export function injectDarkModeStyle(recipePath) { 12export 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 @@
1const { ipcRenderer } = require('electron'); 1import { ipcRenderer } from 'electron';
2const uuidV1 = require('uuid/v1'); 2import uuidV1 from 'uuid/v1';
3
4const debug = require('debug')('Franz:Notifications');
3 5
4class Notification { 6class 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 @@
1import { ipcRenderer } from 'electron';
2import path from 'path';
3import { observable } from 'mobx';
4
5import RecipeWebview from './lib/RecipeWebview';
6
7import spellchecker, { switchDict, disable as disableSpellchecker } from './spellchecker';
8import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode';
9import contextMenu from './contextMenu';
10import './notifications';
11
12const debug = require('debug')('Franz:Plugin');
13
14window.franzSettings = {};
15let serviceData;
16let overrideSpellcheckerLanguage = false;
17
18
19ipcRenderer.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
52ipcRenderer.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
65ipcRenderer.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 🤦‍
94ipcRenderer.on('get-service-id', (event) => {
95 debug('Asking for service id', event);
96
97 event.sender.send('service-id', serviceData.id);
98});
99
100
101document.addEventListener('DOMContentLoaded', () => {
102 ipcRenderer.sendToHost('hello');
103}, false);
104
105// Patching window.open
106const originalWindowOpen = window.open;
107
108window.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 @@
1import { ipcRenderer } from 'electron';
2import path from 'path';
3import { autorun, computed, observable } from 'mobx';
4
5import RecipeWebview from './lib/RecipeWebview';
6
7import spellchecker, { switchDict, disable as disableSpellchecker } from './spellchecker';
8import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode';
9import contextMenu from './contextMenu';
10import './notifications';
11
12const debug = require('debug')('Franz:Plugin');
13
14class 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 */
111new RecipeController();
112/* eslint-enable no-new */
113
114// Patching window.open
115const originalWindowOpen = window.open;
116
117window.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};