diff options
author | 2018-11-30 14:32:45 +0100 | |
---|---|---|
committer | 2018-11-30 14:32:45 +0100 | |
commit | 3d87c0e45cead95ddb6c11fc6540b82e375bdcf5 (patch) | |
tree | c91f425a39cb585242d6df5b4070de4a2141b3b4 /src | |
parent | Merge branch 'update/monetization' into develop (diff) | |
download | ferdium-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.js | 21 | ||||
-rw-r--r-- | src/config.js | 3 | ||||
-rw-r--r-- | src/containers/settings/EditSettingsScreen.js | 21 | ||||
-rw-r--r-- | src/electron/ipc-api/download.js | 43 | ||||
-rw-r--r-- | src/electron/ipc-api/index.js | 2 | ||||
-rw-r--r-- | src/features/spellchecker/index.js | 28 | ||||
-rw-r--r-- | src/features/spellchecker/styles.js | 26 | ||||
-rw-r--r-- | src/helpers/i18n-helpers.js | 27 | ||||
-rw-r--r-- | src/i18n/languages.js | 80 | ||||
-rw-r--r-- | src/i18n/locales/en-US.json | 1 | ||||
-rw-r--r-- | src/index.js | 14 | ||||
-rw-r--r-- | src/stores/AppStore.js | 64 | ||||
-rw-r--r-- | src/stores/DictionaryStore.js | 45 | ||||
-rw-r--r-- | src/stores/ServicesStore.js | 12 | ||||
-rw-r--r-- | src/stores/SettingsStore.js | 11 | ||||
-rw-r--r-- | src/stores/index.js | 2 | ||||
-rw-r--r-- | src/webview/contextMenu.js | 175 | ||||
-rw-r--r-- | src/webview/plugin.js | 29 | ||||
-rw-r--r-- | src/webview/spellchecker.js | 111 |
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 | ||
37 | export const SETTINGS_PATH = path.join(app.getPath('userData'), 'config'); | 38 | export const SETTINGS_PATH = path.join(app.getPath('userData'), 'config'); |
39 | |||
40 | export 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'; | |||
7 | import SettingsStore from '../../stores/SettingsStore'; | 7 | import SettingsStore from '../../stores/SettingsStore'; |
8 | import UserStore from '../../stores/UserStore'; | 8 | import UserStore from '../../stores/UserStore'; |
9 | import Form from '../../lib/Form'; | 9 | import Form from '../../lib/Form'; |
10 | import { APP_LOCALES } from '../../i18n/languages'; | 10 | import { APP_LOCALES, SPELLCHECKER_LOCALES } from '../../i18n/languages'; |
11 | import { gaPage } from '../../lib/analytics'; | 11 | import { gaPage } from '../../lib/analytics'; |
12 | import { DEFAULT_APP_SETTINGS } from '../../config'; | 12 | import { DEFAULT_APP_SETTINGS } from '../../config'; |
13 | import { config as spellcheckerConfig } from '../../features/spellchecker'; | 13 | import { 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 @@ | |||
1 | import { ipcMain, dialog } from 'electron'; | ||
2 | import { download } from 'electron-dl'; | ||
3 | import mime from 'mime-types'; | ||
4 | import fs from 'fs-extra'; | ||
5 | |||
6 | const debug = require('debug')('Franz:ipcApi:download'); | ||
7 | |||
8 | function 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 | |||
18 | export 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 @@ | |||
1 | import autoUpdate from './autoUpdate'; | 1 | import autoUpdate from './autoUpdate'; |
2 | import settings from './settings'; | 2 | import settings from './settings'; |
3 | import appIndicator from './appIndicator'; | 3 | import appIndicator from './appIndicator'; |
4 | import download from './download'; | ||
4 | 5 | ||
5 | export default (params) => { | 6 | export 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 @@ | |||
1 | export 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 @@ | |||
1 | export 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 | ||
30 | export default APP_LOCALES; | 30 | // Hunspell compatible keys |
31 | export 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 = { | 69 | export 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 @@ | |||
1 | import { app, BrowserWindow, shell, ipcMain } from 'electron'; | 1 | import { app, BrowserWindow, shell, ipcMain } from 'electron'; |
2 | |||
2 | import fs from 'fs-extra'; | 3 | import fs from 'fs-extra'; |
3 | import path from 'path'; | 4 | import path from 'path'; |
4 | |||
5 | import windowStateKeeper from 'electron-window-state'; | 5 | import windowStateKeeper from 'electron-window-state'; |
6 | 6 | ||
7 | import { isDevMode, isMac, isWindows, isLinux } from './environment'; | 7 | import { isDevMode, isMac, isWindows, isLinux } from './environment'; |
8 | |||
9 | // DEV MODE: Save user data into FranzDev | ||
10 | if (isDevMode) { | ||
11 | app.setPath('userData', path.join(app.getPath('appData'), 'FranzDev')); | ||
12 | } | ||
13 | /* eslint-disable import/first */ | ||
8 | import ipcApi from './electron/ipc-api'; | 14 | import ipcApi from './electron/ipc-api'; |
9 | import Tray from './lib/Tray'; | 15 | import Tray from './lib/Tray'; |
10 | import Settings from './electron/Settings'; | 16 | import Settings from './electron/Settings'; |
@@ -13,6 +19,7 @@ import { appId } from './package.json'; // eslint-disable-line import/no-unresol | |||
13 | import './electron/exception'; | 19 | import './electron/exception'; |
14 | 20 | ||
15 | import { DEFAULT_APP_SETTINGS } from './config'; | 21 | import { DEFAULT_APP_SETTINGS } from './config'; |
22 | /* eslint-enable import/first */ | ||
16 | 23 | ||
17 | const debug = require('debug')('Franz:App'); | 24 | const debug = require('debug')('Franz:App'); |
18 | 25 | ||
@@ -21,11 +28,6 @@ const debug = require('debug')('Franz:App'); | |||
21 | let mainWindow; | 28 | let mainWindow; |
22 | let willQuitApp = false; | 29 | let willQuitApp = false; |
23 | 30 | ||
24 | // DEV MODE: Save user data into FranzDev | ||
25 | if (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 |
30 | fs.emptyDirSync(path.join(app.getPath('userData'), 'recipes', 'temp')); | 32 | fs.emptyDirSync(path.join(app.getPath('userData'), 'recipes', 'temp')); |
31 | fs.ensureFileSync(path.join(app.getPath('userData'), 'window-state.json')); | 33 | fs.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'; | |||
13 | import locales from '../i18n/translations'; | 13 | import locales from '../i18n/translations'; |
14 | import { gaEvent } from '../lib/analytics'; | 14 | import { gaEvent } from '../lib/analytics'; |
15 | import { onVisibilityChange } from '../helpers/visibility-helper'; | 15 | import { onVisibilityChange } from '../helpers/visibility-helper'; |
16 | import { getLocale } from '../helpers/i18n-helpers'; | ||
16 | 17 | ||
17 | import { getServiceIdsFromPartitions, removeServicePartitionDirectory } from '../helpers/service-helpers.js'; | 18 | import { 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 @@ | |||
1 | import { observable } from 'mobx'; | ||
2 | import { createDownloader } from 'hunspell-dict-downloader'; | ||
3 | |||
4 | import Store from './lib/Store'; | ||
5 | |||
6 | import { DICTIONARY_PATH } from '../config'; | ||
7 | |||
8 | const debug = require('debug')('Franz:DictionaryStore'); | ||
9 | |||
10 | export 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'; | |||
5 | import Store from './lib/Store'; | 5 | import Store from './lib/Store'; |
6 | import Request from './lib/Request'; | 6 | import Request from './lib/Request'; |
7 | import CachedRequest from './lib/CachedRequest'; | 7 | import CachedRequest from './lib/CachedRequest'; |
8 | import { getLocale } from '../helpers/i18n-helpers'; | ||
8 | 9 | ||
9 | import { DEFAULT_APP_SETTINGS, FILE_SYSTEM_SETTINGS_TYPES } from '../config'; | 10 | import { DEFAULT_APP_SETTINGS, FILE_SYSTEM_SETTINGS_TYPES } from '../config'; |
11 | import { SPELLCHECKER_LOCALES } from '../i18n/languages'; | ||
10 | 12 | ||
11 | const { systemPreferences } = remote; | 13 | const { systemPreferences } = remote; |
12 | const debug = require('debug')('Franz:SettingsStore'); | 14 | const 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'; | |||
9 | import PaymentStore from './PaymentStore'; | 9 | import PaymentStore from './PaymentStore'; |
10 | import NewsStore from './NewsStore'; | 10 | import NewsStore from './NewsStore'; |
11 | import RequestStore from './RequestStore'; | 11 | import RequestStore from './RequestStore'; |
12 | import DictionaryStore from './DictionaryStore'; | ||
12 | import GlobalErrorStore from './GlobalErrorStore'; | 13 | import GlobalErrorStore from './GlobalErrorStore'; |
13 | 14 | ||
14 | export default (api, actions, router) => { | 15 | export 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 | |||
4 | import { clipboard, remote, ipcRenderer, shell } from 'electron'; | ||
5 | |||
6 | import { isDevMode } from '../environment'; | ||
7 | |||
8 | const debug = require('debug')('Franz:contextMenu'); | ||
9 | |||
10 | const { Menu } = remote; | ||
11 | |||
12 | // const win = remote.getCurrentWindow(); | ||
13 | const webContents = remote.getCurrentWebContents(); | ||
14 | |||
15 | function 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 | |||
24 | const 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 | |||
160 | export 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 @@ | |||
1 | import { ipcRenderer } from 'electron'; | 1 | import { ipcRenderer } from 'electron'; |
2 | import { ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker'; | ||
3 | import path from 'path'; | 2 | import path from 'path'; |
4 | 3 | ||
5 | import { isDevMode } from '../environment'; | ||
6 | import RecipeWebview from './lib/RecipeWebview'; | 4 | import RecipeWebview from './lib/RecipeWebview'; |
7 | 5 | ||
8 | import Spellchecker from './spellchecker'; | 6 | import spellchecker, { switchDict, disable as disableSpellchecker } from './spellchecker'; |
9 | import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode'; | 7 | import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode'; |
8 | import contextMenu from './contextMenu'; | ||
10 | import './notifications'; | 9 | import './notifications'; |
11 | 10 | ||
12 | const debug = require('debug')('Franz:Plugin'); | 11 | const debug = require('debug')('Franz:Plugin'); |
@@ -34,19 +33,21 @@ ipcRenderer.on('initializeRecipe', (e, data) => { | |||
34 | } | 33 | } |
35 | }); | 34 | }); |
36 | 35 | ||
37 | const spellchecker = new Spellchecker(); | 36 | // Needs to run asap to intialize dictionaries |
38 | spellchecker.initialize(); | 37 | (async () => { |
38 | const spellcheckingProvider = await spellchecker(); | ||
39 | contextMenu(spellcheckingProvider); | ||
40 | })(); | ||
39 | 41 | ||
40 | const contextMenuBuilder = new ContextMenuBuilder(spellchecker.handler, null, isDevMode); | 42 | ipcRenderer.on('settings-update', async (e, data) => { |
41 | |||
42 | new ContextMenuListener((info) => { // eslint-disable-line | ||
43 | contextMenuBuilder.showPopupMenu(info); | ||
44 | }); | ||
45 | |||
46 | ipcRenderer.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 🤦 |
68 | ipcRenderer.on('get-service-id', (event) => { | 69 | ipcRenderer.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 @@ | |||
1 | import { SpellCheckHandler } from 'electron-spellchecker'; | 1 | import { webFrame } from 'electron'; |
2 | import fs from 'fs'; | ||
3 | import path from 'path'; | ||
4 | import { SpellCheckerProvider } from 'electron-hunspell'; | ||
2 | 5 | ||
3 | import { isMac } from '../environment'; | 6 | import { DICTIONARY_PATH } from '../config'; |
4 | 7 | ||
5 | export default class Spellchecker { | 8 | const debug = require('debug')('Franz:spellchecker'); |
6 | isInitialized = false; | ||
7 | handler = null; | ||
8 | initRetries = 0; | ||
9 | DOMCheckInterval = null; | ||
10 | 9 | ||
11 | get inputs() { | 10 | let provider; |
12 | return document.querySelectorAll('input[type="text"], [contenteditable="true"], textarea'); | 11 | let currentDict; |
13 | } | 12 | let _isEnabled = false; |
14 | 13 | ||
15 | initialize() { | 14 | async 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 | |||
29 | export 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) { | 62 | export 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 | ||
82 | export function isEnabled() { | ||
83 | return _isEnabled; | ||
84 | } | ||
85 | |||
86 | export function disable() { | ||
87 | if (isEnabled()) { | ||
88 | webFrame.setSpellCheckProvider(currentDict, true, { spellCheck: () => true }); | ||
89 | _isEnabled = false; | ||
90 | currentDict = null; | ||
91 | } | ||
92 | } | ||