aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/server/LocalApi.js4
-rw-r--r--src/components/services/content/ServiceView.js16
-rw-r--r--src/components/services/content/ServiceWebview.js1
-rw-r--r--src/components/ui/Modal/styles.js2
-rw-r--r--src/features/todos/components/TodosWebview.js3
-rw-r--r--src/features/todos/store.js4
-rw-r--r--src/features/webControls/components/WebControls.js190
-rw-r--r--src/features/webControls/containers/WebControlsScreen.js128
-rw-r--r--src/index.js17
-rw-r--r--src/lib/Menu.js7
-rw-r--r--src/lib/TouchBar.js4
-rw-r--r--src/models/Recipe.js4
-rw-r--r--src/models/Service.js44
-rw-r--r--src/stores/AppStore.js3
-rw-r--r--src/stores/ServicesStore.js1
-rw-r--r--src/webview/contextMenu.js9
-rw-r--r--src/webview/recipe.js9
-rw-r--r--src/webview/spellchecker.js48
18 files changed, 433 insertions, 61 deletions
diff --git a/src/api/server/LocalApi.js b/src/api/server/LocalApi.js
index ab1604a27..383f38b16 100644
--- a/src/api/server/LocalApi.js
+++ b/src/api/server/LocalApi.js
@@ -45,13 +45,13 @@ export default class LocalApi {
45 const s = session.fromPartition(`persist:service-${serviceId}`); 45 const s = session.fromPartition(`persist:service-${serviceId}`);
46 46
47 debug('LocalApi::clearCache resolves', serviceId); 47 debug('LocalApi::clearCache resolves', serviceId);
48 return new Promise(resolve => s.clearCache(resolve)); 48 return s.clearCache();
49 } 49 }
50 50
51 async clearAppCache() { 51 async clearAppCache() {
52 const s = session.defaultSession; 52 const s = session.defaultSession;
53 53
54 debug('LocalApi::clearCache clearAppCache'); 54 debug('LocalApi::clearCache clearAppCache');
55 return new Promise(resolve => s.clearCache(resolve)); 55 return s.clearCache();
56 } 56 }
57} 57}
diff --git a/src/components/services/content/ServiceView.js b/src/components/services/content/ServiceView.js
index f65f51346..e8df58a1e 100644
--- a/src/components/services/content/ServiceView.js
+++ b/src/components/services/content/ServiceView.js
@@ -12,6 +12,7 @@ import WebviewErrorHandler from './ErrorHandlers/WebviewErrorHandler';
12import ServiceDisabled from './ServiceDisabled'; 12import ServiceDisabled from './ServiceDisabled';
13import ServiceRestricted from './ServiceRestricted'; 13import ServiceRestricted from './ServiceRestricted';
14import ServiceWebview from './ServiceWebview'; 14import ServiceWebview from './ServiceWebview';
15import WebControlsScreen from '../../../features/webControls/containers/WebControlsScreen';
15 16
16export default @observer class ServiceView extends Component { 17export default @observer class ServiceView extends Component {
17 static propTypes = { 18 static propTypes = {
@@ -137,11 +138,16 @@ export default @observer class ServiceView extends Component {
137 type={service.restrictionType} 138 type={service.restrictionType}
138 /> 139 />
139 ) : ( 140 ) : (
140 <ServiceWebview 141 <>
141 service={service} 142 {service.recipe.id === 'franz-custom-website' && (
142 setWebviewReference={setWebviewReference} 143 <WebControlsScreen service={service} />
143 detachService={detachService} 144 )}
144 /> 145 <ServiceWebview
146 service={service}
147 setWebviewReference={setWebviewReference}
148 detachService={detachService}
149 />
150 </>
145 )} 151 )}
146 </> 152 </>
147 )} 153 )}
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js
index 7252c695f..07bd17d9c 100644
--- a/src/components/services/content/ServiceWebview.js
+++ b/src/components/services/content/ServiceWebview.js
@@ -41,6 +41,7 @@ class ServiceWebview extends Component {
41 }} 41 }}
42 onUpdateTargetUrl={this.updateTargetUrl} 42 onUpdateTargetUrl={this.updateTargetUrl}
43 useragent={service.userAgent} 43 useragent={service.userAgent}
44 disablewebsecurity={service.recipe.disablewebsecurity}
44 allowpopups 45 allowpopups
45 /> 46 />
46 ); 47 );
diff --git a/src/components/ui/Modal/styles.js b/src/components/ui/Modal/styles.js
index 49b970c97..c2bebf9bb 100644
--- a/src/components/ui/Modal/styles.js
+++ b/src/components/ui/Modal/styles.js
@@ -13,7 +13,7 @@ export default theme => ({
13 display: 'flex', 13 display: 'flex',
14 }, 14 },
15 modal: { 15 modal: {
16 background: '#FFF', 16 background: theme.colorModalBackground,
17 maxWidth: '90%', 17 maxWidth: '90%',
18 height: 'auto', 18 height: 'auto',
19 margin: 'auto auto', 19 margin: 'auto auto',
diff --git a/src/features/todos/components/TodosWebview.js b/src/features/todos/components/TodosWebview.js
index d052da6f2..f24c0b044 100644
--- a/src/features/todos/components/TodosWebview.js
+++ b/src/features/todos/components/TodosWebview.js
@@ -35,9 +35,6 @@ const styles = theme => ({
35 35
36 transform: ({ isVisible, width }) => `translateX(${isVisible ? 0 : width}px)`, 36 transform: ({ isVisible, width }) => `translateX(${isVisible ? 0 : width}px)`,
37 37
38 '&:hover $closeTodosButton': {
39 opacity: 1,
40 },
41 '& webview': { 38 '& webview': {
42 height: '100%', 39 height: '100%',
43 }, 40 },
diff --git a/src/features/todos/store.js b/src/features/todos/store.js
index abf176604..4480b2545 100644
--- a/src/features/todos/store.js
+++ b/src/features/todos/store.js
@@ -162,6 +162,10 @@ export default class TodoStore extends FeatureStore {
162 theme: isDarkThemeActive ? ThemeType.dark : ThemeType.default, 162 theme: isDarkThemeActive ? ThemeType.dark : ThemeType.default,
163 }, 163 },
164 }); 164 });
165
166 this.webview.addEventListener('new-window', ({ url }) => {
167 this.actions.app.openExternalUrl({ url });
168 });
165 }; 169 };
166 170
167 _goToService = ({ url, serviceId }) => { 171 _goToService = ({ url, serviceId }) => {
diff --git a/src/features/webControls/components/WebControls.js b/src/features/webControls/components/WebControls.js
new file mode 100644
index 000000000..03f601a17
--- /dev/null
+++ b/src/features/webControls/components/WebControls.js
@@ -0,0 +1,190 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss';
5import { Icon } from '@meetfranz/ui';
6
7import {
8 mdiReload, mdiArrowRight, mdiArrowLeft, mdiHomeOutline,
9} from '@mdi/js';
10
11const styles = theme => ({
12 root: {
13 background: theme.colorBackground,
14 position: 'relative',
15 borderLeft: [1, 'solid', theme.todos.todosLayer.borderLeftColor],
16 zIndex: 300,
17 height: 50,
18 display: 'flex',
19 flexDirection: 'row',
20 alignItems: 'center',
21 padding: [0, 20],
22
23 '& + div': {
24 height: 'calc(100% - 50px)',
25 },
26 },
27 button: {
28 width: 30,
29 height: 50,
30 transition: 'opacity 0.25s',
31
32 '&:hover': {
33 opacity: 0.8,
34 },
35
36 '&:disabled': {
37 opacity: 0.5,
38 },
39 },
40 icon: {
41 width: '20px !important',
42 height: 20,
43 marginTop: 5,
44 },
45 input: {
46 marginBottom: 0,
47 height: 'auto',
48 marginLeft: 10,
49 flex: 1,
50 border: 0,
51 padding: [4, 10],
52 borderRadius: theme.borderRadius,
53 background: theme.inputBackground,
54 color: theme.inputColor,
55 },
56 inputButton: {
57 color: theme.colorText,
58 },
59});
60
61@injectSheet(styles) @observer
62class WebControls extends Component {
63 static propTypes = {
64 classes: PropTypes.object.isRequired,
65 goHome: PropTypes.func.isRequired,
66 canGoBack: PropTypes.bool.isRequired,
67 goBack: PropTypes.func.isRequired,
68 canGoForward: PropTypes.bool.isRequired,
69 goForward: PropTypes.func.isRequired,
70 reload: PropTypes.func.isRequired,
71 url: PropTypes.string.isRequired,
72 navigate: PropTypes.func.isRequired,
73 }
74
75 static getDerivedStateFromProps(props, state) {
76 const { url } = props;
77 const { editUrl } = state;
78
79 if (!editUrl) {
80 return {
81 inputUrl: url,
82 editUrl: state.editUrl,
83 };
84 }
85 }
86
87 inputRef = React.createRef();
88
89 state = {
90 inputUrl: '',
91 editUrl: false,
92 }
93
94 render() {
95 const {
96 classes,
97 goHome,
98 canGoBack,
99 goBack,
100 canGoForward,
101 goForward,
102 reload,
103 url,
104 navigate,
105 } = this.props;
106
107 const {
108 inputUrl,
109 editUrl,
110 } = this.state;
111
112 return (
113 <div className={classes.root}>
114 <button
115 onClick={goHome}
116 type="button"
117 className={classes.button}
118 >
119 <Icon
120 icon={mdiHomeOutline}
121 className={classes.icon}
122 />
123 </button>
124 <button
125 onClick={goBack}
126 type="button"
127 className={classes.button}
128 disabled={!canGoBack}
129 >
130 <Icon
131 icon={mdiArrowLeft}
132 className={classes.icon}
133 />
134 </button>
135 <button
136 onClick={goForward}
137 type="button"
138 className={classes.button}
139 disabled={!canGoForward}
140 >
141 <Icon
142 icon={mdiArrowRight}
143 className={classes.icon}
144 />
145 </button>
146 <button
147 onClick={reload}
148 type="button"
149 className={classes.button}
150 >
151 <Icon
152 icon={mdiReload}
153 className={classes.icon}
154 />
155 </button>
156 <input
157 value={editUrl ? inputUrl : url}
158 className={classes.input}
159 onChange={event => this.setState({
160 inputUrl: event.target.value,
161 })}
162 onFocus={(event) => {
163 event.target.select();
164 this.setState({
165 editUrl: true,
166 });
167 }}
168 onKeyDown={(event) => {
169 if (event.key === 'Enter') {
170 this.setState({
171 editUrl: false,
172 });
173 navigate(inputUrl);
174 this.inputRef.current.blur();
175 } else if (event.key === 'Escape') {
176 this.setState({
177 editUrl: false,
178 inputUrl: url,
179 });
180 event.target.blur();
181 }
182 }}
183 ref={this.inputRef}
184 />
185 </div>
186 );
187 }
188}
189
190export default WebControls;
diff --git a/src/features/webControls/containers/WebControlsScreen.js b/src/features/webControls/containers/WebControlsScreen.js
new file mode 100644
index 000000000..1452d5a3d
--- /dev/null
+++ b/src/features/webControls/containers/WebControlsScreen.js
@@ -0,0 +1,128 @@
1import React, { Component } from 'react';
2import { observer, inject } from 'mobx-react';
3import PropTypes from 'prop-types';
4
5import { autorun, observable } from 'mobx';
6import WebControls from '../components/WebControls';
7import ServicesStore from '../../../stores/ServicesStore';
8import Service from '../../../models/Service';
9
10const URL_EVENTS = [
11 'load-commit',
12 // 'dom-ready',
13 'will-navigate',
14 'did-navigate',
15 'did-navigate-in-page',
16];
17
18@inject('stores', 'actions') @observer
19class WebControlsScreen extends Component {
20 @observable url = '';
21
22 @observable canGoBack = false;
23
24 @observable canGoForward = false;
25
26 webview = null;
27
28 autorunDisposer = null;
29
30 componentDidMount() {
31 const { service } = this.props;
32
33 this.autorunDisposer = autorun(() => {
34 if (service.isAttached) {
35 this.webview = service.webview;
36
37 URL_EVENTS.forEach((event) => {
38 this.webview.addEventListener(event, (e) => {
39 if (!e.isMainFrame) return;
40
41 this.url = e.url;
42 this.canGoBack = this.webview.canGoBack();
43 this.canGoForward = this.webview.canGoForward();
44 });
45 });
46 }
47 });
48 }
49
50 componentWillUnmount() {
51 this.autorunDisposer();
52 }
53
54 goHome() {
55 const { reloadActive } = this.props.actions.service;
56
57 if (!this.webview) return;
58
59 reloadActive();
60 }
61
62 reload() {
63 if (!this.webview) return;
64
65 this.webview.reload();
66 }
67
68 goBack() {
69 if (!this.webview) return;
70
71 this.webview.goBack();
72 }
73
74 goForward() {
75 if (!this.webview) return;
76
77 this.webview.goForward();
78 }
79
80 navigate(newUrl) {
81 if (!this.webview) return;
82
83 let url = newUrl;
84
85 try {
86 url = new URL(url).toString();
87 } catch (err) {
88 // eslint-disable-next-line no-useless-escape
89 if (url.match(/^((?!-))(xn--)?[a-z0-9][a-z0-9-_]{0,61}[a-z0-9]{0,1}\.(xn--)?([a-z0-9\-]{1,61}|[a-z0-9-]{1,30}\.[a-z]{2,})$/)) {
90 url = `http://${url}`;
91 } else {
92 url = `https://www.google.com/search?query=${url}`;
93 }
94 }
95
96 this.webview.loadURL(url);
97 this.url = url;
98 }
99
100 render() {
101 return (
102 <WebControls
103 goHome={() => this.goHome()}
104 reload={() => this.reload()}
105 canGoBack={this.canGoBack}
106 goBack={() => this.goBack()}
107 canGoForward={this.canGoForward}
108 goForward={() => this.goForward()}
109 navigate={url => this.navigate(url)}
110 url={this.url}
111 />
112 );
113 }
114}
115
116export default WebControlsScreen;
117
118WebControlsScreen.wrappedComponent.propTypes = {
119 service: PropTypes.instanceOf(Service).isRequired,
120 stores: PropTypes.shape({
121 services: PropTypes.instanceOf(ServicesStore).isRequired,
122 }).isRequired,
123 actions: PropTypes.shape({
124 service: PropTypes.shape({
125 reloadActive: PropTypes.func.isRequired,
126 }).isRequired,
127 }).isRequired,
128};
diff --git a/src/index.js b/src/index.js
index d9d51fd5b..7de7a5e1c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -331,22 +331,7 @@ app.on('login', (event, webContents, request, authInfo, callback) => {
331 debug('browser login event', authInfo); 331 debug('browser login event', authInfo);
332 event.preventDefault(); 332 event.preventDefault();
333 333
334 if (authInfo.isProxy && authInfo.scheme === 'basic') { 334 if (!authInfo.isProxy && authInfo.scheme === 'basic') {
335 debug('Sending service echo ping');
336 webContents.send('get-service-id');
337
338 ipcMain.once('service-id', (e, id) => {
339 debug('Received service id', id);
340
341 const ps = proxySettings.get(id);
342 if (ps) {
343 debug('Sending proxy auth callback for service', id);
344 callback(ps.user, ps.password);
345 } else {
346 debug('No proxy auth config found for', id);
347 }
348 });
349 } else if (authInfo.scheme === 'basic') {
350 debug('basic auth handler', authInfo); 335 debug('basic auth handler', authInfo);
351 basicAuthHandler(mainWindow, authInfo); 336 basicAuthHandler(mainWindow, authInfo);
352 } 337 }
diff --git a/src/lib/Menu.js b/src/lib/Menu.js
index b72f0df96..32bd1644b 100644
--- a/src/lib/Menu.js
+++ b/src/lib/Menu.js
@@ -302,6 +302,9 @@ const _templateFactory = intl => [
302 label: intl.formatMessage(menuItems.pasteAndMatchStyle), 302 label: intl.formatMessage(menuItems.pasteAndMatchStyle),
303 accelerator: 'Cmd+Shift+V', 303 accelerator: 'Cmd+Shift+V',
304 selector: 'pasteAndMatchStyle:', 304 selector: 'pasteAndMatchStyle:',
305 click() {
306 getActiveWebview().pasteAndMatchStyle();
307 },
305 }, 308 },
306 { 309 {
307 label: intl.formatMessage(menuItems.delete), 310 label: intl.formatMessage(menuItems.delete),
@@ -867,6 +870,10 @@ export default class FranzMenu {
867 checked: service.isActive, 870 checked: service.isActive,
868 click: () => { 871 click: () => {
869 this.actions.service.setActive({ serviceId: service.id }); 872 this.actions.service.setActive({ serviceId: service.id });
873
874 if (isMac && i === 0) {
875 app.mainWindow.restore();
876 }
870 }, 877 },
871 }))); 878 })));
872 879
diff --git a/src/lib/TouchBar.js b/src/lib/TouchBar.js
index 97c02d194..1de46d2a3 100644
--- a/src/lib/TouchBar.js
+++ b/src/lib/TouchBar.js
@@ -29,7 +29,7 @@ export default class FranzTouchBar {
29 const { TouchBarButton, TouchBarSpacer } = TouchBar; 29 const { TouchBarButton, TouchBarSpacer } = TouchBar;
30 30
31 const buttons = []; 31 const buttons = [];
32 this.stores.services.enabled.forEach(((service) => { 32 this.stores.services.allDisplayed.forEach(((service) => {
33 buttons.push(new TouchBarButton({ 33 buttons.push(new TouchBarButton({
34 label: `${service.name}${service.unreadDirectMessageCount > 0 34 label: `${service.name}${service.unreadDirectMessageCount > 0
35 ? ' 🔴' : ''} ${service.unreadDirectMessageCount === 0 35 ? ' 🔴' : ''} ${service.unreadDirectMessageCount === 0
@@ -42,7 +42,7 @@ export default class FranzTouchBar {
42 }), new TouchBarSpacer({ size: 'small' })); 42 }), new TouchBarSpacer({ size: 'small' }));
43 })); 43 }));
44 44
45 const touchBar = new TouchBar(buttons); 45 const touchBar = new TouchBar({ items: buttons });
46 currentWindow.setTouchBar(touchBar); 46 currentWindow.setTouchBar(touchBar);
47 } else { 47 } else {
48 currentWindow.setTouchBar(null); 48 currentWindow.setTouchBar(null);
diff --git a/src/models/Recipe.js b/src/models/Recipe.js
index b0d60e75e..00c0f699f 100644
--- a/src/models/Recipe.js
+++ b/src/models/Recipe.js
@@ -36,6 +36,8 @@ export default class Recipe {
36 36
37 message = ''; 37 message = '';
38 38
39 disablewebsecurity = false;
40
39 constructor(data) { 41 constructor(data) {
40 if (!data) { 42 if (!data) {
41 throw Error('Recipe config not valid'); 43 throw Error('Recipe config not valid');
@@ -74,6 +76,8 @@ export default class Recipe {
74 this.urlInputPrefix = data.config.urlInputPrefix || this.urlInputPrefix; 76 this.urlInputPrefix = data.config.urlInputPrefix || this.urlInputPrefix;
75 this.urlInputSuffix = data.config.urlInputSuffix || this.urlInputSuffix; 77 this.urlInputSuffix = data.config.urlInputSuffix || this.urlInputSuffix;
76 78
79 this.disablewebsecurity = data.config.disablewebsecurity || this.disablewebsecurity;
80
77 this.message = data.config.message || this.message; 81 this.message = data.config.message || this.message;
78 } 82 }
79 83
diff --git a/src/models/Service.js b/src/models/Service.js
index 848a84aa2..e45c39564 100644
--- a/src/models/Service.js
+++ b/src/models/Service.js
@@ -134,6 +134,9 @@ export default class Service {
134 id: this.id, 134 id: this.id,
135 spellcheckerLanguage: this.spellcheckerLanguage, 135 spellcheckerLanguage: this.spellcheckerLanguage,
136 isDarkModeEnabled: this.isDarkModeEnabled, 136 isDarkModeEnabled: this.isDarkModeEnabled,
137 team: this.team,
138 url: this.url,
139 hasCustomIcon: this.hasCustomIcon,
137 }; 140 };
138 } 141 }
139 142
@@ -185,19 +188,24 @@ export default class Service {
185 return userAgent; 188 return userAgent;
186 } 189 }
187 190
188 initializeWebViewEvents({ handleIPCMessage, openWindow }) { 191 initializeWebViewEvents({ handleIPCMessage, openWindow, stores }) {
192 const webContents = this.webview.getWebContents();
193
189 this.webview.addEventListener('ipc-message', e => handleIPCMessage({ 194 this.webview.addEventListener('ipc-message', e => handleIPCMessage({
190 serviceId: this.id, 195 serviceId: this.id,
191 channel: e.channel, 196 channel: e.channel,
192 args: e.args, 197 args: e.args,
193 })); 198 }));
194 199
195 this.webview.addEventListener('new-window', (event, url, frameName, options) => openWindow({ 200 this.webview.addEventListener('new-window', (event, url, frameName, options) => {
196 event, 201 console.log('open window', event, url, frameName, options);
197 url, 202 openWindow({
198 frameName, 203 event,
199 options, 204 url,
200 })); 205 frameName,
206 options,
207 });
208 });
201 209
202 this.webview.addEventListener('did-start-loading', (event) => { 210 this.webview.addEventListener('did-start-loading', (event) => {
203 debug('Did start load', this.name, event); 211 debug('Did start load', this.name, event);
@@ -231,6 +239,28 @@ export default class Service {
231 debug('Service crashed', this.name); 239 debug('Service crashed', this.name);
232 this.hasCrashed = true; 240 this.hasCrashed = true;
233 }); 241 });
242
243 webContents.on('login', (event, request, authInfo, callback) => {
244 // const authCallback = callback;
245 debug('browser login event', authInfo);
246 event.preventDefault();
247
248 if (authInfo.isProxy && authInfo.scheme === 'basic') {
249 debug('Sending service echo ping');
250 webContents.send('get-service-id');
251
252 debug('Received service id', this.id);
253
254 const ps = stores.settings.proxy[this.id];
255
256 if (ps) {
257 debug('Sending proxy auth callback for service', this.id);
258 callback(ps.user, ps.password);
259 } else {
260 debug('No proxy auth config found for', this.id);
261 }
262 }
263 });
234 } 264 }
235 265
236 initializeWebViewListener() { 266 initializeWebViewListener() {
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
index 315235ba4..0398b7533 100644
--- a/src/stores/AppStore.js
+++ b/src/stores/AppStore.js
@@ -23,6 +23,7 @@ import { getLocale } from '../helpers/i18n-helpers';
23 23
24import { getServiceIdsFromPartitions, removeServicePartitionDirectory } from '../helpers/service-helpers.js'; 24import { getServiceIdsFromPartitions, removeServicePartitionDirectory } from '../helpers/service-helpers.js';
25import { isValidExternalURL } from '../helpers/url-helpers'; 25import { isValidExternalURL } from '../helpers/url-helpers';
26import { sleep } from '../helpers/async-helpers';
26 27
27const debug = require('debug')('Franz:AppStore'); 28const debug = require('debug')('Franz:AppStore');
28 29
@@ -327,6 +328,8 @@ export default class AppStore extends Store {
327 328
328 await clearAppCache._promise; 329 await clearAppCache._promise;
329 330
331 await sleep(ms('1s'));
332
330 this.getAppCacheSizeRequest.execute(); 333 this.getAppCacheSizeRequest.execute();
331 334
332 this.isClearingAllCache = false; 335 this.isClearingAllCache = false;
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index d1fd2be3d..08befe4eb 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -345,6 +345,7 @@ export default class ServicesStore extends Store {
345 service.initializeWebViewEvents({ 345 service.initializeWebViewEvents({
346 handleIPCMessage: this.actions.service.handleIPCMessage, 346 handleIPCMessage: this.actions.service.handleIPCMessage,
347 openWindow: this.actions.service.openWindow, 347 openWindow: this.actions.service.openWindow,
348 stores: this.stores,
348 }); 349 });
349 service.initializeWebViewListener(); 350 service.initializeWebViewListener();
350 } 351 }
diff --git a/src/webview/contextMenu.js b/src/webview/contextMenu.js
index 83914f581..d3b976554 100644
--- a/src/webview/contextMenu.js
+++ b/src/webview/contextMenu.js
@@ -255,9 +255,9 @@ const buildMenuTpl = (props, suggestions, isSpellcheckEnabled, defaultSpellcheck
255 }, 255 },
256 { 256 {
257 id: 'resetToDefault', 257 id: 'resetToDefault',
258 label: `Reset to system default (${SPELLCHECKER_LOCALES[defaultSpellcheckerLanguage]})`, 258 label: `Reset to system default (${defaultSpellcheckerLanguage === 'automatic' ? 'Automatic' : SPELLCHECKER_LOCALES[defaultSpellcheckerLanguage]})`,
259 type: 'radio', 259 type: 'radio',
260 visible: defaultSpellcheckerLanguage !== spellcheckerLanguage, 260 visible: defaultSpellcheckerLanguage !== spellcheckerLanguage || (defaultSpellcheckerLanguage !== 'automatic' && spellcheckerLanguage === 'automatic'),
261 click() { 261 click() {
262 debug('Resetting service spellchecker to system default'); 262 debug('Resetting service spellchecker to system default');
263 ipcRenderer.sendToHost('set-service-spellchecker-language', 'reset'); 263 ipcRenderer.sendToHost('set-service-spellchecker-language', 'reset');
@@ -297,12 +297,13 @@ const buildMenuTpl = (props, suggestions, isSpellcheckEnabled, defaultSpellcheck
297}; 297};
298 298
299export default function contextMenu(spellcheckProvider, isSpellcheckEnabled, getDefaultSpellcheckerLanguage, getSpellcheckerLanguage) { 299export default function contextMenu(spellcheckProvider, isSpellcheckEnabled, getDefaultSpellcheckerLanguage, getSpellcheckerLanguage) {
300 webContents.on('context-menu', (e, props) => { 300 webContents.on('context-menu', async (e, props) => {
301 e.preventDefault(); 301 e.preventDefault();
302 302
303 let suggestions = []; 303 let suggestions = [];
304 if (spellcheckProvider && props.misspelledWord) { 304 if (spellcheckProvider && props.misspelledWord) {
305 suggestions = spellcheckProvider.getSuggestion(props.misspelledWord); 305 debug('Mispelled word', props.misspelledWord);
306 suggestions = await spellcheckProvider.getSuggestion(props.misspelledWord);
306 307
307 debug('Suggestions', suggestions); 308 debug('Suggestions', suggestions);
308 } 309 }
diff --git a/src/webview/recipe.js b/src/webview/recipe.js
index c223b73de..353eb31fd 100644
--- a/src/webview/recipe.js
+++ b/src/webview/recipe.js
@@ -12,6 +12,7 @@ import contextMenu from './contextMenu';
12import './notifications'; 12import './notifications';
13 13
14import { DEFAULT_APP_SETTINGS } from '../config'; 14import { DEFAULT_APP_SETTINGS } from '../config';
15import { isDevMode } from '../environment';
15 16
16const debug = require('debug')('Franz:Plugin'); 17const debug = require('debug')('Franz:Plugin');
17 18
@@ -173,11 +174,17 @@ new RecipeController();
173// Patching window.open 174// Patching window.open
174const originalWindowOpen = window.open; 175const originalWindowOpen = window.open;
175 176
177
176window.open = (url, frameName, features) => { 178window.open = (url, frameName, features) => {
179 debug('window.open', url, frameName, features);
177 // We need to differentiate if the link should be opened in a popup or in the systems default browser 180 // We need to differentiate if the link should be opened in a popup or in the systems default browser
178 if (!frameName && !features) { 181 if (!frameName && !features && typeof features !== 'string') {
179 return ipcRenderer.sendToHost('new-window', url); 182 return ipcRenderer.sendToHost('new-window', url);
180 } 183 }
181 184
182 return originalWindowOpen(url, frameName, features); 185 return originalWindowOpen(url, frameName, features);
183}; 186};
187
188if (isDevMode) {
189 window.log = console.log;
190}
diff --git a/src/webview/spellchecker.js b/src/webview/spellchecker.js
index 9158b3b94..06cbd283a 100644
--- a/src/webview/spellchecker.js
+++ b/src/webview/spellchecker.js
@@ -1,6 +1,7 @@
1import { webFrame } from 'electron'; 1import { webFrame } from 'electron';
2import { SpellCheckerProvider } from 'electron-hunspell'; 2import { attachSpellCheckProvider, SpellCheckerProvider } from 'electron-hunspell';
3import path from 'path'; 3import path from 'path';
4import { readFileSync } from 'fs';
4 5
5import { DICTIONARY_PATH } from '../config'; 6import { DICTIONARY_PATH } from '../config';
6import { SPELLCHECKER_LOCALES } from '../i18n/languages'; 7import { SPELLCHECKER_LOCALES } from '../i18n/languages';
@@ -10,18 +11,21 @@ const debug = require('debug')('Franz:spellchecker');
10let provider; 11let provider;
11let currentDict; 12let currentDict;
12let _isEnabled = false; 13let _isEnabled = false;
14let attached;
15
16const DEFAULT_LOCALE = 'en-us';
13 17
14async function loadDictionary(locale) { 18async function loadDictionary(locale) {
15 try { 19 try {
16 const fileLocation = path.join(DICTIONARY_PATH, `hunspell-dict-${locale}/${locale}`); 20 const fileLocation = path.join(DICTIONARY_PATH, `hunspell-dict-${locale}/${locale}`);
17 await provider.loadDictionary(locale, `${fileLocation}.dic`, `${fileLocation}.aff`);
18 debug('Loaded dictionary', locale, 'from', fileLocation); 21 debug('Loaded dictionary', locale, 'from', fileLocation);
22 return provider.loadDictionary(locale, readFileSync(`${fileLocation}.dic`), readFileSync(`${fileLocation}.aff`));
19 } catch (err) { 23 } catch (err) {
20 console.error('Could not load dictionary', err); 24 console.error('Could not load dictionary', err);
21 } 25 }
22} 26}
23 27
24export async function switchDict(locale) { 28export async function switchDict(locale = DEFAULT_LOCALE) {
25 try { 29 try {
26 debug('Trying to load dictionary', locale); 30 debug('Trying to load dictionary', locale);
27 31
@@ -40,8 +44,8 @@ export async function switchDict(locale) {
40 if (currentDict) { 44 if (currentDict) {
41 provider.unloadDictionary(locale); 45 provider.unloadDictionary(locale);
42 } 46 }
43 loadDictionary(locale); 47 await loadDictionary(locale);
44 provider.switchDictionary(locale); 48 await attached.switchLanguage(locale);
45 49
46 debug('Switched dictionary to', locale); 50 debug('Switched dictionary to', locale);
47 51
@@ -52,18 +56,32 @@ export async function switchDict(locale) {
52 } 56 }
53} 57}
54 58
55export default async function initialize(languageCode = 'en-us') { 59export function getSpellcheckerLocaleByFuzzyIdentifier(identifier) {
60 const locales = Object.keys(SPELLCHECKER_LOCALES).filter(key => key === identifier.toLowerCase() || key.split('-')[0] === identifier.toLowerCase());
61
62 if (locales.length >= 1) {
63 return locales[0];
64 }
65
66 return null;
67}
68
69export default async function initialize(languageCode = DEFAULT_LOCALE) {
56 try { 70 try {
57 provider = new SpellCheckerProvider(); 71 provider = new SpellCheckerProvider();
58 const locale = languageCode.toLowerCase(); 72 const locale = getSpellcheckerLocaleByFuzzyIdentifier(languageCode);
59 73
60 debug('Init spellchecker'); 74 debug('Init spellchecker');
61 await provider.initialize(); 75 await provider.initialize();
62 // await loadDictionaries();
63 76
64 debug('Available spellchecker dictionaries', provider.availableDictionaries); 77 debug('Attaching spellcheck provider');
78 attached = await attachSpellCheckProvider(provider);
79
80 const availableDictionaries = await provider.getAvailableDictionaries();
65 81
66 switchDict(locale); 82 debug('Available spellchecker dictionaries', availableDictionaries);
83
84 await switchDict(locale);
67 85
68 return provider; 86 return provider;
69 } catch (err) { 87 } catch (err) {
@@ -83,13 +101,3 @@ export function disable() {
83 currentDict = null; 101 currentDict = null;
84 } 102 }
85} 103}
86
87export function getSpellcheckerLocaleByFuzzyIdentifier(identifier) {
88 const locales = Object.keys(SPELLCHECKER_LOCALES).filter(key => key === identifier.toLowerCase() || key.split('-')[0] === identifier.toLowerCase());
89
90 if (locales.length >= 1) {
91 return locales[0];
92 }
93
94 return null;
95}