aboutsummaryrefslogtreecommitdiffstats
path: root/src
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
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')
-rw-r--r--src/components/settings/settings/EditSettingsForm.js21
-rw-r--r--src/config.js3
-rw-r--r--src/containers/settings/EditSettingsScreen.js21
-rw-r--r--src/electron/ipc-api/download.js43
-rw-r--r--src/electron/ipc-api/index.js2
-rw-r--r--src/features/spellchecker/index.js28
-rw-r--r--src/features/spellchecker/styles.js26
-rw-r--r--src/helpers/i18n-helpers.js27
-rw-r--r--src/i18n/languages.js80
-rw-r--r--src/i18n/locales/en-US.json1
-rw-r--r--src/index.js14
-rw-r--r--src/stores/AppStore.js64
-rw-r--r--src/stores/DictionaryStore.js45
-rw-r--r--src/stores/ServicesStore.js12
-rw-r--r--src/stores/SettingsStore.js11
-rw-r--r--src/stores/index.js2
-rw-r--r--src/webview/contextMenu.js175
-rw-r--r--src/webview/plugin.js29
-rw-r--r--src/webview/spellchecker.js111
19 files changed, 526 insertions, 189 deletions
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js
index 280449ead..1ec2ab614 100644
--- a/src/components/settings/settings/EditSettingsForm.js
+++ b/src/components/settings/settings/EditSettingsForm.js
@@ -168,6 +168,18 @@ export default @observer class EditSettingsForm extends Component {
168 {/* Language */} 168 {/* Language */}
169 <h2 id="language">{intl.formatMessage(messages.headlineLanguage)}</h2> 169 <h2 id="language">{intl.formatMessage(messages.headlineLanguage)}</h2>
170 <Select field={form.$('locale')} showLabel={false} /> 170 <Select field={form.$('locale')} showLabel={false} />
171 <PremiumFeatureContainer
172 condition={isSpellcheckerPremiumFeature}
173 >
174 <div>
175 <Toggle
176 field={form.$('enableSpellchecking')}
177 />
178 {form.$('enableSpellchecking').value && (
179 <Select field={form.$('spellcheckerLanguage')} />
180 )}
181 </div>
182 </PremiumFeatureContainer>
171 <a 183 <a
172 href={FRANZ_TRANSLATION} 184 href={FRANZ_TRANSLATION}
173 target="_blank" 185 target="_blank"
@@ -178,17 +190,8 @@ export default @observer class EditSettingsForm extends Component {
178 190
179 {/* Advanced */} 191 {/* Advanced */}
180 <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2> 192 <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2>
181 <PremiumFeatureContainer
182 condition={isSpellcheckerPremiumFeature}
183 >
184 <Toggle
185 field={form.$('enableSpellchecking')}
186 disabled
187 />
188 </PremiumFeatureContainer>
189 <Toggle field={form.$('enableGPUAcceleration')} /> 193 <Toggle field={form.$('enableGPUAcceleration')} />
190 <p className="settings__help">{intl.formatMessage(messages.enableGPUAccelerationInfo)}</p> 194 <p className="settings__help">{intl.formatMessage(messages.enableGPUAccelerationInfo)}</p>
191 {/* <Select field={form.$('spellcheckingLanguage')} /> */}
192 <div className="settings__settings-group"> 195 <div className="settings__settings-group">
193 <h3> 196 <h3>
194 {intl.formatMessage(messages.subheadlineCache)} 197 {intl.formatMessage(messages.subheadlineCache)}
diff --git a/src/config.js b/src/config.js
index b5702a202..6d00b8670 100644
--- a/src/config.js
+++ b/src/config.js
@@ -17,6 +17,7 @@ export const DEFAULT_APP_SETTINGS = {
17 showDisabledServices: true, 17 showDisabledServices: true,
18 showMessageBadgeWhenMuted: true, 18 showMessageBadgeWhenMuted: true,
19 enableSpellchecking: true, 19 enableSpellchecking: true,
20 spellcheckerLanguage: 'en-us',
20 darkMode: false, 21 darkMode: false,
21 locale: '', 22 locale: '',
22 fallbackLocale: 'en-US', 23 fallbackLocale: 'en-US',
@@ -35,3 +36,5 @@ export const FILE_SYSTEM_SETTINGS_TYPES = [
35]; 36];
36 37
37export const SETTINGS_PATH = path.join(app.getPath('userData'), 'config'); 38export const SETTINGS_PATH = path.join(app.getPath('userData'), 'config');
39
40export const DICTIONARY_PATH = path.join(app.getPath('userData'), 'dicts');
diff --git a/src/containers/settings/EditSettingsScreen.js b/src/containers/settings/EditSettingsScreen.js
index 7da009c8b..ea1d319d9 100644
--- a/src/containers/settings/EditSettingsScreen.js
+++ b/src/containers/settings/EditSettingsScreen.js
@@ -7,7 +7,7 @@ import AppStore from '../../stores/AppStore';
7import SettingsStore from '../../stores/SettingsStore'; 7import SettingsStore from '../../stores/SettingsStore';
8import UserStore from '../../stores/UserStore'; 8import UserStore from '../../stores/UserStore';
9import Form from '../../lib/Form'; 9import Form from '../../lib/Form';
10import { APP_LOCALES } from '../../i18n/languages'; 10import { APP_LOCALES, SPELLCHECKER_LOCALES } from '../../i18n/languages';
11import { gaPage } from '../../lib/analytics'; 11import { gaPage } from '../../lib/analytics';
12import { DEFAULT_APP_SETTINGS } from '../../config'; 12import { DEFAULT_APP_SETTINGS } from '../../config';
13import { config as spellcheckerConfig } from '../../features/spellchecker'; 13import { config as spellcheckerConfig } from '../../features/spellchecker';
@@ -60,8 +60,8 @@ const messages = defineMessages({
60 id: 'settings.app.form.enableGPUAcceleration', 60 id: 'settings.app.form.enableGPUAcceleration',
61 defaultMessage: '!!!Enable GPU Acceleration', 61 defaultMessage: '!!!Enable GPU Acceleration',
62 }, 62 },
63 spellcheckingLanguage: { 63 spellcheckerLanguage: {
64 id: 'settings.app.form.spellcheckingLanguage', 64 id: 'settings.app.form.spellcheckerLanguage',
65 defaultMessage: '!!!Language for spell checking', 65 defaultMessage: '!!!Language for spell checking',
66 }, 66 },
67 beta: { 67 beta: {
@@ -98,6 +98,7 @@ export default @inject('stores', 'actions') @observer class EditSettingsScreen e
98 darkMode: settingsData.darkMode, 98 darkMode: settingsData.darkMode,
99 showMessageBadgeWhenMuted: settingsData.showMessageBadgeWhenMuted, 99 showMessageBadgeWhenMuted: settingsData.showMessageBadgeWhenMuted,
100 enableSpellchecking: settingsData.enableSpellchecking, 100 enableSpellchecking: settingsData.enableSpellchecking,
101 spellcheckerLanguage: settingsData.spellcheckerLanguage,
101 beta: settingsData.beta, // we need this info in the main process as well 102 beta: settingsData.beta, // we need this info in the main process as well
102 locale: settingsData.locale, // we need this info in the main process as well 103 locale: settingsData.locale, // we need this info in the main process as well
103 }, 104 },
@@ -123,6 +124,14 @@ export default @inject('stores', 'actions') @observer class EditSettingsScreen e
123 }); 124 });
124 }); 125 });
125 126
127 const spellcheckingLanguages = [];
128 Object.keys(SPELLCHECKER_LOCALES).sort(Intl.Collator().compare).forEach((key) => {
129 spellcheckingLanguages.push({
130 value: key,
131 label: SPELLCHECKER_LOCALES[key],
132 });
133 });
134
126 const config = { 135 const config = {
127 fields: { 136 fields: {
128 autoLaunchOnStart: { 137 autoLaunchOnStart: {
@@ -165,6 +174,12 @@ export default @inject('stores', 'actions') @observer class EditSettingsScreen e
165 value: !this.props.stores.user.data.isPremium && spellcheckerConfig.isPremiumFeature ? false : settings.all.app.enableSpellchecking, 174 value: !this.props.stores.user.data.isPremium && spellcheckerConfig.isPremiumFeature ? false : settings.all.app.enableSpellchecking,
166 default: !this.props.stores.user.data.isPremium && spellcheckerConfig.isPremiumFeature ? false : DEFAULT_APP_SETTINGS.enableSpellchecking, 175 default: !this.props.stores.user.data.isPremium && spellcheckerConfig.isPremiumFeature ? false : DEFAULT_APP_SETTINGS.enableSpellchecking,
167 }, 176 },
177 spellcheckerLanguage: {
178 label: intl.formatMessage(messages.spellcheckerLanguage),
179 value: settings.all.app.spellcheckerLanguage,
180 options: spellcheckingLanguages,
181 default: DEFAULT_APP_SETTINGS.spellcheckerLanguage,
182 },
168 darkMode: { 183 darkMode: {
169 label: intl.formatMessage(messages.darkMode), 184 label: intl.formatMessage(messages.darkMode),
170 value: settings.all.app.darkMode, 185 value: settings.all.app.darkMode,
diff --git a/src/electron/ipc-api/download.js b/src/electron/ipc-api/download.js
new file mode 100644
index 000000000..399ca6117
--- /dev/null
+++ b/src/electron/ipc-api/download.js
@@ -0,0 +1,43 @@
1import { ipcMain, dialog } from 'electron';
2import { download } from 'electron-dl';
3import mime from 'mime-types';
4import fs from 'fs-extra';
5
6const debug = require('debug')('Franz:ipcApi:download');
7
8function decodeBase64Image(dataString) {
9 const matches = dataString.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
10
11 if (matches.length !== 3) {
12 return new Error('Invalid input string');
13 }
14
15 return new Buffer(matches[2], 'base64');
16}
17
18export default (params) => {
19 ipcMain.on('download-file', async (event, { url, content, fileOptions = {} }) => {
20 try {
21 if (!content) {
22 const dl = await download(params.mainWindow, url, {
23 saveAs: true,
24 });
25 debug('File saved to', dl.getSavePath());
26 } else {
27 const extension = mime.extension(fileOptions.mime);
28 const filename = `${fileOptions.name}.${extension}`;
29
30 dialog.showSaveDialog(params.mainWindow, {
31 defaultPath: filename,
32 }, (name) => {
33 const binaryImage = decodeBase64Image(content);
34 fs.writeFileSync(name, binaryImage, 'binary');
35
36 debug('File blob saved to', name);
37 });
38 }
39 } catch (e) {
40 console.error(e);
41 }
42 });
43};
diff --git a/src/electron/ipc-api/index.js b/src/electron/ipc-api/index.js
index 4ea6d1475..be8e0815a 100644
--- a/src/electron/ipc-api/index.js
+++ b/src/electron/ipc-api/index.js
@@ -1,9 +1,11 @@
1import autoUpdate from './autoUpdate'; 1import autoUpdate from './autoUpdate';
2import settings from './settings'; 2import settings from './settings';
3import appIndicator from './appIndicator'; 3import appIndicator from './appIndicator';
4import download from './download';
4 5
5export default (params) => { 6export default (params) => {
6 settings(params); 7 settings(params);
7 autoUpdate(params); 8 autoUpdate(params);
8 appIndicator(params); 9 appIndicator(params);
10 download(params);
9}; 11};
diff --git a/src/features/spellchecker/index.js b/src/features/spellchecker/index.js
index 8b3fb7e00..8516f816c 100644
--- a/src/features/spellchecker/index.js
+++ b/src/features/spellchecker/index.js
@@ -12,26 +12,24 @@ export default function init(stores) {
12 reaction( 12 reaction(
13 () => stores.features.features.isSpellcheckerPremiumFeature, 13 () => stores.features.features.isSpellcheckerPremiumFeature,
14 (enabled, r) => { 14 (enabled, r) => {
15 if (enabled) { 15 debug('Initializing `spellchecker` feature');
16 debug('Initializing `spellchecker` feature');
17 16
18 // Dispose the reaction to run this only once 17 // Dispose the reaction to run this only once
19 r.dispose(); 18 r.dispose();
20 19
21 const { isSpellcheckerPremiumFeature } = stores.features.features; 20 const { isSpellcheckerPremiumFeature } = stores.features.features;
22 21
23 config.isPremiumFeature = isSpellcheckerPremiumFeature !== undefined ? isSpellcheckerPremiumFeature : DEFAULT_IS_PREMIUM_FEATURE; 22 config.isPremiumFeature = isSpellcheckerPremiumFeature !== undefined ? isSpellcheckerPremiumFeature : DEFAULT_IS_PREMIUM_FEATURE;
24 23
25 autorun(() => { 24 autorun(() => {
26 if (!stores.user.data.isPremium && config.isPremiumFeature) { 25 if (!stores.user.data.isPremium && config.isPremiumFeature) {
27 debug('Override settings.spellcheckerEnabled flag to false'); 26 debug('Override settings.spellcheckerEnabled flag to false');
28 27
29 Object.assign(stores.settings.all.app, { 28 Object.assign(stores.settings.all.app, {
30 enableSpellchecker: false, 29 enableSpellchecker: false,
31 }); 30 });
32 } 31 }
33 }); 32 });
34 }
35 }, 33 },
36 ); 34 );
37} 35}
diff --git a/src/features/spellchecker/styles.js b/src/features/spellchecker/styles.js
deleted file mode 100644
index 097368d9a..000000000
--- a/src/features/spellchecker/styles.js
+++ /dev/null
@@ -1,26 +0,0 @@
1export default (theme) => {
2 console.log(theme);
3 return ({
4 container: {
5 background: theme.colorBackground,
6 position: 'absolute',
7 top: 0,
8 width: '100%',
9 display: 'flex',
10 'flex-direction': 'column',
11 'align-items': 'center',
12 'justify-content': 'center',
13 'z-index': 150,
14 },
15 headline: {
16 color: theme.colorHeadline,
17 margin: [25, 0, 40],
18 'max-width': 500,
19 'text-align': 'center',
20 'line-height': '1.3em',
21 },
22 button: {
23 margin: [40, 0, 20],
24 },
25 });
26};
diff --git a/src/helpers/i18n-helpers.js b/src/helpers/i18n-helpers.js
new file mode 100644
index 000000000..afd28cab4
--- /dev/null
+++ b/src/helpers/i18n-helpers.js
@@ -0,0 +1,27 @@
1export function getLocale({ locale, locales, defaultLocale, fallbackLocale }) {
2 let localeStr = locale;
3 if (locales[locale] === undefined) {
4 let localeFuzzy;
5 Object.keys(locales).forEach((localStr) => {
6 if (locales && Object.hasOwnProperty.call(locales, localStr)) {
7 if (locale.substring(0, 2) === localStr.substring(0, 2)) {
8 localeFuzzy = localStr;
9 }
10 }
11 });
12
13 if (localeFuzzy !== undefined) {
14 localeStr = localeFuzzy;
15 }
16 }
17
18 if (locales[localeStr] === undefined) {
19 localeStr = defaultLocale;
20 }
21
22 if (!localeStr) {
23 localeStr = fallbackLocale;
24 }
25
26 return localeStr;
27}
diff --git a/src/i18n/languages.js b/src/i18n/languages.js
index 34b369da7..b262df01e 100644
--- a/src/i18n/languages.js
+++ b/src/i18n/languages.js
@@ -27,45 +27,43 @@ export const APP_LOCALES = {
27 es: 'Español', 27 es: 'Español',
28}; 28};
29 29
30export default APP_LOCALES; 30// Hunspell compatible keys
31export const SPELLCHECKER_LOCALES = {
32 'bg-bg': 'български език',
33 'ca-es': 'Català',
34 'cs-cz': 'Čeština',
35 'da-dk': 'Dansk',
36 'de-de': 'Deutsch',
37 'el-gr': 'λληνικά (Greek)',
38 'en-us': 'English',
39 'es-es': 'Español',
40 'et-ee': 'Estonian',
41 'fa-ir': 'فارسی (Persian)',
42 'fo-fo': 'Faroese',
43 'fr-fr': 'Français',
44 'he-il': 'עברית (Hebrew)',
45 'hr-hr': 'Hrvatski jezik',
46 'hu-hu': 'Magyar',
47 'it-it': 'Italiano',
48 ko: 'Korean',
49 'lt-lt': 'Lietuvių kalba',
50 'lv-lv': 'Latviešu valoda',
51 'nb-no': 'Norsk bokmål',
52 'nl-nl': 'Nederlands',
53 'pl-pl': 'Język polski',
54 'pt-br': 'Português (Brazil)',
55 'pt-pt': 'Português',
56 'ro-ro': 'Limba română',
57 'ru-ru': 'Русский (Russian)',
58 'sk-sk': 'Slovenčina',
59 'sl-si': 'Slovenski jezik',
60 sr: 'Српски језик (Serbian)',
61 'sv-se': 'Svenska',
62 'ta-in': 'தமிழ் (Tamil)',
63 'tg-tg': 'Тоҷикӣ (Tajik)',
64 tr: 'Türkçe',
65 'uk-ua': 'Українська (Ukrainian)',
66 vi: 'Tiếng Việt',
67};
31 68
32// export const SPELLCHECKER_LOCALES = { 69export default APP_LOCALES;
33// af: 'Afrikaans',
34// sq: 'Albanian',
35// ar: 'Arabic',
36// bg: 'Bulgarian',
37// zh: 'Chinese',
38// hr: 'Croatian',
39// cs: 'Czech',
40// da: 'Danish',
41// nl: 'Dutch',
42// en: 'English',
43// 'en-AU': 'English (AU)',
44// 'en-CA': 'English (CA)',
45// 'en-GB': 'English (GB)',
46// fi: 'Finnish',
47// fr: 'French',
48// ka: 'Georgian',
49// de: 'German',
50// el: 'Greek, Modern',
51// hi: 'Hindi',
52// hu: 'Hungarian',
53// id: 'Indonesian',
54// it: 'Italian',
55// ja: 'Japanese',
56// jv: 'Javanese',
57// ko: 'Korean',
58// lt: 'Lithuanian',
59// lv: 'Latvian',
60// ms: 'Malay',
61// no: 'Norwegian',
62// pl: 'Polish',
63// pt: 'Portuguese',
64// ro: 'Romanian, Moldavian, Moldovan',
65// ru: 'Russian',
66// sk: 'Slovak',
67// es: 'Spanish',
68// sv: 'Swedish',
69// uk: 'Ukrainian',
70// vi: 'Vietnamese',
71// };
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index 8d82f98a4..864a18862 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -174,6 +174,7 @@
174 "settings.app.form.runInBackground": "Keep Franz in background when closing the window", 174 "settings.app.form.runInBackground": "Keep Franz in background when closing the window",
175 "settings.app.form.language": "Language", 175 "settings.app.form.language": "Language",
176 "settings.app.form.enableSpellchecking": "Enable spell checking", 176 "settings.app.form.enableSpellchecking": "Enable spell checking",
177 "settings.app.form.spellcheckerLanguage": "Spell checking language",
177 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration", 178 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration",
178 "settings.app.form.showDisabledServices": "Display disabled services tabs", 179 "settings.app.form.showDisabledServices": "Display disabled services tabs",
179 "settings.app.form.showMessagesBadgesWhenMuted": "Show unread message badge when notifications are disabled", 180 "settings.app.form.showMessagesBadgesWhenMuted": "Show unread message badge when notifications are disabled",
diff --git a/src/index.js b/src/index.js
index 994531dbf..663f81cc9 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,10 +1,16 @@
1import { app, BrowserWindow, shell, ipcMain } from 'electron'; 1import { app, BrowserWindow, shell, ipcMain } from 'electron';
2
2import fs from 'fs-extra'; 3import fs from 'fs-extra';
3import path from 'path'; 4import path from 'path';
4
5import windowStateKeeper from 'electron-window-state'; 5import windowStateKeeper from 'electron-window-state';
6 6
7import { isDevMode, isMac, isWindows, isLinux } from './environment'; 7import { isDevMode, isMac, isWindows, isLinux } from './environment';
8
9// DEV MODE: Save user data into FranzDev
10if (isDevMode) {
11 app.setPath('userData', path.join(app.getPath('appData'), 'FranzDev'));
12}
13/* eslint-disable import/first */
8import ipcApi from './electron/ipc-api'; 14import ipcApi from './electron/ipc-api';
9import Tray from './lib/Tray'; 15import Tray from './lib/Tray';
10import Settings from './electron/Settings'; 16import Settings from './electron/Settings';
@@ -13,6 +19,7 @@ import { appId } from './package.json'; // eslint-disable-line import/no-unresol
13import './electron/exception'; 19import './electron/exception';
14 20
15import { DEFAULT_APP_SETTINGS } from './config'; 21import { DEFAULT_APP_SETTINGS } from './config';
22/* eslint-enable import/first */
16 23
17const debug = require('debug')('Franz:App'); 24const debug = require('debug')('Franz:App');
18 25
@@ -21,11 +28,6 @@ const debug = require('debug')('Franz:App');
21let mainWindow; 28let mainWindow;
22let willQuitApp = false; 29let willQuitApp = false;
23 30
24// DEV MODE: Save user data into FranzDev
25if (isDevMode) {
26 app.setPath('userData', path.join(app.getPath('appData'), 'FranzDev'));
27}
28
29// Ensure that the recipe directory exists 31// Ensure that the recipe directory exists
30fs.emptyDirSync(path.join(app.getPath('userData'), 'recipes', 'temp')); 32fs.emptyDirSync(path.join(app.getPath('userData'), 'recipes', 'temp'));
31fs.ensureFileSync(path.join(app.getPath('userData'), 'window-state.json')); 33fs.ensureFileSync(path.join(app.getPath('userData'), 'window-state.json'));
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
index 3e8b18801..45335c488 100644
--- a/src/stores/AppStore.js
+++ b/src/stores/AppStore.js
@@ -13,6 +13,7 @@ import { isMac, isLinux, isWindows } from '../environment';
13import locales from '../i18n/translations'; 13import locales from '../i18n/translations';
14import { gaEvent } from '../lib/analytics'; 14import { gaEvent } from '../lib/analytics';
15import { onVisibilityChange } from '../helpers/visibility-helper'; 15import { onVisibilityChange } from '../helpers/visibility-helper';
16import { getLocale } from '../helpers/i18n-helpers';
16 17
17import { getServiceIdsFromPartitions, removeServicePartitionDirectory } from '../helpers/service-helpers.js'; 18import { getServiceIdsFromPartitions, removeServicePartitionDirectory } from '../helpers/service-helpers.js';
18 19
@@ -59,6 +60,8 @@ export default class AppStore extends Store {
59 60
60 @observable isFocused = true; 61 @observable isFocused = true;
61 62
63 dictionaries = [];
64
62 constructor(...args) { 65 constructor(...args) {
63 super(...args); 66 super(...args);
64 67
@@ -82,7 +85,7 @@ export default class AppStore extends Store {
82 ]); 85 ]);
83 } 86 }
84 87
85 setup() { 88 async setup() {
86 this._appStartsCounter(); 89 this._appStartsCounter();
87 // Focus the active service 90 // Focus the active service
88 window.addEventListener('focus', this.actions.service.focusActiveService); 91 window.addEventListener('focus', this.actions.service.focusActiveService);
@@ -169,11 +172,6 @@ export default class AppStore extends Store {
169 172
170 onVisibilityChange((isVisible) => { 173 onVisibilityChange((isVisible) => {
171 this.isFocused = isVisible; 174 this.isFocused = isVisible;
172 // debug('Last focus', moment().diff(this.timeLastFocusStart));
173
174 // if (isVisible) {
175 // this.timeLastFocusStart = moment();
176 // }
177 175
178 debug('Window is visible/focused', isVisible); 176 debug('Window is visible/focused', isVisible);
179 }); 177 });
@@ -322,31 +320,37 @@ export default class AppStore extends Store {
322 } 320 }
323 321
324 _getDefaultLocale() { 322 _getDefaultLocale() {
325 let locale = app.getLocale(); 323 return getLocale({
326 if (locales[locale] === undefined) { 324 locale: app.getLocale(),
327 let localeFuzzy; 325 locales,
328 Object.keys(locales).forEach((localStr) => { 326 defaultLocale,
329 if (locales && Object.hasOwnProperty.call(locales, localStr)) { 327 fallbackLocale: DEFAULT_APP_SETTINGS.fallbackLocale,
330 if (locale.substring(0, 2) === localStr.substring(0, 2)) { 328 });
331 localeFuzzy = localStr;
332 }
333 }
334 });
335
336 if (localeFuzzy !== undefined) {
337 locale = localeFuzzy;
338 }
339 }
340
341 if (locales[locale] === undefined) {
342 locale = defaultLocale;
343 }
344
345 if (!locale) {
346 locale = DEFAULT_APP_SETTINGS.fallbackLocale;
347 }
348 329
349 return locale; 330 // if (locales[locale] === undefined) {
331 // let localeFuzzy;
332 // Object.keys(locales).forEach((localStr) => {
333 // if (locales && Object.hasOwnProperty.call(locales, localStr)) {
334 // if (locale.substring(0, 2) === localStr.substring(0, 2)) {
335 // localeFuzzy = localStr;
336 // }
337 // }
338 // });
339
340 // if (localeFuzzy !== undefined) {
341 // locale = localeFuzzy;
342 // }
343 // }
344
345 // if (locales[locale] === undefined) {
346 // locale = defaultLocale;
347 // }
348
349 // if (!locale) {
350 // locale = DEFAULT_APP_SETTINGS.fallbackLocale;
351 // }
352
353 // return locale;
350 } 354 }
351 355
352 _muteAppHandler() { 356 _muteAppHandler() {
diff --git a/src/stores/DictionaryStore.js b/src/stores/DictionaryStore.js
new file mode 100644
index 000000000..b9c5f2abf
--- /dev/null
+++ b/src/stores/DictionaryStore.js
@@ -0,0 +1,45 @@
1import { observable } from 'mobx';
2import { createDownloader } from 'hunspell-dict-downloader';
3
4import Store from './lib/Store';
5
6import { DICTIONARY_PATH } from '../config';
7
8const debug = require('debug')('Franz:DictionaryStore');
9
10export default class DictionaryStore extends Store {
11 @observable available = []
12 @observable installed = []
13
14 _dictDownloader = null
15
16 constructor(...args) {
17 super(...args);
18
19 this.registerReactions([
20 this._downloadDictForUserLocale.bind(this),
21 ]);
22 }
23
24 async setup() {
25 this._dictDownloader = await createDownloader(DICTIONARY_PATH);
26 debug('dicts', this._dictDownloader);
27
28 this.available = this._dictDownloader.availableDictionaries;
29 this.installed = this._dictDownloader.installedDictionaries;
30
31 if (!this.installed.includes('en-us')) {
32 this._dictDownloader.installDictionary('en-us');
33 }
34 }
35
36 _downloadDictForUserLocale() {
37 const spellcheckerLanguage = this.stores.settings.app.spellcheckerLanguage;
38
39 debug('trying to Downloading dict for', spellcheckerLanguage);
40 if (!this.installed.includes(spellcheckerLanguage) && this.available.includes(spellcheckerLanguage) && spellcheckerLanguage !== 'en-us') {
41 debug('Downloading dict for', spellcheckerLanguage);
42 this._dictDownloader.installDictionary(spellcheckerLanguage);
43 }
44 }
45}
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index e22b343e7..8f217ea94 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -67,9 +67,14 @@ export default class ServicesStore extends Store {
67 } 67 }
68 68
69 setup() { 69 setup() {
70 // Single key reactions 70 // Single key reactions for the sake of your CPU
71 reaction( 71 reaction(
72 () => this.stores.settings.all.app.enableSpellchecking, 72 () => this.stores.settings.app.enableSpellchecking,
73 () => this._shareSettingsWithServiceProcess(),
74 );
75
76 reaction(
77 () => this.stores.settings.app.spellcheckerLanguage,
73 () => this._shareSettingsWithServiceProcess(), 78 () => this._shareSettingsWithServiceProcess(),
74 ); 79 );
75 } 80 }
@@ -590,9 +595,10 @@ export default class ServicesStore extends Store {
590 } 595 }
591 596
592 _shareSettingsWithServiceProcess() { 597 _shareSettingsWithServiceProcess() {
598 const settings = this.stores.settings.app;
593 this.actions.service.sendIPCMessageToAllServices({ 599 this.actions.service.sendIPCMessageToAllServices({
594 channel: 'settings-update', 600 channel: 'settings-update',
595 args: this.stores.settings.all.app, 601 args: settings,
596 }); 602 });
597 } 603 }
598 604
diff --git a/src/stores/SettingsStore.js b/src/stores/SettingsStore.js
index b62ac15e0..4a42ed924 100644
--- a/src/stores/SettingsStore.js
+++ b/src/stores/SettingsStore.js
@@ -5,8 +5,10 @@ import localStorage from 'mobx-localstorage';
5import Store from './lib/Store'; 5import Store from './lib/Store';
6import Request from './lib/Request'; 6import Request from './lib/Request';
7import CachedRequest from './lib/CachedRequest'; 7import CachedRequest from './lib/CachedRequest';
8import { getLocale } from '../helpers/i18n-helpers';
8 9
9import { DEFAULT_APP_SETTINGS, FILE_SYSTEM_SETTINGS_TYPES } from '../config'; 10import { DEFAULT_APP_SETTINGS, FILE_SYSTEM_SETTINGS_TYPES } from '../config';
11import { SPELLCHECKER_LOCALES } from '../i18n/languages';
10 12
11const { systemPreferences } = remote; 13const { systemPreferences } = remote;
12const debug = require('debug')('Franz:SettingsStore'); 14const debug = require('debug')('Franz:SettingsStore');
@@ -41,7 +43,6 @@ export default class SettingsStore extends Store {
41 }); 43 });
42 44
43 this.fileSystemSettingsTypes.forEach((type) => { 45 this.fileSystemSettingsTypes.forEach((type) => {
44 console.log(type);
45 ipcRenderer.send('getAppSettings', type); 46 ipcRenderer.send('getAppSettings', type);
46 }); 47 });
47 } 48 }
@@ -157,10 +158,18 @@ export default class SettingsStore extends Store {
157 158
158 // Enable dark mode once 159 // Enable dark mode once
159 if (!this.all.migration['5.0.0-beta.19-settings']) { 160 if (!this.all.migration['5.0.0-beta.19-settings']) {
161 const spellcheckerLanguage = getLocale({
162 locale: this.stores.settings.app.locale,
163 locales: SPELLCHECKER_LOCALES,
164 defaultLocale: DEFAULT_APP_SETTINGS.spellcheckerLanguage,
165 fallbackLocale: DEFAULT_APP_SETTINGS.spellcheckerLanguage,
166 });
167
160 this.actions.settings.update({ 168 this.actions.settings.update({
161 type: 'app', 169 type: 'app',
162 data: { 170 data: {
163 darkMode: systemPreferences.isDarkMode(), 171 darkMode: systemPreferences.isDarkMode(),
172 spellcheckerLanguage,
164 }, 173 },
165 }); 174 });
166 175
diff --git a/src/stores/index.js b/src/stores/index.js
index 96b844c95..f547d0a7a 100644
--- a/src/stores/index.js
+++ b/src/stores/index.js
@@ -9,6 +9,7 @@ import UIStore from './UIStore';
9import PaymentStore from './PaymentStore'; 9import PaymentStore from './PaymentStore';
10import NewsStore from './NewsStore'; 10import NewsStore from './NewsStore';
11import RequestStore from './RequestStore'; 11import RequestStore from './RequestStore';
12import DictionaryStore from './DictionaryStore';
12import GlobalErrorStore from './GlobalErrorStore'; 13import GlobalErrorStore from './GlobalErrorStore';
13 14
14export default (api, actions, router) => { 15export default (api, actions, router) => {
@@ -26,6 +27,7 @@ export default (api, actions, router) => {
26 payment: new PaymentStore(stores, api, actions), 27 payment: new PaymentStore(stores, api, actions),
27 news: new NewsStore(stores, api, actions), 28 news: new NewsStore(stores, api, actions),
28 requests: new RequestStore(stores, api, actions), 29 requests: new RequestStore(stores, api, actions),
30 dictionary: new DictionaryStore(stores, api, actions),
29 globalError: new GlobalErrorStore(stores, api, actions), 31 globalError: new GlobalErrorStore(stores, api, actions),
30 }); 32 });
31 // Initialize all stores 33 // Initialize all stores
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}