aboutsummaryrefslogtreecommitdiffstats
path: root/src/webview
diff options
context:
space:
mode:
authorLibravatar Stefan Malzner <stefan@adlk.io>2018-11-30 14:32:45 +0100
committerLibravatar Stefan Malzner <stefan@adlk.io>2018-11-30 14:32:45 +0100
commit3d87c0e45cead95ddb6c11fc6540b82e375bdcf5 (patch)
treec91f425a39cb585242d6df5b4070de4a2141b3b4 /src/webview
parentMerge branch 'update/monetization' into develop (diff)
downloadferdium-app-3d87c0e45cead95ddb6c11fc6540b82e375bdcf5.tar.gz
ferdium-app-3d87c0e45cead95ddb6c11fc6540b82e375bdcf5.tar.zst
ferdium-app-3d87c0e45cead95ddb6c11fc6540b82e375bdcf5.zip
feat(App): Improved spell checker & context menu
Diffstat (limited to 'src/webview')
-rw-r--r--src/webview/contextMenu.js175
-rw-r--r--src/webview/plugin.js29
-rw-r--r--src/webview/spellchecker.js111
3 files changed, 260 insertions, 55 deletions
diff --git a/src/webview/contextMenu.js b/src/webview/contextMenu.js
new file mode 100644
index 000000000..4dda51bde
--- /dev/null
+++ b/src/webview/contextMenu.js
@@ -0,0 +1,175 @@
1// This is heavily based on https://github.com/sindresorhus/electron-context-menu
2// ❤ @sindresorhus
3
4import { clipboard, remote, ipcRenderer, shell } from 'electron';
5
6import { isDevMode } from '../environment';
7
8const debug = require('debug')('Franz:contextMenu');
9
10const { Menu } = remote;
11
12// const win = remote.getCurrentWindow();
13const webContents = remote.getCurrentWebContents();
14
15function delUnusedElements(menuTpl) {
16 let notDeletedPrevEl;
17 return menuTpl.filter(el => el.visible !== false).filter((el, i, array) => {
18 const toDelete = el.type === 'separator' && (!notDeletedPrevEl || i === array.length - 1 || array[i + 1].type === 'separator');
19 notDeletedPrevEl = toDelete ? notDeletedPrevEl : el;
20 return !toDelete;
21 });
22}
23
24const buildMenuTpl = (props, suggestions) => {
25 const { editFlags } = props;
26 const hasText = props.selectionText.trim().length > 0;
27 const can = type => editFlags[`can${type}`] && hasText;
28
29 let menuTpl = [
30 {
31 type: 'separator',
32 }, {
33 id: 'cut',
34 role: can('Cut') ? 'cut' : '',
35 enabled: can('Cut'),
36 visible: props.isEditable,
37 }, {
38 id: 'copy',
39 label: 'Copy',
40 role: can('Copy') ? 'copy' : '',
41 enabled: can('Copy'),
42 visible: props.isEditable || hasText,
43 }, {
44 id: 'paste',
45 label: 'Paste',
46 role: editFlags.canPaste ? 'paste' : '',
47 enabled: editFlags.canPaste,
48 visible: props.isEditable,
49 }, {
50 type: 'separator',
51 },
52 ];
53
54 if (props.linkURL && props.mediaType === 'none') {
55 menuTpl = [{
56 type: 'separator',
57 }, {
58 id: 'openLink',
59 label: 'Open Link in Browser',
60 click() {
61 shell.openExternal(props.linkURL);
62 },
63 }, {
64 id: 'copyLink',
65 label: 'Copy Link',
66 click() {
67 clipboard.write({
68 bookmark: props.linkText,
69 text: props.linkURL,
70 });
71 },
72 }, {
73 type: 'separator',
74 }];
75 }
76
77 if (props.mediaType === 'image') {
78 menuTpl.push({
79 type: 'separator',
80 }, {
81 id: 'openImage',
82 label: 'Open Image in Browser',
83 click() {
84 shell.openExternal(props.srcURL);
85 },
86 }, {
87 id: 'copyImageAddress',
88 label: 'Copy Image Address',
89 click() {
90 clipboard.write({
91 bookmark: props.srcURL,
92 text: props.srcURL,
93 });
94 },
95 }, {
96 type: 'separator',
97 });
98 }
99
100 if (props.mediaType === 'image') {
101 menuTpl.push({
102 id: 'saveImageAs',
103 label: 'Save Image As…',
104 async click() {
105 if (props.srcURL.startsWith('blob:')) {
106 const url = new window.URL(props.srcURL.substr(5));
107 const fileName = url.pathname.substr(1);
108 const resp = await window.fetch(props.srcURL);
109 const blob = await resp.blob();
110 const reader = new window.FileReader();
111 reader.readAsDataURL(blob);
112 reader.onloadend = () => {
113 const base64data = reader.result;
114
115 ipcRenderer.send('download-file', {
116 content: base64data,
117 fileOptions: {
118 name: fileName,
119 mime: blob.type,
120 },
121 });
122 };
123 debug('binary string', blob);
124 } else {
125 ipcRenderer.send('download-file', { url: props.srcURL });
126 }
127 },
128 }, {
129 type: 'separator',
130 });
131 }
132
133 if (suggestions.length > 0) {
134 suggestions.reverse().map(suggestion => menuTpl.unshift({
135 id: `suggestion-${suggestion}`,
136 label: suggestion,
137 click() {
138 webContents.replaceMisspelling(suggestion);
139 },
140 }));
141 }
142
143 if (isDevMode) {
144 menuTpl.push({
145 type: 'separator',
146 }, {
147 id: 'inspect',
148 label: 'Inspect Element',
149 click() {
150 webContents.inspectElement(props.x, props.y);
151 },
152 }, {
153 type: 'separator',
154 });
155 }
156
157 return delUnusedElements(menuTpl);
158};
159
160export default function contextMenu(spellcheckProvider) {
161 webContents.on('context-menu', (e, props) => {
162 e.preventDefault();
163
164 let suggestions = [];
165 if (spellcheckProvider && props.misspelledWord) {
166 suggestions = spellcheckProvider.getSuggestion(props.misspelledWord);
167
168 debug('Suggestions', suggestions);
169 }
170
171 const menu = Menu.buildFromTemplate(buildMenuTpl(props, suggestions.slice(0, 5)));
172
173 menu.popup(remote.getCurrentWindow());
174 });
175}
diff --git a/src/webview/plugin.js b/src/webview/plugin.js
index 427ec75ad..72530733d 100644
--- a/src/webview/plugin.js
+++ b/src/webview/plugin.js
@@ -1,12 +1,11 @@
1import { ipcRenderer } from 'electron'; 1import { ipcRenderer } from 'electron';
2import { ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker';
3import path from 'path'; 2import path from 'path';
4 3
5import { isDevMode } from '../environment';
6import RecipeWebview from './lib/RecipeWebview'; 4import RecipeWebview from './lib/RecipeWebview';
7 5
8import Spellchecker from './spellchecker'; 6import spellchecker, { switchDict, disable as disableSpellchecker } from './spellchecker';
9import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode'; 7import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode';
8import contextMenu from './contextMenu';
10import './notifications'; 9import './notifications';
11 10
12const debug = require('debug')('Franz:Plugin'); 11const debug = require('debug')('Franz:Plugin');
@@ -34,19 +33,21 @@ ipcRenderer.on('initializeRecipe', (e, data) => {
34 } 33 }
35}); 34});
36 35
37const spellchecker = new Spellchecker(); 36// Needs to run asap to intialize dictionaries
38spellchecker.initialize(); 37(async () => {
38 const spellcheckingProvider = await spellchecker();
39 contextMenu(spellcheckingProvider);
40})();
39 41
40const contextMenuBuilder = new ContextMenuBuilder(spellchecker.handler, null, isDevMode); 42ipcRenderer.on('settings-update', async (e, data) => {
41
42new ContextMenuListener((info) => { // eslint-disable-line
43 contextMenuBuilder.showPopupMenu(info);
44});
45
46ipcRenderer.on('settings-update', (e, data) => {
47 debug('Settings update received', data); 43 debug('Settings update received', data);
48 44
49 spellchecker.toggleSpellchecker(data.enableSpellchecking); 45 if (data.enableSpellchecking) {
46 switchDict(data.spellcheckerLanguage);
47 } else {
48 disableSpellchecker();
49 }
50
50 window.franzSettings = data; 51 window.franzSettings = data;
51}); 52});
52 53
@@ -64,7 +65,7 @@ ipcRenderer.on('service-settings-update', (e, data) => {
64 } 65 }
65}); 66});
66 67
67// Needed for current implementation of electrons 'login' event 68// Needed for current implementation of electrons 'login' event 🤦‍
68ipcRenderer.on('get-service-id', (event) => { 69ipcRenderer.on('get-service-id', (event) => {
69 debug('Asking for service id', event); 70 debug('Asking for service id', event);
70 71
diff --git a/src/webview/spellchecker.js b/src/webview/spellchecker.js
index a504a4039..b0192b7ef 100644
--- a/src/webview/spellchecker.js
+++ b/src/webview/spellchecker.js
@@ -1,63 +1,92 @@
1import { SpellCheckHandler } from 'electron-spellchecker'; 1import { webFrame } from 'electron';
2import fs from 'fs';
3import path from 'path';
4import { SpellCheckerProvider } from 'electron-hunspell';
2 5
3import { isMac } from '../environment'; 6import { DICTIONARY_PATH } from '../config';
4 7
5export default class Spellchecker { 8const debug = require('debug')('Franz:spellchecker');
6 isInitialized = false;
7 handler = null;
8 initRetries = 0;
9 DOMCheckInterval = null;
10 9
11 get inputs() { 10let provider;
12 return document.querySelectorAll('input[type="text"], [contenteditable="true"], textarea'); 11let currentDict;
13 } 12let _isEnabled = false;
14 13
15 initialize() { 14async function loadDictionaries() {
16 this.handler = new SpellCheckHandler(); 15 const rawList = fs.readdirSync(DICTIONARY_PATH);
17 16
18 if (!isMac) { 17 const dicts = rawList.filter(item => !item.startsWith('.') && fs.lstatSync(path.join(DICTIONARY_PATH, item)).isDirectory());
19 this.attach(); 18
20 } else { 19 debug('Found dictionaries', dicts);
21 this.isInitialized = true; 20
22 } 21 for (let i = 0; i < dicts.length; i += 1) {
22 const fileLocation = `${DICTIONARY_PATH}/${dicts[i]}/${dicts[i]}`;
23 debug('Trying to load', fileLocation);
24 // eslint-disable-next-line
25 await provider.loadDictionary(dicts[i], `${fileLocation}.dic`, `${fileLocation}.aff`);
23 } 26 }
27}
28
29export async function switchDict(locale) {
30 try {
31 debug('Trying to load dictionary', locale);
24 32
25 attach() { 33 if (!provider.availableDictionaries.includes(locale)) {
26 let initFailed = false; 34 console.warn('Dict not available', locale);
27 35
28 if (this.initRetries > 3) {
29 console.error('Could not initialize spellchecker');
30 return; 36 return;
31 } 37 }
32 38
33 try { 39 if (!provider) {
34 this.handler.attachToInput(); 40 console.warn('SpellcheckProvider not initialized');
35 this.handler.switchLanguage(navigator.language); 41
36 } catch (err) { 42 return;
37 initFailed = true;
38 this.initRetries = +1;
39 setTimeout(() => { this.attach(); console.warn('Spellchecker init failed, trying again in 5s'); }, 5000);
40 } 43 }
41 44
42 if (!initFailed) { 45 if (locale === currentDict) {
43 this.isInitialized = true; 46 console.warn('Dictionary is already used', currentDict);
47
48 return;
44 } 49 }
45 }
46 50
47 toggleSpellchecker(enable = false) { 51 provider.switchDictionary(locale);
48 this.inputs.forEach((input) => {
49 input.setAttribute('spellcheck', enable);
50 });
51 52
52 this.intervalHandler(enable); 53 debug('Switched dictionary to', locale);
54
55 currentDict = locale;
56 _isEnabled = true;
57 } catch (err) {
58 console.error(err);
53 } 59 }
60}
54 61
55 intervalHandler(enable) { 62export default async function initialize(languageCode = 'en-us') {
56 clearInterval(this.DOMCheckInterval); 63 try {
64 provider = new SpellCheckerProvider();
65 const locale = languageCode.toLowerCase();
57 66
58 if (enable) { 67 debug('Init spellchecker');
59 this.DOMCheckInterval = setInterval(() => this.toggleSpellchecker(enable), 30000); 68 await provider.initialize();
60 } 69 await loadDictionaries();
70
71 debug('Available spellchecker dictionaries', provider.availableDictionaries);
72
73 switchDict(locale);
74
75 return provider;
76 } catch (err) {
77 console.error(err);
78 return false;
61 } 79 }
62} 80}
63 81
82export function isEnabled() {
83 return _isEnabled;
84}
85
86export function disable() {
87 if (isEnabled()) {
88 webFrame.setSpellCheckProvider(currentDict, true, { spellCheck: () => true });
89 _isEnabled = false;
90 currentDict = null;
91 }
92}