aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar Vijay A <vraravam@users.noreply.github.com>2021-10-18 07:33:47 +0530
committerLibravatar Vijay A <vraravam@users.noreply.github.com>2021-10-18 07:33:47 +0530
commit3e28975d32315444c4c535fda7ba2aa08a3a0bc2 (patch)
treeef7f7d9083b1d437b2cf68005cd6b831f526729c /src
parentBumped up version to: 5.6.3-beta.1 [skip ci] (diff)
parent5.6.3-nightly.37 [skip ci] (diff)
downloadferdium-app-3e28975d32315444c4c535fda7ba2aa08a3a0bc2.tar.gz
ferdium-app-3e28975d32315444c4c535fda7ba2aa08a3a0bc2.tar.zst
ferdium-app-3e28975d32315444c4c535fda7ba2aa08a3a0bc2.zip
Merge branch 'nightly' into release
Diffstat (limited to 'src')
-rw-r--r--src/I18n.js2
-rw-r--r--src/actions/index.ts2
-rw-r--r--src/actions/news.ts7
-rw-r--r--src/actions/user.ts8
-rw-r--r--src/api/NewsApi.ts18
-rw-r--r--src/api/index.ts2
-rw-r--r--src/api/server/LocalApi.ts19
-rw-r--r--src/api/server/ServerApi.js36
-rw-r--r--src/app.js1
-rw-r--r--src/components/AppUpdateInfoBar.js60
-rw-r--r--src/components/AppUpdateInfoBar.tsx55
-rw-r--r--src/components/auth/AuthLayout.js4
-rw-r--r--src/components/auth/ChangeServer.js2
-rw-r--r--src/components/auth/Import.js2
-rw-r--r--src/components/auth/Invite.js2
-rw-r--r--src/components/auth/Locked.js2
-rw-r--r--src/components/auth/Login.js2
-rw-r--r--src/components/auth/Password.js2
-rw-r--r--src/components/auth/SetupAssistant.js7
-rw-r--r--src/components/auth/Signup.js2
-rw-r--r--src/components/auth/Welcome.js2
-rw-r--r--src/components/layout/AppLayout.js61
-rw-r--r--src/components/layout/Sidebar.js6
-rw-r--r--src/components/services/content/ConnectionLostBanner.js7
-rw-r--r--src/components/services/content/ErrorHandlers/WebviewErrorHandler.js2
-rw-r--r--src/components/services/content/ErrorHandlers/styles.ts (renamed from src/components/services/content/ErrorHandlers/styles.js)2
-rw-r--r--src/components/services/content/ServiceDisabled.js2
-rw-r--r--src/components/services/content/ServiceView.js4
-rw-r--r--src/components/services/content/ServiceWebview.js38
-rw-r--r--src/components/services/content/Services.js2
-rw-r--r--src/components/services/content/WebviewCrashHandler.js2
-rw-r--r--src/components/services/tabs/TabBarSortableList.js12
-rw-r--r--src/components/services/tabs/TabItem.js61
-rw-r--r--src/components/services/tabs/Tabbar.js2
-rw-r--r--src/components/settings/SettingsLayout.js2
-rw-r--r--src/components/settings/account/AccountDashboard.js4
-rw-r--r--src/components/settings/navigation/SettingsNavigation.js2
-rw-r--r--src/components/settings/recipes/RecipeItem.js2
-rw-r--r--src/components/settings/recipes/RecipesDashboard.js8
-rw-r--r--src/components/settings/services/EditServiceForm.js3
-rw-r--r--src/components/settings/services/ServiceError.js2
-rw-r--r--src/components/settings/services/ServiceItem.js2
-rw-r--r--src/components/settings/services/ServicesDashboard.js2
-rw-r--r--src/components/settings/settings/EditSettingsForm.js20
-rw-r--r--src/components/settings/supportFerdi/SupportFerdiDashboard.js234
-rw-r--r--src/components/settings/supportFerdi/SupportFerdiDashboard.tsx232
-rw-r--r--src/components/settings/team/TeamDashboard.js2
-rw-r--r--src/components/settings/user/EditUserForm.js5
-rw-r--r--src/components/ui/AppLoader/index.tsx (renamed from src/components/ui/AppLoader/index.js)25
-rw-r--r--src/components/ui/AppLoader/styles.ts (renamed from src/components/ui/AppLoader/styles.js)0
-rw-r--r--src/components/ui/Button.js95
-rw-r--r--src/components/ui/Button.tsx73
-rw-r--r--src/components/ui/FAB.js65
-rw-r--r--src/components/ui/FAB.tsx51
-rw-r--r--src/components/ui/FullscreenLoader/index.js2
-rw-r--r--src/components/ui/FullscreenLoader/styles.ts (renamed from src/components/ui/FullscreenLoader/styles.js)0
-rw-r--r--src/components/ui/ImageUpload.tsx (renamed from src/components/ui/ImageUpload.js)26
-rw-r--r--src/components/ui/InfoBar.js2
-rw-r--r--src/components/ui/Infobox.js2
-rw-r--r--src/components/ui/Input.js2
-rw-r--r--src/components/ui/Link.js2
-rw-r--r--src/components/ui/Loader.tsx (renamed from src/components/ui/Loader.js)32
-rw-r--r--src/components/ui/Modal/index.tsx (renamed from src/components/ui/Modal/index.js)31
-rw-r--r--src/components/ui/Modal/styles.ts (renamed from src/components/ui/Modal/styles.js)2
-rw-r--r--src/components/ui/Radio.tsx (renamed from src/components/ui/Radio.js)21
-rw-r--r--src/components/ui/SearchInput.tsx (renamed from src/components/ui/SearchInput.js)40
-rw-r--r--src/components/ui/Select.js4
-rw-r--r--src/components/ui/ServiceIcon.js20
-rw-r--r--src/components/ui/Slider.js2
-rw-r--r--src/components/ui/StatusBarTargetUrl.js2
-rw-r--r--src/components/ui/Tabs/TabItem.tsx2
-rw-r--r--src/components/ui/Tabs/Tabs.js6
-rw-r--r--src/components/ui/Toggle.js2
-rw-r--r--src/components/ui/ToggleRaw.js2
-rw-r--r--src/components/ui/WebviewLoader/index.js2
-rw-r--r--src/components/ui/WebviewLoader/styles.ts (renamed from src/components/ui/WebviewLoader/styles.js)2
-rw-r--r--src/components/ui/badge/ProBadge.tsx64
-rw-r--r--src/components/ui/badge/index.tsx71
-rw-r--r--src/components/ui/button/index.tsx265
-rw-r--r--src/components/ui/effects/Appear.js47
-rw-r--r--src/components/ui/effects/Appear.tsx39
-rw-r--r--src/components/ui/error/index.tsx20
-rw-r--r--src/components/ui/error/styles.ts9
-rw-r--r--src/components/ui/headline/index.tsx70
-rw-r--r--src/components/ui/icon/index.tsx46
-rw-r--r--src/components/ui/infobox/index.tsx205
-rw-r--r--src/components/ui/input/index.tsx208
-rw-r--r--src/components/ui/input/scorePassword.ts42
-rw-r--r--src/components/ui/input/styles.ts102
-rw-r--r--src/components/ui/label/index.tsx52
-rw-r--r--src/components/ui/label/styles.ts12
-rw-r--r--src/components/ui/loader/index.tsx44
-rw-r--r--src/components/ui/select/index.tsx460
-rw-r--r--src/components/ui/textarea/index.tsx126
-rw-r--r--src/components/ui/textarea/styles.ts54
-rw-r--r--src/components/ui/toggle/index.tsx125
-rw-r--r--src/components/ui/typings/generic.ts19
-rw-r--r--src/components/ui/wrapper/index.tsx37
-rw-r--r--src/components/util/ErrorBoundary/index.js2
-rw-r--r--src/components/util/ErrorBoundary/styles.js2
-rw-r--r--src/config.ts29
-rw-r--r--src/containers/auth/AuthLayoutContainer.js2
-rw-r--r--src/containers/auth/ChangeServerScreen.js2
-rw-r--r--src/containers/auth/ImportScreen.js2
-rw-r--r--src/containers/auth/InviteScreen.js2
-rw-r--r--src/containers/auth/LockedScreen.js2
-rw-r--r--src/containers/auth/LoginScreen.js2
-rw-r--r--src/containers/auth/PasswordScreen.js2
-rw-r--r--src/containers/auth/SetupAssistantScreen.js2
-rw-r--r--src/containers/auth/SignupScreen.js2
-rw-r--r--src/containers/auth/WelcomeScreen.js2
-rw-r--r--src/containers/layout/AppLayoutContainer.js17
-rw-r--r--src/containers/settings/AccountScreen.js2
-rw-r--r--src/containers/settings/EditServiceScreen.js28
-rw-r--r--src/containers/settings/EditSettingsScreen.js30
-rw-r--r--src/containers/settings/EditUserScreen.js2
-rw-r--r--src/containers/settings/InviteScreen.js2
-rw-r--r--src/containers/settings/RecipesScreen.js24
-rw-r--r--src/containers/settings/ServicesScreen.js2
-rw-r--r--src/containers/settings/SettingsWindow.js2
-rw-r--r--src/containers/settings/SupportScreen.js2
-rw-r--r--src/containers/settings/TeamScreen.js2
-rw-r--r--src/electron-util.ts21
-rw-r--r--src/electron/exception.ts2
-rw-r--r--src/electron/ipc-api/appIndicator.ts34
-rw-r--r--src/electron/ipc-api/index.ts2
-rw-r--r--src/electron/ipc-api/localServer.ts4
-rw-r--r--src/electron/ipc-api/sessionStorage.ts6
-rw-r--r--src/enforce-macos-app-location.ts18
-rw-r--r--src/environment-remote.ts18
-rw-r--r--src/environment.ts7
-rw-r--r--src/features/basicAuth/Component.js2
-rw-r--r--src/features/basicAuth/index.ts (renamed from src/features/basicAuth/index.js)6
-rw-r--r--src/features/nightlyBuilds/Component.js4
-rw-r--r--src/features/nightlyBuilds/index.ts (renamed from src/features/nightlyBuilds/index.js)10
-rw-r--r--src/features/publishDebugInfo/Component.js5
-rw-r--r--src/features/quickSwitch/Component.js8
-rw-r--r--src/features/quickSwitch/index.ts (renamed from src/features/quickSwitch/index.js)2
-rwxr-xr-xsrc/features/settingsWS/actions.ts11
-rw-r--r--src/features/todos/components/TodosWebview.js17
-rw-r--r--src/features/todos/containers/TodosScreen.js19
-rw-r--r--src/features/todos/preload.ts (renamed from src/features/todos/preload.js)2
-rw-r--r--src/features/todos/store.js2
-rw-r--r--src/features/utils/ActionBinding.ts5
-rw-r--r--src/features/utils/FeatureStore.test.js21
-rw-r--r--src/features/webControls/components/WebControls.js7
-rw-r--r--src/features/webControls/containers/WebControlsScreen.js2
-rw-r--r--src/features/workspaces/components/CreateWorkspaceForm.js6
-rw-r--r--src/features/workspaces/components/EditWorkspaceForm.js5
-rw-r--r--src/features/workspaces/components/WorkspaceDrawer.js6
-rw-r--r--src/features/workspaces/components/WorkspaceDrawerItem.js10
-rw-r--r--src/features/workspaces/components/WorkspaceItem.tsx (renamed from src/features/workspaces/components/WorkspaceItem.js)10
-rw-r--r--src/features/workspaces/components/WorkspaceServiceListItem.tsx (renamed from src/features/workspaces/components/WorkspaceServiceListItem.js)37
-rw-r--r--src/features/workspaces/components/WorkspaceSwitchingIndicator.js4
-rw-r--r--src/features/workspaces/components/WorkspacesDashboard.js8
-rw-r--r--src/features/workspaces/containers/EditWorkspaceScreen.tsx (renamed from src/features/workspaces/containers/EditWorkspaceScreen.js)36
-rw-r--r--src/features/workspaces/containers/WorkspacesScreen.tsx (renamed from src/features/workspaces/containers/WorkspacesScreen.js)22
-rw-r--r--src/features/workspaces/store.js46
-rw-r--r--src/helpers/array-helpers.ts9
-rw-r--r--src/helpers/async-helpers.ts4
-rw-r--r--src/helpers/i18n-helpers.ts56
-rw-r--r--src/helpers/password-helpers.ts6
-rw-r--r--src/helpers/routing-helpers.ts3
-rw-r--r--src/helpers/schedule-helpers.ts53
-rw-r--r--src/helpers/url-helpers.ts5
-rw-r--r--src/helpers/userAgent-helpers.ts11
-rw-r--r--src/i18n/apply-branding.ts16
-rw-r--r--src/i18n/locales/af.json3
-rw-r--r--src/i18n/locales/ar.json3
-rw-r--r--src/i18n/locales/be.json3
-rw-r--r--src/i18n/locales/bs.json3
-rw-r--r--src/i18n/locales/ca.json3
-rw-r--r--src/i18n/locales/cs.json3
-rw-r--r--src/i18n/locales/da.json3
-rw-r--r--src/i18n/locales/de.json3
-rw-r--r--src/i18n/locales/el.json3
-rw-r--r--src/i18n/locales/en-US.json3
-rw-r--r--src/i18n/locales/es.json3
-rw-r--r--src/i18n/locales/fi.json5
-rw-r--r--src/i18n/locales/fr.json151
-rw-r--r--src/i18n/locales/ga.json3
-rw-r--r--src/i18n/locales/he.json3
-rw-r--r--src/i18n/locales/hi.json3
-rw-r--r--src/i18n/locales/hr.json3
-rw-r--r--src/i18n/locales/hu.json3
-rw-r--r--src/i18n/locales/id.json3
-rw-r--r--src/i18n/locales/it.json3
-rw-r--r--src/i18n/locales/ja.json3
-rw-r--r--src/i18n/locales/ka.json3
-rw-r--r--src/i18n/locales/ko.json3
-rw-r--r--src/i18n/locales/nl-BE.json3
-rw-r--r--src/i18n/locales/nl.json3
-rw-r--r--src/i18n/locales/no.json3
-rw-r--r--src/i18n/locales/pl.json71
-rw-r--r--src/i18n/locales/pt-BR.json3
-rw-r--r--src/i18n/locales/pt.json3
-rw-r--r--src/i18n/locales/ro.json3
-rw-r--r--src/i18n/locales/ru.json3
-rw-r--r--src/i18n/locales/sk.json3
-rw-r--r--src/i18n/locales/sl.json3
-rw-r--r--src/i18n/locales/sr.json3
-rw-r--r--src/i18n/locales/sv.json251
-rw-r--r--src/i18n/locales/te.json3
-rw-r--r--src/i18n/locales/tr.json3
-rw-r--r--src/i18n/locales/uk.json3
-rw-r--r--src/i18n/locales/vi.json3
-rw-r--r--src/i18n/locales/zh-HANT.json131
-rw-r--r--src/i18n/locales/zh.json671
-rw-r--r--src/index.ts (renamed from src/index.js)192
-rw-r--r--src/internal-server/app/Controllers/Http/RecipeController.js2
-rw-r--r--src/internal-server/app/Controllers/Http/ServiceController.js2
-rw-r--r--src/internal-server/app/Controllers/Http/UserController.js5
-rw-r--r--src/internal-server/app/Controllers/Http/WorkspaceController.js2
-rw-r--r--src/internal-server/config/app.js1
-rw-r--r--src/internal-server/config/bodyParser.js12
-rw-r--r--src/internal-server/database/migrations/1503250034279_user.js2
-rw-r--r--src/internal-server/database/migrations/1566385379883_service_schema.js2
-rw-r--r--src/internal-server/database/migrations/1566554231482_recipe_schema.js2
-rw-r--r--src/internal-server/database/migrations/1566554359294_workspace_schema.js2
-rw-r--r--src/internal-server/start.ts18
-rw-r--r--src/internal-server/start/app.js9
-rw-r--r--src/internal-server/start/routes.js1
-rw-r--r--src/internal-server/test.ts8
-rw-r--r--src/jsUtils.ts17
-rw-r--r--src/lib/Menu.js4
-rw-r--r--src/lib/Tray.js53
-rw-r--r--src/models/News.ts33
-rw-r--r--src/models/Recipe.ts98
-rw-r--r--src/models/Service.js6
-rw-r--r--src/models/User.ts3
-rw-r--r--src/models/UserAgent.js8
-rw-r--r--src/routes.js2
-rw-r--r--src/stores.types.ts367
-rw-r--r--src/stores/AppStore.js50
-rw-r--r--src/stores/NewsStore.js55
-rw-r--r--src/stores/RecipePreviewsStore.js12
-rw-r--r--src/stores/ServicesStore.js25
-rw-r--r--src/stores/SettingsStore.js6
-rw-r--r--src/stores/UIStore.ts2
-rw-r--r--src/stores/UserStore.js2
-rw-r--r--src/stores/index.ts2
-rw-r--r--src/stores/lib/Reaction.ts (renamed from src/stores/lib/Reaction.js)6
-rw-r--r--src/stores/lib/Request.js82
-rw-r--r--src/stores/lib/Store.js3
-rw-r--r--src/themes/IStyleTypes.ts7
-rw-r--r--src/themes/dark/index.ts171
-rw-r--r--src/themes/default/index.ts251
-rw-r--r--src/themes/index.ts21
-rw-r--r--src/themes/legacy/index.ts40
-rw-r--r--src/webview/badge.ts13
-rw-r--r--src/webview/contextMenu.js20
-rw-r--r--src/webview/contextMenu.ts29
-rw-r--r--src/webview/contextMenuBuilder.ts (renamed from src/webview/contextMenuBuilder.js)221
-rw-r--r--src/webview/darkmode/custom.ts (renamed from src/webview/darkmode/custom.js)0
-rw-r--r--src/webview/darkmode/ignore.js3
-rw-r--r--src/webview/darkmode/ignore.ts1
-rw-r--r--src/webview/find.ts (renamed from src/webview/find.js)10
-rw-r--r--src/webview/notifications.ts (renamed from src/webview/notifications.js)7
-rw-r--r--src/webview/screenshare.ts (renamed from src/webview/screenshare.js)8
-rw-r--r--src/webview/spellchecker.ts6
260 files changed, 5381 insertions, 2252 deletions
diff --git a/src/I18n.js b/src/I18n.js
index 6fb4cdc61..2a50050ee 100644
--- a/src/I18n.js
+++ b/src/I18n.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4import { IntlProvider } from 'react-intl'; 4import { IntlProvider } from 'react-intl';
diff --git a/src/actions/index.ts b/src/actions/index.ts
index aecdac675..983afa64b 100644
--- a/src/actions/index.ts
+++ b/src/actions/index.ts
@@ -7,7 +7,6 @@ import recipePreview from './recipePreview';
7import ui from './ui'; 7import ui from './ui';
8import app from './app'; 8import app from './app';
9import user from './user'; 9import user from './user';
10import news from './news';
11import settings from './settings'; 10import settings from './settings';
12import requests from './requests'; 11import requests from './requests';
13import workspaces from '../features/workspaces/actions'; 12import workspaces from '../features/workspaces/actions';
@@ -20,7 +19,6 @@ const actions = {
20 ui, 19 ui,
21 app, 20 app,
22 user, 21 user,
23 news,
24 settings, 22 settings,
25 requests, 23 requests,
26}; 24};
diff --git a/src/actions/news.ts b/src/actions/news.ts
deleted file mode 100644
index db106e84f..000000000
--- a/src/actions/news.ts
+++ /dev/null
@@ -1,7 +0,0 @@
1import PropTypes from 'prop-types';
2
3export default {
4 hide: {
5 newsId: PropTypes.string.isRequired,
6 },
7};
diff --git a/src/actions/user.ts b/src/actions/user.ts
index 20d27ee53..15a9216bd 100644
--- a/src/actions/user.ts
+++ b/src/actions/user.ts
@@ -25,8 +25,10 @@ export default {
25 userData: PropTypes.object.isRequired, 25 userData: PropTypes.object.isRequired,
26 }, 26 },
27 resetStatus: {}, 27 resetStatus: {},
28 importLegacyServices: PropTypes.arrayOf(PropTypes.shape({ 28 importLegacyServices: PropTypes.arrayOf(
29 recipe: PropTypes.string.isRequired, 29 PropTypes.shape({
30 })).isRequired, 30 recipe: PropTypes.string.isRequired,
31 }),
32 ).isRequired,
31 delete: {}, 33 delete: {},
32}; 34};
diff --git a/src/api/NewsApi.ts b/src/api/NewsApi.ts
deleted file mode 100644
index 31d3d903b..000000000
--- a/src/api/NewsApi.ts
+++ /dev/null
@@ -1,18 +0,0 @@
1export default class NewsApi {
2 server: any;
3
4 local: any;
5
6 constructor(server: any, local: any) {
7 this.server = server;
8 this.local = local;
9 }
10
11 latest() {
12 return this.server.getLatestNews();
13 }
14
15 hide(id: any) {
16 return this.server.hideNews(id);
17 }
18}
diff --git a/src/api/index.ts b/src/api/index.ts
index 73f613da1..59fad194a 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -4,7 +4,6 @@ import RecipePreviewsApi from './RecipePreviewsApi';
4import RecipesApi from './RecipesApi'; 4import RecipesApi from './RecipesApi';
5import UserApi from './UserApi'; 5import UserApi from './UserApi';
6import LocalApi from './LocalApi'; 6import LocalApi from './LocalApi';
7import NewsApi from './NewsApi';
8import FeaturesApi from './FeaturesApi'; 7import FeaturesApi from './FeaturesApi';
9 8
10export default (server: any, local: any) => ({ 9export default (server: any, local: any) => ({
@@ -15,5 +14,4 @@ export default (server: any, local: any) => ({
15 features: new FeaturesApi(server), 14 features: new FeaturesApi(server),
16 user: new UserApi(server, local), 15 user: new UserApi(server, local),
17 local: new LocalApi(server, local), 16 local: new LocalApi(server, local),
18 news: new NewsApi(server, local),
19}); 17});
diff --git a/src/api/server/LocalApi.ts b/src/api/server/LocalApi.ts
index 19eacf9ff..1a46aaefe 100644
--- a/src/api/server/LocalApi.ts
+++ b/src/api/server/LocalApi.ts
@@ -1,5 +1,6 @@
1import { ExecException } from 'child_process';
1import { ipcRenderer } from 'electron'; 2import { ipcRenderer } from 'electron';
2import du from 'du'; 3import fastFolderSize from 'fast-folder-size';
3 4
4import { getServicePartitionsDirectory } from '../../helpers/service-helpers'; 5import { getServicePartitionsDirectory } from '../../helpers/service-helpers';
5 6
@@ -29,13 +30,19 @@ export default class LocalApi {
29 // Services 30 // Services
30 async getAppCacheSize() { 31 async getAppCacheSize() {
31 const partitionsDir = getServicePartitionsDirectory(); 32 const partitionsDir = getServicePartitionsDirectory();
33
32 return new Promise((resolve, reject) => { 34 return new Promise((resolve, reject) => {
33 du(partitionsDir, {}, (err: Error | null, size?: number | undefined) => { 35 fastFolderSize(
34 if (err) reject(err); 36 partitionsDir,
37 (err: ExecException | null, bytes: number | undefined) => {
38 if (err) {
39 reject(err);
40 }
35 41
36 debug('LocalApi::getAppCacheSize resolves', size); 42 debug('LocalApi::getAppCacheSize resolves', bytes);
37 resolve(size); 43 resolve(bytes);
38 }); 44 },
45 );
39 }); 46 });
40 } 47 }
41 48
diff --git a/src/api/server/ServerApi.js b/src/api/server/ServerApi.js
index e321f9372..6bbf572fa 100644
--- a/src/api/server/ServerApi.js
+++ b/src/api/server/ServerApi.js
@@ -16,14 +16,12 @@ import fetch from 'electron-fetch';
16import ServiceModel from '../../models/Service'; 16import ServiceModel from '../../models/Service';
17import RecipePreviewModel from '../../models/RecipePreview'; 17import RecipePreviewModel from '../../models/RecipePreview';
18import RecipeModel from '../../models/Recipe'; 18import RecipeModel from '../../models/Recipe';
19import NewsModel from '../../models/News';
20import UserModel from '../../models/User'; 19import UserModel from '../../models/User';
21 20
22import { sleep } from '../../helpers/async-helpers'; 21import { sleep } from '../../helpers/async-helpers';
23 22
24import { SERVER_NOT_LOADED } from '../../config'; 23import { SERVER_NOT_LOADED } from '../../config';
25import { osArch, osPlatform } from '../../environment'; 24import { userDataRecipesPath, userDataPath } from '../../environment-remote';
26import { userDataRecipesPath, userDataPath, ferdiVersion } from '../../environment-remote';
27import { asarRecipesPath } from '../../helpers/asar-helpers'; 25import { asarRecipesPath } from '../../helpers/asar-helpers';
28import apiBase from '../apiBase'; 26import apiBase from '../apiBase';
29import { prepareAuthRequest, sendAuthRequest } from '../utils/auth'; 27import { prepareAuthRequest, sendAuthRequest } from '../utils/auth';
@@ -442,25 +440,6 @@ export default class ServerApi {
442 } 440 }
443 } 441 }
444 442
445 // News
446 async getLatestNews() {
447 const url = `${apiBase(
448 true,
449 )}/news?platform=${osPlatform}&arch=${osArch}&version=${ferdiVersion}`;
450 const request = await sendAuthRequest(url);
451 if (!request.ok) throw request;
452 const data = await request.json();
453 const news = this._mapNewsModels(data);
454 debug('ServerApi::getLatestNews resolves', news);
455 return news;
456 }
457
458 async hideNews(id) {
459 const request = await sendAuthRequest(`${apiBase()}/news/${id}/read`);
460 if (!request.ok) throw request;
461 debug('ServerApi::hideNews resolves', id);
462 }
463
464 // Health Check 443 // Health Check
465 async healthCheck() { 444 async healthCheck() {
466 if (apiBase() === SERVER_NOT_LOADED) { 445 if (apiBase() === SERVER_NOT_LOADED) {
@@ -587,19 +566,6 @@ export default class ServerApi {
587 .filter(recipe => recipe !== null); 566 .filter(recipe => recipe !== null);
588 } 567 }
589 568
590 _mapNewsModels(news) {
591 return news
592 .map(newsItem => {
593 try {
594 return new NewsModel(newsItem);
595 } catch (error) {
596 console.error(error);
597 return null;
598 }
599 })
600 .filter(newsItem => newsItem !== null);
601 }
602
603 _getDevRecipes() { 569 _getDevRecipes() {
604 const recipesDirectory = getDevRecipeDirectory(); 570 const recipesDirectory = getDevRecipeDirectory();
605 try { 571 try {
diff --git a/src/app.js b/src/app.js
index 8a1f99320..aea57a673 100644
--- a/src/app.js
+++ b/src/app.js
@@ -1,6 +1,5 @@
1import { webFrame } from 'electron'; 1import { webFrame } from 'electron';
2 2
3import React from 'react';
4import { render } from 'react-dom'; 3import { render } from 'react-dom';
5import { Provider } from 'mobx-react'; 4import { Provider } from 'mobx-react';
6import { syncHistoryWithStore, RouterStore } from 'mobx-react-router'; 5import { syncHistoryWithStore, RouterStore } from 'mobx-react-router';
diff --git a/src/components/AppUpdateInfoBar.js b/src/components/AppUpdateInfoBar.js
deleted file mode 100644
index 47b730bde..000000000
--- a/src/components/AppUpdateInfoBar.js
+++ /dev/null
@@ -1,60 +0,0 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { defineMessages, injectIntl } from 'react-intl';
4
5import InfoBar from './ui/InfoBar';
6import { GITHUB_FERDI_URL } from '../config';
7import { openExternalUrl } from '../helpers/url-helpers';
8
9const messages = defineMessages({
10 updateAvailable: {
11 id: 'infobar.updateAvailable',
12 defaultMessage: 'A new update for Ferdi is available.',
13 },
14 changelog: {
15 id: 'infobar.buttonChangelog',
16 defaultMessage: 'What is new?',
17 },
18 buttonInstallUpdate: {
19 id: 'infobar.buttonInstallUpdate',
20 defaultMessage: 'Restart & install update',
21 },
22});
23
24class AppUpdateInfoBar extends Component {
25 static propTypes = {
26 onInstallUpdate: PropTypes.func.isRequired,
27 onHide: PropTypes.func.isRequired,
28 };
29
30 render() {
31 const { intl } = this.props;
32 const { onInstallUpdate, onHide } = this.props;
33
34 return (
35 <InfoBar
36 type="primary"
37 ctaLabel={intl.formatMessage(messages.buttonInstallUpdate)}
38 onClick={onInstallUpdate}
39 onHide={onHide}
40 >
41 <span className="mdi mdi-information" />
42 {intl.formatMessage(messages.updateAvailable)}{' '}
43 <button
44 className="info-bar__inline-button"
45 type="button"
46 onClick={() =>
47 openExternalUrl(
48 `${GITHUB_FERDI_URL}/ferdi/blob/develop/CHANGELOG.md`,
49 true,
50 )
51 }
52 >
53 <u>{intl.formatMessage(messages.changelog)}</u>
54 </button>
55 </InfoBar>
56 );
57 }
58}
59
60export default injectIntl(AppUpdateInfoBar);
diff --git a/src/components/AppUpdateInfoBar.tsx b/src/components/AppUpdateInfoBar.tsx
new file mode 100644
index 000000000..1dd3723cc
--- /dev/null
+++ b/src/components/AppUpdateInfoBar.tsx
@@ -0,0 +1,55 @@
1import { defineMessages, useIntl } from 'react-intl';
2
3import InfoBar from './ui/InfoBar';
4import { GITHUB_FERDI_URL } from '../config';
5import { openExternalUrl } from '../helpers/url-helpers';
6
7const messages = defineMessages({
8 updateAvailable: {
9 id: 'infobar.updateAvailable',
10 defaultMessage: 'A new update for Ferdi is available.',
11 },
12 changelog: {
13 id: 'infobar.buttonChangelog',
14 defaultMessage: 'What is new?',
15 },
16 buttonInstallUpdate: {
17 id: 'infobar.buttonInstallUpdate',
18 defaultMessage: 'Restart & install update',
19 },
20});
21
22type Props = {
23 onInstallUpdate: () => void;
24 onHide: () => void;
25};
26
27const AppUpdateInfoBar = ({ onInstallUpdate, onHide }: Props) => {
28 const intl = useIntl();
29
30 return (
31 <InfoBar
32 type="primary"
33 ctaLabel={intl.formatMessage(messages.buttonInstallUpdate)}
34 onClick={onInstallUpdate}
35 onHide={onHide}
36 >
37 <span className="mdi mdi-information" />
38 {intl.formatMessage(messages.updateAvailable)}{' '}
39 <button
40 className="info-bar__inline-button"
41 type="button"
42 onClick={() =>
43 openExternalUrl(
44 `${GITHUB_FERDI_URL}/ferdi/blob/develop/CHANGELOG.md`,
45 true,
46 )
47 }
48 >
49 <u>{intl.formatMessage(messages.changelog)}</u>
50 </button>
51 </InfoBar>
52 );
53};
54
55export default AppUpdateInfoBar;
diff --git a/src/components/auth/AuthLayout.js b/src/components/auth/AuthLayout.js
index 17ac221a2..00eded728 100644
--- a/src/components/auth/AuthLayout.js
+++ b/src/components/auth/AuthLayout.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { cloneElement, Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { TitleBar } from 'electron-react-titlebar/renderer'; 4import { TitleBar } from 'electron-react-titlebar/renderer';
@@ -87,7 +87,7 @@ class AuthLayout extends Component {
87 )} 87 )}
88 <div className="auth__layout"> 88 <div className="auth__layout">
89 {/* Inject globalError into children */} 89 {/* Inject globalError into children */}
90 {React.cloneElement(children, { 90 {cloneElement(children, {
91 error, 91 error,
92 })} 92 })}
93 </div> 93 </div>
diff --git a/src/components/auth/ChangeServer.js b/src/components/auth/ChangeServer.js
index b98fb50f7..9aeebc5c8 100644
--- a/src/components/auth/ChangeServer.js
+++ b/src/components/auth/ChangeServer.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
diff --git a/src/components/auth/Import.js b/src/components/auth/Import.js
index 44cb7e791..fe2fe9872 100644
--- a/src/components/auth/Import.js
+++ b/src/components/auth/Import.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
diff --git a/src/components/auth/Invite.js b/src/components/auth/Invite.js
index df8980314..dd71c2450 100644
--- a/src/components/auth/Invite.js
+++ b/src/components/auth/Invite.js
@@ -1,4 +1,4 @@
1import React, { Component, Fragment } from 'react'; 1import { Component, Fragment } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
diff --git a/src/components/auth/Locked.js b/src/components/auth/Locked.js
index a507ba140..5b36b9fc2 100644
--- a/src/components/auth/Locked.js
+++ b/src/components/auth/Locked.js
@@ -1,5 +1,5 @@
1import { systemPreferences } from '@electron/remote'; 1import { systemPreferences } from '@electron/remote';
2import React, { Component } from 'react'; 2import { Component } from 'react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import { observer } from 'mobx-react'; 4import { observer } from 'mobx-react';
5import { defineMessages, injectIntl } from 'react-intl'; 5import { defineMessages, injectIntl } from 'react-intl';
diff --git a/src/components/auth/Login.js b/src/components/auth/Login.js
index 2f9986858..9f3f636e3 100644
--- a/src/components/auth/Login.js
+++ b/src/components/auth/Login.js
@@ -1,5 +1,5 @@
1/* eslint jsx-a11y/anchor-is-valid: 0 */ 1/* eslint jsx-a11y/anchor-is-valid: 0 */
2import React, { Component } from 'react'; 2import { Component } from 'react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import { observer, inject } from 'mobx-react'; 4import { observer, inject } from 'mobx-react';
5import { defineMessages, injectIntl } from 'react-intl'; 5import { defineMessages, injectIntl } from 'react-intl';
diff --git a/src/components/auth/Password.js b/src/components/auth/Password.js
index 3e678f638..d5bc7fa80 100644
--- a/src/components/auth/Password.js
+++ b/src/components/auth/Password.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
diff --git a/src/components/auth/SetupAssistant.js b/src/components/auth/SetupAssistant.js
index 299c40c63..1665bf837 100644
--- a/src/components/auth/SetupAssistant.js
+++ b/src/components/auth/SetupAssistant.js
@@ -1,12 +1,13 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
5import injectSheet from 'react-jss'; 5import injectSheet from 'react-jss';
6import classnames from 'classnames'; 6import classnames from 'classnames';
7 7
8import { Input, Button } from '@meetfranz/forms'; 8import { Input } from '../ui/input/index';
9import { Badge } from '@meetfranz/ui'; 9import { Button } from '../ui/button/index';
10import { Badge } from '../ui/badge';
10import Modal from '../ui/Modal'; 11import Modal from '../ui/Modal';
11import Infobox from '../ui/Infobox'; 12import Infobox from '../ui/Infobox';
12import Appear from '../ui/effects/Appear'; 13import Appear from '../ui/effects/Appear';
diff --git a/src/components/auth/Signup.js b/src/components/auth/Signup.js
index 816a49669..00625a3ac 100644
--- a/src/components/auth/Signup.js
+++ b/src/components/auth/Signup.js
@@ -1,5 +1,5 @@
1/* eslint jsx-a11y/anchor-is-valid: 0 */ 1/* eslint jsx-a11y/anchor-is-valid: 0 */
2import React, { Component } from 'react'; 2import { Component } from 'react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import { observer, inject } from 'mobx-react'; 4import { observer, inject } from 'mobx-react';
5import { defineMessages, injectIntl } from 'react-intl'; 5import { defineMessages, injectIntl } from 'react-intl';
diff --git a/src/components/auth/Welcome.js b/src/components/auth/Welcome.js
index 2d2e2ab28..809ec67a7 100644
--- a/src/components/auth/Welcome.js
+++ b/src/components/auth/Welcome.js
@@ -1,5 +1,5 @@
1/* eslint jsx-a11y/anchor-is-valid: 0 */ 1/* eslint jsx-a11y/anchor-is-valid: 0 */
2import React, { Component } from 'react'; 2import { Component } from 'react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import { observer, PropTypes as MobxPropTypes, inject } from 'mobx-react'; 4import { observer, PropTypes as MobxPropTypes, inject } from 'mobx-react';
5import { defineMessages, injectIntl } from 'react-intl'; 5import { defineMessages, injectIntl } from 'react-intl';
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js
index 0a65dcffa..4bacc547b 100644
--- a/src/components/layout/AppLayout.js
+++ b/src/components/layout/AppLayout.js
@@ -1,6 +1,6 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
5import { TitleBar } from 'electron-react-titlebar/renderer'; 5import { TitleBar } from 'electron-react-titlebar/renderer';
6import injectSheet from 'react-jss'; 6import injectSheet from 'react-jss';
@@ -20,10 +20,6 @@ import { workspaceStore } from '../../features/workspaces';
20import AppUpdateInfoBar from '../AppUpdateInfoBar'; 20import AppUpdateInfoBar from '../AppUpdateInfoBar';
21import Todos from '../../features/todos/containers/TodosScreen'; 21import Todos from '../../features/todos/containers/TodosScreen';
22 22
23function createMarkup(HTMLString) {
24 return { __html: HTMLString };
25}
26
27const messages = defineMessages({ 23const messages = defineMessages({
28 servicesUpdated: { 24 servicesUpdated: {
29 id: 'infobar.servicesUpdated', 25 id: 'infobar.servicesUpdated',
@@ -73,11 +69,9 @@ class AppLayout extends Component {
73 workspacesDrawer: PropTypes.element.isRequired, 69 workspacesDrawer: PropTypes.element.isRequired,
74 services: PropTypes.element.isRequired, 70 services: PropTypes.element.isRequired,
75 children: PropTypes.element, 71 children: PropTypes.element,
76 news: MobxPropTypes.arrayOrObservableArray.isRequired,
77 showServicesUpdatedInfoBar: PropTypes.bool.isRequired, 72 showServicesUpdatedInfoBar: PropTypes.bool.isRequired,
78 appUpdateIsDownloaded: PropTypes.bool.isRequired, 73 appUpdateIsDownloaded: PropTypes.bool.isRequired,
79 authRequestFailed: PropTypes.bool.isRequired, 74 authRequestFailed: PropTypes.bool.isRequired,
80 removeNewsItem: PropTypes.func.isRequired,
81 reloadServicesAfterUpdate: PropTypes.func.isRequired, 75 reloadServicesAfterUpdate: PropTypes.func.isRequired,
82 installAppUpdate: PropTypes.func.isRequired, 76 installAppUpdate: PropTypes.func.isRequired,
83 showRequiredRequestsError: PropTypes.bool.isRequired, 77 showRequiredRequestsError: PropTypes.bool.isRequired,
@@ -103,11 +97,9 @@ class AppLayout extends Component {
103 sidebar, 97 sidebar,
104 services, 98 services,
105 children, 99 children,
106 news,
107 showServicesUpdatedInfoBar, 100 showServicesUpdatedInfoBar,
108 appUpdateIsDownloaded, 101 appUpdateIsDownloaded,
109 authRequestFailed, 102 authRequestFailed,
110 removeNewsItem,
111 reloadServicesAfterUpdate, 103 reloadServicesAfterUpdate,
112 installAppUpdate, 104 installAppUpdate,
113 showRequiredRequestsError, 105 showRequiredRequestsError,
@@ -132,26 +124,6 @@ class AppLayout extends Component {
132 {sidebar} 124 {sidebar}
133 <div className="app__service"> 125 <div className="app__service">
134 <WorkspaceSwitchingIndicator /> 126 <WorkspaceSwitchingIndicator />
135 {news.length > 0 &&
136 news.map(item => (
137 <InfoBar
138 key={item.id}
139 position="top"
140 type={item.type}
141 sticky={item.sticky}
142 onHide={() => removeNewsItem({ newsId: item.id })}
143 >
144 <span
145 dangerouslySetInnerHTML={createMarkup(item.message)}
146 onClick={event => {
147 const { target } = event;
148 if (target && target.hasAttribute('data-is-news-cta')) {
149 removeNewsItem({ newsId: item.id });
150 }
151 }}
152 />
153 </InfoBar>
154 ))}
155 {!areRequiredRequestsSuccessful && showRequiredRequestsError && ( 127 {!areRequiredRequestsSuccessful && showRequiredRequestsError && (
156 <InfoBar 128 <InfoBar
157 type="danger" 129 type="danger"
@@ -176,19 +148,22 @@ class AppLayout extends Component {
176 {intl.formatMessage(messages.authRequestFailed)} 148 {intl.formatMessage(messages.authRequestFailed)}
177 </InfoBar> 149 </InfoBar>
178 )} 150 )}
179 {showServicesUpdatedInfoBar && this.state.shouldShowServicesUpdatedInfoBar && ( 151 {showServicesUpdatedInfoBar &&
180 <InfoBar 152 this.state.shouldShowServicesUpdatedInfoBar && (
181 type="primary" 153 <InfoBar
182 ctaLabel={intl.formatMessage(messages.buttonReloadServices)} 154 type="primary"
183 onClick={reloadServicesAfterUpdate} 155 ctaLabel={intl.formatMessage(messages.buttonReloadServices)}
184 onHide={() => { 156 onClick={reloadServicesAfterUpdate}
185 this.setState({ shouldShowServicesUpdatedInfoBar: false }); 157 onHide={() => {
186 }} 158 this.setState({
187 > 159 shouldShowServicesUpdatedInfoBar: false,
188 <span className="mdi mdi-power-plug" /> 160 });
189 {intl.formatMessage(messages.servicesUpdated)} 161 }}
190 </InfoBar> 162 >
191 )} 163 <span className="mdi mdi-power-plug" />
164 {intl.formatMessage(messages.servicesUpdated)}
165 </InfoBar>
166 )}
192 {appUpdateIsDownloaded && this.state.shouldShowAppUpdateInfoBar && ( 167 {appUpdateIsDownloaded && this.state.shouldShowAppUpdateInfoBar && (
193 <AppUpdateInfoBar 168 <AppUpdateInfoBar
194 onInstallUpdate={installAppUpdate} 169 onInstallUpdate={installAppUpdate}
diff --git a/src/components/layout/Sidebar.js b/src/components/layout/Sidebar.js
index 87233f7ca..fc33a3c58 100644
--- a/src/components/layout/Sidebar.js
+++ b/src/components/layout/Sidebar.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import ReactTooltip from 'react-tooltip'; 3import ReactTooltip from 'react-tooltip';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
@@ -177,7 +177,7 @@ class Sidebar extends Component {
177 > 177 >
178 <i className="mdi mdi-check-all" /> 178 <i className="mdi mdi-check-all" />
179 </button> 179 </button>
180 ) : null} 180 ) : null}
181 {workspaceStore.isFeatureEnabled ? ( 181 {workspaceStore.isFeatureEnabled ? (
182 <button 182 <button
183 type="button" 183 type="button"
@@ -243,7 +243,7 @@ class Sidebar extends Component {
243 this.props.stores.app.updateStatusTypes.AVAILABLE || 243 this.props.stores.app.updateStatusTypes.AVAILABLE ||
244 this.props.stores.app.updateStatus === 244 this.props.stores.app.updateStatus ===
245 this.props.stores.app.updateStatusTypes.DOWNLOADED) && ( 245 this.props.stores.app.updateStatusTypes.DOWNLOADED) && (
246 <span className="update-available">•</span> 246 <span className="update-available">•</span>
247 )} 247 )}
248 </button> 248 </button>
249 {this.state.tooltipEnabled && ( 249 {this.state.tooltipEnabled && (
diff --git a/src/components/services/content/ConnectionLostBanner.js b/src/components/services/content/ConnectionLostBanner.js
index 423edb3c7..5111a081a 100644
--- a/src/components/services/content/ConnectionLostBanner.js
+++ b/src/components/services/content/ConnectionLostBanner.js
@@ -1,13 +1,12 @@
1import React, { Component } from 'react'; 1import { createRef, Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss'; 4import injectSheet from 'react-jss';
5import { Icon } from '@meetfranz/ui';
6import { defineMessages, injectIntl } from 'react-intl'; 5import { defineMessages, injectIntl } from 'react-intl';
7 6
8import { mdiAlert } from '@mdi/js'; 7import { mdiAlert } from '@mdi/js';
9import { LIVE_API_FERDI_WEBSITE } from '../../../config'; 8import { LIVE_API_FERDI_WEBSITE } from '../../../config';
10// import { Button } from '@meetfranz/forms'; 9import { Icon } from '../../ui/icon';
11 10
12const messages = defineMessages({ 11const messages = defineMessages({
13 text: { 12 text: {
@@ -78,7 +77,7 @@ class ConnectionLostBanner extends Component {
78 reload: PropTypes.func.isRequired, 77 reload: PropTypes.func.isRequired,
79 }; 78 };
80 79
81 inputRef = React.createRef(); 80 inputRef = createRef();
82 81
83 render() { 82 render() {
84 const { classes, name, reload } = this.props; 83 const { classes, name, reload } = this.props;
diff --git a/src/components/services/content/ErrorHandlers/WebviewErrorHandler.js b/src/components/services/content/ErrorHandlers/WebviewErrorHandler.js
index b00db8c3f..5c93de80f 100644
--- a/src/components/services/content/ErrorHandlers/WebviewErrorHandler.js
+++ b/src/components/services/content/ErrorHandlers/WebviewErrorHandler.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
diff --git a/src/components/services/content/ErrorHandlers/styles.js b/src/components/services/content/ErrorHandlers/styles.ts
index 72d62f5e3..9e2509ee5 100644
--- a/src/components/services/content/ErrorHandlers/styles.js
+++ b/src/components/services/content/ErrorHandlers/styles.ts
@@ -1,4 +1,4 @@
1export default (theme) => ({ 1export default theme => ({
2 component: { 2 component: {
3 left: 0, 3 left: 0,
4 position: 'absolute', 4 position: 'absolute',
diff --git a/src/components/services/content/ServiceDisabled.js b/src/components/services/content/ServiceDisabled.js
index e59ed58bd..476b23235 100644
--- a/src/components/services/content/ServiceDisabled.js
+++ b/src/components/services/content/ServiceDisabled.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
diff --git a/src/components/services/content/ServiceView.js b/src/components/services/content/ServiceView.js
index 81401b1d2..1bc1fbf5f 100644
--- a/src/components/services/content/ServiceView.js
+++ b/src/components/services/content/ServiceView.js
@@ -1,4 +1,4 @@
1import React, { Component, Fragment } from 'react'; 1import { Component, Fragment } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { autorun } from 'mobx'; 3import { autorun } from 'mobx';
4import { observer, inject } from 'mobx-react'; 4import { observer, inject } from 'mobx-react';
@@ -123,7 +123,7 @@ class ServiceView extends Component {
123 service.isFirstLoad && 123 service.isFirstLoad &&
124 !service.isServiceAccessRestricted && ( 124 !service.isServiceAccessRestricted && (
125 <WebviewLoader loaded={false} name={service.name} /> 125 <WebviewLoader loaded={false} name={service.name} />
126 )} 126 )}
127 {service.isError && ( 127 {service.isError && (
128 <WebviewErrorHandler 128 <WebviewErrorHandler
129 name={service.recipe.name} 129 name={service.recipe.name}
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js
index d3170be53..185d41175 100644
--- a/src/components/services/content/ServiceWebview.js
+++ b/src/components/services/content/ServiceWebview.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { observable, reaction } from 'mobx'; 4import { observable, reaction } from 'mobx';
@@ -24,12 +24,10 @@ class ServiceWebview extends Component {
24 super(props); 24 super(props);
25 25
26 reaction( 26 reaction(
27 () => ( 27 () => this.webview,
28 this.webview
29 ),
30 () => { 28 () => {
31 if (this.webview && this.webview.view) { 29 if (this.webview && this.webview.view) {
32 this.webview.view.addEventListener('console-message', (e) => { 30 this.webview.view.addEventListener('console-message', e => {
33 debug('Service logged a message:', e.message); 31 debug('Service logged a message:', e.message);
34 }); 32 });
35 } 33 }
@@ -55,20 +53,26 @@ class ServiceWebview extends Component {
55 }; 53 };
56 54
57 render() { 55 render() {
58 const { 56 const { service, setWebviewReference, isSpellcheckerEnabled } = this.props;
59 service,
60 setWebviewReference,
61 isSpellcheckerEnabled,
62 } = this.props;
63 57
64 const preloadScript = join(__dirname, '..', '..', '..', 'webview', 'recipe.js'); 58 const preloadScript = join(
59 __dirname,
60 '..',
61 '..',
62 '..',
63 'webview',
64 'recipe.js',
65 );
65 66
66 return ( 67 return (
67 <ElectronWebView 68 <ElectronWebView
68 ref={(webview) => { 69 ref={webview => {
69 this.webview = webview; 70 this.webview = webview;
70 if (webview && webview.view) { 71 if (webview && webview.view) {
71 webview.view.addEventListener('did-stop-loading', this.refocusWebview); 72 webview.view.addEventListener(
73 'did-stop-loading',
74 this.refocusWebview,
75 );
72 } 76 }
73 }} 77 }}
74 autosize 78 autosize
@@ -83,10 +87,14 @@ class ServiceWebview extends Component {
83 }} 87 }}
84 onUpdateTargetUrl={this.updateTargetUrl} 88 onUpdateTargetUrl={this.updateTargetUrl}
85 useragent={service.userAgent} 89 useragent={service.userAgent}
86 disablewebsecurity={service.recipe.disablewebsecurity ? true : undefined} 90 disablewebsecurity={
91 service.recipe.disablewebsecurity ? true : undefined
92 }
87 allowpopups 93 allowpopups
88 nodeintegration 94 nodeintegration
89 webpreferences={`spellcheck=${isSpellcheckerEnabled ? 1 : 0}, contextIsolation=1, enableRemoteModule=1`} 95 webpreferences={`spellcheck=${
96 isSpellcheckerEnabled ? 1 : 0
97 }, contextIsolation=1, enableRemoteModule=1`}
90 /> 98 />
91 ); 99 );
92 } 100 }
diff --git a/src/components/services/content/Services.js b/src/components/services/content/Services.js
index fb43fb816..1edf31bd3 100644
--- a/src/components/services/content/Services.js
+++ b/src/components/services/content/Services.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes, inject } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes, inject } from 'mobx-react';
4import { Link } from 'react-router'; 4import { Link } from 'react-router';
diff --git a/src/components/services/content/WebviewCrashHandler.js b/src/components/services/content/WebviewCrashHandler.js
index a332602be..3607435b3 100644
--- a/src/components/services/content/WebviewCrashHandler.js
+++ b/src/components/services/content/WebviewCrashHandler.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
diff --git a/src/components/services/tabs/TabBarSortableList.js b/src/components/services/tabs/TabBarSortableList.js
index 1a389991d..69a12e982 100644
--- a/src/components/services/tabs/TabBarSortableList.js
+++ b/src/components/services/tabs/TabBarSortableList.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 2import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import { SortableContainer } from 'react-sortable-hoc'; 4import { SortableContainer } from 'react-sortable-hoc';
@@ -22,7 +22,7 @@ class TabBarSortableList extends Component {
22 wakeUpService: PropTypes.func.isRequired, 22 wakeUpService: PropTypes.func.isRequired,
23 showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired, 23 showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired,
24 showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired, 24 showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired,
25 } 25 };
26 26
27 render() { 27 render() {
28 const { 28 const {
@@ -43,9 +43,7 @@ class TabBarSortableList extends Component {
43 } = this.props; 43 } = this.props;
44 44
45 return ( 45 return (
46 <ul 46 <ul className="tabs">
47 className="tabs"
48 >
49 {services.map((service, index) => ( 47 {services.map((service, index) => (
50 <TabItem 48 <TabItem
51 key={service.id} 49 key={service.id}
@@ -54,7 +52,9 @@ class TabBarSortableList extends Component {
54 index={index} 52 index={index}
55 shortcutIndex={index + 1} 53 shortcutIndex={index + 1}
56 reload={() => reload({ serviceId: service.id })} 54 reload={() => reload({ serviceId: service.id })}
57 toggleNotifications={() => toggleNotifications({ serviceId: service.id })} 55 toggleNotifications={() =>
56 toggleNotifications({ serviceId: service.id })
57 }
58 toggleAudio={() => toggleAudio({ serviceId: service.id })} 58 toggleAudio={() => toggleAudio({ serviceId: service.id })}
59 toggleDarkMode={() => toggleDarkMode({ serviceId: service.id })} 59 toggleDarkMode={() => toggleDarkMode({ serviceId: service.id })}
60 deleteService={() => deleteService({ serviceId: service.id })} 60 deleteService={() => deleteService({ serviceId: service.id })}
diff --git a/src/components/services/tabs/TabItem.js b/src/components/services/tabs/TabItem.js
index 2474682df..ed8430b89 100644
--- a/src/components/services/tabs/TabItem.js
+++ b/src/components/services/tabs/TabItem.js
@@ -1,17 +1,18 @@
1import { Menu, dialog, app } from '@electron/remote'; 1import { Menu, dialog, app } from '@electron/remote';
2import React, { Component } from 'react'; 2import { Component } from 'react';
3import { defineMessages, injectIntl } from 'react-intl'; 3import { defineMessages, injectIntl } from 'react-intl';
4import PropTypes from 'prop-types'; 4import PropTypes from 'prop-types';
5import { observer } from 'mobx-react'; 5import { inject, observer } from 'mobx-react';
6import classnames from 'classnames'; 6import classnames from 'classnames';
7import { SortableElement } from 'react-sortable-hoc'; 7import { SortableElement } from 'react-sortable-hoc';
8import injectSheet from 'react-jss'; 8import injectSheet from 'react-jss';
9import ms from 'ms'; 9import ms from 'ms';
10 10
11import { observable, autorun } from 'mobx'; 11import { observable, autorun, reaction } from 'mobx';
12import ServiceModel from '../../../models/Service'; 12import ServiceModel from '../../../models/Service';
13import { cmdOrCtrlShortcutKey } from '../../../environment'; 13import { cmdOrCtrlShortcutKey } from '../../../environment';
14import globalMessages from '../../../i18n/globalMessages'; 14import globalMessages from '../../../i18n/globalMessages';
15import SettingsStore from '../../../stores/SettingsStore';
15 16
16const IS_SERVICE_DEBUGGING_ENABLED = ( 17const IS_SERVICE_DEBUGGING_ENABLED = (
17 localStorage.getItem('debug') || '' 18 localStorage.getItem('debug') || ''
@@ -112,6 +113,7 @@ const styles = {
112}; 113};
113 114
114@injectSheet(styles) 115@injectSheet(styles)
116@inject('stores')
115@observer 117@observer
116class TabItem extends Component { 118class TabItem extends Component {
117 static propTypes = { 119 static propTypes = {
@@ -131,6 +133,9 @@ class TabItem extends Component {
131 wakeUpService: PropTypes.func.isRequired, 133 wakeUpService: PropTypes.func.isRequired,
132 showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired, 134 showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired,
133 showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired, 135 showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired,
136 stores: PropTypes.shape({
137 settings: PropTypes.instanceOf(SettingsStore).isRequired,
138 }).isRequired,
134 }; 139 };
135 140
136 @observable isPolled = false; 141 @observable isPolled = false;
@@ -142,37 +147,37 @@ class TabItem extends Component {
142 this.state = { 147 this.state = {
143 showShortcutIndex: false, 148 showShortcutIndex: false,
144 }; 149 };
150
151 reaction(
152 () => this.props.stores.settings.app.enableLongPressServiceHint,
153 () => {
154 this.checkForLongPress(
155 this.props.stores.settings.app.enableLongPressServiceHint,
156 );
157 },
158 );
145 } 159 }
146 160
147 handleShowShortcutIndex = () => { 161 handleShortcutIndex = (event, showShortcutIndex = true) => {
148 this.setState({ showShortcutIndex: true }); 162 if (event.key === 'Shift') {
163 this.setState({ showShortcutIndex });
164 }
149 }; 165 };
150 166
151 checkForLongPress = () => { 167 checkForLongPress = enableLongPressServiceHint => {
152 let longpressDelay = null; 168 if (enableLongPressServiceHint) {
153 const longpressDelayDuration = 1000; 169 document.addEventListener('keydown', e => {
154 170 this.handleShortcutIndex(e);
155 document.addEventListener( 171 });
156 'keydown',
157 e => {
158 if (e.ctrlKey || e.metaKey) {
159 longpressDelay = setTimeout(
160 this.handleShowShortcutIndex,
161 longpressDelayDuration,
162 );
163 }
164 },
165 { capture: true },
166 );
167 172
168 document.addEventListener('keyup', () => { 173 document.addEventListener('keyup', e => {
169 clearTimeout(longpressDelay); 174 this.handleShortcutIndex(e, false);
170 this.setState({ showShortcutIndex: false }); 175 });
171 }); 176 }
172 }; 177 };
173 178
174 componentDidMount() { 179 componentDidMount() {
175 const { service } = this.props; 180 const { service, stores } = this.props;
176 181
177 if (IS_SERVICE_DEBUGGING_ENABLED) { 182 if (IS_SERVICE_DEBUGGING_ENABLED) {
178 autorun(() => { 183 autorun(() => {
@@ -194,7 +199,7 @@ class TabItem extends Component {
194 }); 199 });
195 } 200 }
196 201
197 this.checkForLongPress(); 202 this.checkForLongPress(stores.settings.app.enableLongPressServiceHint);
198 } 203 }
199 204
200 render() { 205 render() {
@@ -364,7 +369,7 @@ class TabItem extends Component {
364 /> 369 />
365 </> 370 </>
366 )} 371 )}
367 {shortcutIndex && this.state.showShortcutIndex && ( 372 {shortcutIndex <= 9 && this.state.showShortcutIndex && (
368 <span className="tab-item__shortcut-index">{shortcutIndex}</span> 373 <span className="tab-item__shortcut-index">{shortcutIndex}</span>
369 )} 374 )}
370 </li> 375 </li>
diff --git a/src/components/services/tabs/Tabbar.js b/src/components/services/tabs/Tabbar.js
index a77799819..4ab0e8611 100644
--- a/src/components/services/tabs/Tabbar.js
+++ b/src/components/services/tabs/Tabbar.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4 4
diff --git a/src/components/settings/SettingsLayout.js b/src/components/settings/SettingsLayout.js
index 71250bd4d..be11fdb8e 100644
--- a/src/components/settings/SettingsLayout.js
+++ b/src/components/settings/SettingsLayout.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js
index 544821e9a..6c489e64b 100644
--- a/src/components/settings/account/AccountDashboard.js
+++ b/src/components/settings/account/AccountDashboard.js
@@ -1,9 +1,9 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
5import ReactTooltip from 'react-tooltip'; 5import ReactTooltip from 'react-tooltip';
6import { H1, H2 } from '@meetfranz/ui'; 6import { H1, H2 } from '../../ui/headline';
7 7
8import Loader from '../../ui/Loader'; 8import Loader from '../../ui/Loader';
9import Button from '../../ui/Button'; 9import Button from '../../ui/Button';
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js
index 72c7faa66..18a71fdeb 100644
--- a/src/components/settings/navigation/SettingsNavigation.js
+++ b/src/components/settings/navigation/SettingsNavigation.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { defineMessages, injectIntl } from 'react-intl'; 3import { defineMessages, injectIntl } from 'react-intl';
4import { inject, observer } from 'mobx-react'; 4import { inject, observer } from 'mobx-react';
diff --git a/src/components/settings/recipes/RecipeItem.js b/src/components/settings/recipes/RecipeItem.js
index ca188aa99..1e910e6dc 100644
--- a/src/components/settings/recipes/RecipeItem.js
+++ b/src/components/settings/recipes/RecipeItem.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4 4
diff --git a/src/components/settings/recipes/RecipesDashboard.js b/src/components/settings/recipes/RecipesDashboard.js
index 44f5bc39a..bdb6f3ca0 100644
--- a/src/components/settings/recipes/RecipesDashboard.js
+++ b/src/components/settings/recipes/RecipesDashboard.js
@@ -1,12 +1,14 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
5import { Link } from 'react-router'; 5import { Link } from 'react-router';
6 6
7import { Button, Input } from '@meetfranz/forms';
8import injectSheet from 'react-jss'; 7import injectSheet from 'react-jss';
9import { H3, H2 } from '@meetfranz/ui'; 8
9import { Button } from '../../ui/button/index';
10import { Input } from '../../ui/input/index';
11import { H3, H2 } from '../../ui/headline';
10import SearchInput from '../../ui/SearchInput'; 12import SearchInput from '../../ui/SearchInput';
11import Infobox from '../../ui/Infobox'; 13import Infobox from '../../ui/Infobox';
12import RecipeItem from './RecipeItem'; 14import RecipeItem from './RecipeItem';
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js
index 22089ec45..b040e6cc2 100644
--- a/src/components/settings/services/EditServiceForm.js
+++ b/src/components/settings/services/EditServiceForm.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { Link } from 'react-router'; 4import { Link } from 'react-router';
@@ -356,6 +356,7 @@ class EditServiceForm extends Component {
356 <p className="settings__help indented__help"> 356 <p className="settings__help indented__help">
357 {intl.formatMessage(messages.isHibernationEnabledInfo)} 357 {intl.formatMessage(messages.isHibernationEnabledInfo)}
358 </p> 358 </p>
359 <Toggle field={form.$('isWakeUpEnabled')} />
359 <Toggle field={form.$('isDarkModeEnabled')} /> 360 <Toggle field={form.$('isDarkModeEnabled')} />
360 {form.$('isDarkModeEnabled').value && ( 361 {form.$('isDarkModeEnabled').value && (
361 <> 362 <>
diff --git a/src/components/settings/services/ServiceError.js b/src/components/settings/services/ServiceError.js
index d16d76db2..6dd53a102 100644
--- a/src/components/settings/services/ServiceError.js
+++ b/src/components/settings/services/ServiceError.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import { observer } from 'mobx-react'; 2import { observer } from 'mobx-react';
3import { Link } from 'react-router'; 3import { Link } from 'react-router';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
diff --git a/src/components/settings/services/ServiceItem.js b/src/components/settings/services/ServiceItem.js
index 4916e4ecc..e08b9af1f 100644
--- a/src/components/settings/services/ServiceItem.js
+++ b/src/components/settings/services/ServiceItem.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { defineMessages, injectIntl } from 'react-intl'; 3import { defineMessages, injectIntl } from 'react-intl';
4import ReactTooltip from 'react-tooltip'; 4import ReactTooltip from 'react-tooltip';
diff --git a/src/components/settings/services/ServicesDashboard.js b/src/components/settings/services/ServicesDashboard.js
index bb52db97f..aae6eb855 100644
--- a/src/components/settings/services/ServicesDashboard.js
+++ b/src/components/settings/services/ServicesDashboard.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { Link } from 'react-router'; 4import { Link } from 'react-router';
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js
index 123ab4c2d..948e9ccd5 100644
--- a/src/components/settings/settings/EditSettingsForm.js
+++ b/src/components/settings/settings/EditSettingsForm.js
@@ -1,5 +1,5 @@
1import { systemPreferences } from '@electron/remote'; 1import { systemPreferences } from '@electron/remote';
2import React, { Component } from 'react'; 2import { Component } from 'react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import { observer } from 'mobx-react'; 4import { observer } from 'mobx-react';
5import prettyBytes from 'pretty-bytes'; 5import prettyBytes from 'pretty-bytes';
@@ -12,13 +12,17 @@ import ToggleRaw from '../../ui/ToggleRaw';
12import Select from '../../ui/Select'; 12import Select from '../../ui/Select';
13import Input from '../../ui/Input'; 13import Input from '../../ui/Input';
14 14
15import { DEFAULT_APP_SETTINGS, FRANZ_TRANSLATION, GITHUB_FRANZ_URL } from '../../../config';
16import { 15import {
17 isMac, 16 DEFAULT_APP_SETTINGS,
18 isWindows, 17 FRANZ_TRANSLATION,
19 lockFerdiShortcutKey, 18 GITHUB_FRANZ_URL,
20} from '../../../environment'; 19} from '../../../config';
21import { ferdiVersion, userDataPath, userDataRecipesPath } from '../../../environment-remote'; 20import { isMac, isWindows, lockFerdiShortcutKey } from '../../../environment';
21import {
22 ferdiVersion,
23 userDataPath,
24 userDataRecipesPath,
25} from '../../../environment-remote';
22import { openPath } from '../../../helpers/url-helpers'; 26import { openPath } from '../../../helpers/url-helpers';
23import globalMessages from '../../../i18n/globalMessages'; 27import globalMessages from '../../../i18n/globalMessages';
24 28
@@ -550,6 +554,7 @@ class EditSettingsForm extends Component {
550 554
551 <Hr /> 555 <Hr />
552 <Select field={form.$('iconSize')} /> 556 <Select field={form.$('iconSize')} />
557 <Toggle field={form.$('enableLongPressServiceHint')} />
553 558
554 <Hr /> 559 <Hr />
555 560
@@ -673,6 +678,7 @@ class EditSettingsForm extends Component {
673 {this.state.activeSetttingsTab === 'advanced' && ( 678 {this.state.activeSetttingsTab === 'advanced' && (
674 <div> 679 <div>
675 <Toggle field={form.$('enableGPUAcceleration')} /> 680 <Toggle field={form.$('enableGPUAcceleration')} />
681 <Toggle field={form.$('enableGlobalHideShortcut')} />
676 <p className="settings__help indented__help"> 682 <p className="settings__help indented__help">
677 {intl.formatMessage(messages.appRestartRequired)} 683 {intl.formatMessage(messages.appRestartRequired)}
678 </p> 684 </p>
diff --git a/src/components/settings/supportFerdi/SupportFerdiDashboard.js b/src/components/settings/supportFerdi/SupportFerdiDashboard.js
deleted file mode 100644
index f24e4bd62..000000000
--- a/src/components/settings/supportFerdi/SupportFerdiDashboard.js
+++ /dev/null
@@ -1,234 +0,0 @@
1import React, { Component } from 'react';
2import { defineMessages, injectIntl } from 'react-intl';
3import { BrowserWindow } from '@electron/remote';
4import InfoBar from '../../ui/InfoBar';
5
6const messages = defineMessages({
7 headline: {
8 id: 'settings.supportFerdi.headline',
9 defaultMessage: 'About Ferdi',
10 },
11 title: {
12 id: 'settings.supportFerdi.title',
13 defaultMessage: 'Do you like Ferdi?',
14 },
15 aboutIntro: {
16 id: 'settings.supportFerdi.aboutIntro',
17 defaultMessage:
18 '<p>Ferdi is an open-source and a community-lead application.</p><p>Thanks to the people who make this possbile:</p>',
19 },
20 textListContributors: {
21 id: 'settings.supportFerdi.textListContributors',
22 defaultMessage: 'Full list of contributors',
23 },
24 textListContributorsHere: {
25 id: 'settings.supportFerdi.textListContributorsHere',
26 defaultMessage: 'here',
27 },
28 textVolunteers: {
29 id: 'settings.supportFerdi.textVolunteers',
30 defaultMessage:
31 'The development of Ferdi is done by volunteers. People who use Ferdi like you. They maintain, fix, and improve Ferdi in their spare time.',
32 },
33 textSupportWelcome: {
34 id: 'settings.supportFerdi.textSupportWelcome',
35 defaultMessage:
36 'Support is always welcome. You can find a list of the help we need',
37 },
38 textSupportWelcomeHere: {
39 id: 'settings.supportFerdi.textSupportWelcomeHere',
40 defaultMessage: 'here',
41 },
42 textExpenses: {
43 id: 'settings.supportFerdi.textExpenses',
44 defaultMessage:
45 'While volunteers do most of the work, we still need to pay for servers and certificates. As a community, we are fully transparent on funds we collect and spend - see our',
46 },
47 textOpenCollective: {
48 id: 'settings.supportFerdi.textOpenCollective',
49 defaultMessage: 'Open Collective',
50 },
51 textDonation: {
52 id: 'settings.supportFerdi.textDonation',
53 defaultMessage:
54 'If you feel like supporting Ferdi development with a donation, you can do so on both,',
55 },
56 textDonationAnd: {
57 id: 'settings.supportFerdi.textDonationAnd',
58 defaultMessage: 'and',
59 },
60 textGitHubSponsors: {
61 id: 'settings.supportFerdi.textGitHubSponsors',
62 defaultMessage: 'GitHub Sponsors',
63 },
64 openSurvey: {
65 id: 'settings.supportFerdi.openSurvey',
66 defaultMessage: 'Open survey',
67 },
68 bannerText: {
69 id: 'settings.supportFerdi.bannerText',
70 defaultMessage: 'Do you want to help us improve Ferdi?',
71 },
72});
73
74class SupportFerdiDashboard extends Component {
75 openSurveyWindow() {
76 let win = new BrowserWindow({ width: 670, height: 400 });
77 win.on('closed', () => {
78 win = null;
79 });
80
81 win.loadURL('https://rp28.typeform.com/to/E3phJT');
82 }
83
84 render() {
85 const { intl } = this.props;
86
87 const aboutIntro = intl.formatMessage(messages.aboutIntro);
88
89 return (
90 <div className="settings__main">
91 <div className="settings__header">
92 <span className="settings__header-item">
93 {intl.formatMessage(messages.headline)}
94 </span>
95 </div>
96 <div className="settings__body">
97 <h1>{intl.formatMessage(messages.title)}</h1>
98 <div>
99 <p className="settings__support-badges">
100 <a
101 href="https://github.com/getferdi/ferdi"
102 target="_blank"
103 rel="noreferrer"
104 >
105 <img
106 alt="GitHub Stars"
107 src="https://img.shields.io/github/stars/getferdi/ferdi?style=social"
108 />
109 </a>
110 <a
111 href="https://twitter.com/getferdi/"
112 target="_blank"
113 rel="noreferrer"
114 >
115 <img
116 alt="Twitter Follow"
117 src="https://img.shields.io/twitter/follow/getferdi?label=Follow&style=social"
118 />
119 </a>
120 <a
121 href="https://opencollective.com/getferdi#section-contributors"
122 target="_blank"
123 rel="noreferrer"
124 >
125 <img
126 alt="Open Collective backers"
127 src="https://img.shields.io/opencollective/backers/getferdi?logo=open-collective"
128 />
129 </a>
130 <a
131 href="https://opencollective.com/getferdi#section-contributors"
132 target="_blank"
133 rel="noreferrer"
134 >
135 <img
136 alt="Open Collective sponsors"
137 src="https://img.shields.io/opencollective/sponsors/getferdi?logo=open-collective"
138 />
139 </a>
140 </p>
141 <span dangerouslySetInnerHTML={{ __html: aboutIntro }} />
142 <br />
143 <br />
144 <p>
145 <a href="#contributors-via-opencollective">
146 <img
147 alt="GitHub contributors (non-exhaustive)"
148 width="100%"
149 src="https://opencollective.com/getferdi/contributors.svg?width=642&button=false"
150 />
151 </a>
152 </p>
153 <p>
154 {intl.formatMessage(messages.textListContributors)}
155 <a
156 href="https://github.com/getferdi/ferdi#contributors-"
157 target="_blank"
158 className="link"
159 rel="noreferrer"
160 >
161 {' '}
162 {intl.formatMessage(messages.textListContributorsHere)}
163 <i className="mdi mdi-open-in-new" />
164 </a>
165 <br />
166 <br />
167 </p>
168 <p>{intl.formatMessage(messages.textVolunteers)}</p>
169 <p>
170 {intl.formatMessage(messages.textSupportWelcome)}
171 <a
172 href="https://help.getferdi.com/general/support"
173 target="_blank"
174 className="link"
175 rel="noreferrer"
176 >
177 {' '}
178 {intl.formatMessage(messages.textSupportWelcomeHere)}
179 <i className="mdi mdi-open-in-new" />
180 </a>
181 </p>
182 <p>
183 {intl.formatMessage(messages.textExpenses)}
184 <a
185 href="https://opencollective.com/getferdi#section-budget"
186 target="_blank"
187 className="link"
188 rel="noreferrer"
189 >
190 {' '}
191 {intl.formatMessage(messages.textOpenCollective)}
192 <i className="mdi mdi-open-in-new" />
193 </a>
194 </p>
195 <p>
196 {intl.formatMessage(messages.textDonation)}
197 <a
198 href="https://opencollective.com/getferdi#section-contribute"
199 target="_blank"
200 className="link"
201 rel="noreferrer"
202 >
203 {' '}
204 {intl.formatMessage(messages.textOpenCollective)}
205 <i className="mdi mdi-open-in-new" />
206 </a>{' '}
207 {intl.formatMessage(messages.textDonationAnd)}
208 <a
209 href="https://github.com/sponsors/getferdi"
210 target="_blank"
211 className="link"
212 rel="noreferrer"
213 >
214 {' '}
215 {intl.formatMessage(messages.textGitHubSponsors)}
216 <i className="mdi mdi-open-in-new" />
217 </a>
218 </p>
219 </div>
220 </div>
221 <InfoBar
222 sticky
223 type="primary"
224 ctaLabel={intl.formatMessage(messages.openSurvey)}
225 onClick={this.openSurveyWindow}
226 >
227 {intl.formatMessage(messages.bannerText)}
228 </InfoBar>
229 </div>
230 );
231 }
232}
233
234export default injectIntl(SupportFerdiDashboard);
diff --git a/src/components/settings/supportFerdi/SupportFerdiDashboard.tsx b/src/components/settings/supportFerdi/SupportFerdiDashboard.tsx
new file mode 100644
index 000000000..505c49812
--- /dev/null
+++ b/src/components/settings/supportFerdi/SupportFerdiDashboard.tsx
@@ -0,0 +1,232 @@
1import { defineMessages, useIntl } from 'react-intl';
2import { BrowserWindow } from '@electron/remote';
3import InfoBar from '../../ui/InfoBar';
4
5const messages = defineMessages({
6 headline: {
7 id: 'settings.supportFerdi.headline',
8 defaultMessage: 'About Ferdi',
9 },
10 title: {
11 id: 'settings.supportFerdi.title',
12 defaultMessage: 'Do you like Ferdi?',
13 },
14 aboutIntro: {
15 id: 'settings.supportFerdi.aboutIntro',
16 defaultMessage:
17 '<p>Ferdi is an open-source and a community-lead application.</p><p>Thanks to the people who make this possbile:</p>',
18 },
19 textListContributors: {
20 id: 'settings.supportFerdi.textListContributors',
21 defaultMessage: 'Full list of contributors',
22 },
23 textListContributorsHere: {
24 id: 'settings.supportFerdi.textListContributorsHere',
25 defaultMessage: 'here',
26 },
27 textVolunteers: {
28 id: 'settings.supportFerdi.textVolunteers',
29 defaultMessage:
30 'The development of Ferdi is done by volunteers. People who use Ferdi like you. They maintain, fix, and improve Ferdi in their spare time.',
31 },
32 textSupportWelcome: {
33 id: 'settings.supportFerdi.textSupportWelcome',
34 defaultMessage:
35 'Support is always welcome. You can find a list of the help we need',
36 },
37 textSupportWelcomeHere: {
38 id: 'settings.supportFerdi.textSupportWelcomeHere',
39 defaultMessage: 'here',
40 },
41 textExpenses: {
42 id: 'settings.supportFerdi.textExpenses',
43 defaultMessage:
44 'While volunteers do most of the work, we still need to pay for servers and certificates. As a community, we are fully transparent on funds we collect and spend - see our',
45 },
46 textOpenCollective: {
47 id: 'settings.supportFerdi.textOpenCollective',
48 defaultMessage: 'Open Collective',
49 },
50 textDonation: {
51 id: 'settings.supportFerdi.textDonation',
52 defaultMessage:
53 'If you feel like supporting Ferdi development with a donation, you can do so on both,',
54 },
55 textDonationAnd: {
56 id: 'settings.supportFerdi.textDonationAnd',
57 defaultMessage: 'and',
58 },
59 textGitHubSponsors: {
60 id: 'settings.supportFerdi.textGitHubSponsors',
61 defaultMessage: 'GitHub Sponsors',
62 },
63 openSurvey: {
64 id: 'settings.supportFerdi.openSurvey',
65 defaultMessage: 'Open survey',
66 },
67 bannerText: {
68 id: 'settings.supportFerdi.bannerText',
69 defaultMessage: 'Do you want to help us improve Ferdi?',
70 },
71});
72
73const openSurveyWindow = () => {
74 let win = new BrowserWindow({ width: 670, height: 400 });
75 win.on('closed', () => {
76 // @ts-expect-error Type 'null' is not assignable to type 'BrowserWindow'.
77 win = null;
78 });
79
80 win.loadURL('https://rp28.typeform.com/to/E3phJT');
81};
82
83const SupportFerdiDashboard = () => {
84 const intl = useIntl();
85
86 const aboutIntro = intl.formatMessage(messages.aboutIntro);
87
88 return (
89 <div className="settings__main">
90 <div className="settings__header">
91 <span className="settings__header-item">
92 {intl.formatMessage(messages.headline)}
93 </span>
94 </div>
95 <div className="settings__body">
96 <h1>{intl.formatMessage(messages.title)}</h1>
97 <div>
98 <p className="settings__support-badges">
99 <a
100 href="https://github.com/getferdi/ferdi"
101 target="_blank"
102 rel="noreferrer"
103 >
104 <img
105 alt="GitHub Stars"
106 src="https://img.shields.io/github/stars/getferdi/ferdi?style=social"
107 />
108 </a>
109 <a
110 href="https://twitter.com/getferdi/"
111 target="_blank"
112 rel="noreferrer"
113 >
114 <img
115 alt="Twitter Follow"
116 src="https://img.shields.io/twitter/follow/getferdi?label=Follow&style=social"
117 />
118 </a>
119 <a
120 href="https://opencollective.com/getferdi#section-contributors"
121 target="_blank"
122 rel="noreferrer"
123 >
124 <img
125 alt="Open Collective backers"
126 src="https://img.shields.io/opencollective/backers/getferdi?logo=open-collective"
127 />
128 </a>
129 <a
130 href="https://opencollective.com/getferdi#section-contributors"
131 target="_blank"
132 rel="noreferrer"
133 >
134 <img
135 alt="Open Collective sponsors"
136 src="https://img.shields.io/opencollective/sponsors/getferdi?logo=open-collective"
137 />
138 </a>
139 </p>
140 <span dangerouslySetInnerHTML={{ __html: aboutIntro }} />
141 <br />
142 <br />
143 <p>
144 <a href="#contributors-via-opencollective">
145 <img
146 alt="GitHub contributors (non-exhaustive)"
147 width="100%"
148 src="https://opencollective.com/getferdi/contributors.svg?width=642&button=false"
149 />
150 </a>
151 </p>
152 <p>
153 {intl.formatMessage(messages.textListContributors)}
154 <a
155 href="https://github.com/getferdi/ferdi#contributors-"
156 target="_blank"
157 className="link"
158 rel="noreferrer"
159 >
160 {' '}
161 {intl.formatMessage(messages.textListContributorsHere)}
162 <i className="mdi mdi-open-in-new" />
163 </a>
164 <br />
165 <br />
166 </p>
167 <p>{intl.formatMessage(messages.textVolunteers)}</p>
168 <p>
169 {intl.formatMessage(messages.textSupportWelcome)}
170 <a
171 href="https://help.getferdi.com/general/support"
172 target="_blank"
173 className="link"
174 rel="noreferrer"
175 >
176 {' '}
177 {intl.formatMessage(messages.textSupportWelcomeHere)}
178 <i className="mdi mdi-open-in-new" />
179 </a>
180 </p>
181 <p>
182 {intl.formatMessage(messages.textExpenses)}
183 <a
184 href="https://opencollective.com/getferdi#section-budget"
185 target="_blank"
186 className="link"
187 rel="noreferrer"
188 >
189 {' '}
190 {intl.formatMessage(messages.textOpenCollective)}
191 <i className="mdi mdi-open-in-new" />
192 </a>
193 </p>
194 <p>
195 {intl.formatMessage(messages.textDonation)}
196 <a
197 href="https://opencollective.com/getferdi#section-contribute"
198 target="_blank"
199 className="link"
200 rel="noreferrer"
201 >
202 {' '}
203 {intl.formatMessage(messages.textOpenCollective)}
204 <i className="mdi mdi-open-in-new" />
205 </a>{' '}
206 {intl.formatMessage(messages.textDonationAnd)}
207 <a
208 href="https://github.com/sponsors/getferdi"
209 target="_blank"
210 className="link"
211 rel="noreferrer"
212 >
213 {' '}
214 {intl.formatMessage(messages.textGitHubSponsors)}
215 <i className="mdi mdi-open-in-new" />
216 </a>
217 </p>
218 </div>
219 </div>
220 <InfoBar
221 sticky
222 type="primary"
223 ctaLabel={intl.formatMessage(messages.openSurvey)}
224 onClick={openSurveyWindow}
225 >
226 {intl.formatMessage(messages.bannerText)}
227 </InfoBar>
228 </div>
229 );
230};
231
232export default SupportFerdiDashboard;
diff --git a/src/components/settings/team/TeamDashboard.js b/src/components/settings/team/TeamDashboard.js
index 06f244997..4a1a02571 100644
--- a/src/components/settings/team/TeamDashboard.js
+++ b/src/components/settings/team/TeamDashboard.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
diff --git a/src/components/settings/user/EditUserForm.js b/src/components/settings/user/EditUserForm.js
index adc107ccc..1b8a4f25a 100644
--- a/src/components/settings/user/EditUserForm.js
+++ b/src/components/settings/user/EditUserForm.js
@@ -1,12 +1,11 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
5import { Link } from 'react-router'; 5import { Link } from 'react-router';
6import { Input } from '@meetfranz/forms';
7 6
7import { Input } from '../../ui/input/index';
8import Form from '../../../lib/Form'; 8import Form from '../../../lib/Form';
9// import Input from '../../ui/Input';
10import Button from '../../ui/Button'; 9import Button from '../../ui/Button';
11import Radio from '../../ui/Radio'; 10import Radio from '../../ui/Radio';
12import Infobox from '../../ui/Infobox'; 11import Infobox from '../../ui/Infobox';
diff --git a/src/components/ui/AppLoader/index.js b/src/components/ui/AppLoader/index.tsx
index fa4a719ab..c7c290a57 100644
--- a/src/components/ui/AppLoader/index.js
+++ b/src/components/ui/AppLoader/index.tsx
@@ -1,5 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types';
3import injectSheet, { withTheme } from 'react-jss'; 2import injectSheet, { withTheme } from 'react-jss';
4import classnames from 'classnames'; 3import classnames from 'classnames';
5 4
@@ -19,15 +18,15 @@ const textList = shuffleArray([
19 'Fixing bugs', 18 'Fixing bugs',
20]); 19]);
21 20
21type Props = {
22 classes: typeof styles;
23 theme: any;
24 texts: string[];
25};
26
22@injectSheet(styles) 27@injectSheet(styles)
23@withTheme 28@withTheme
24class AppLoader extends Component { 29class AppLoader extends Component<Props> {
25 static propTypes = {
26 classes: PropTypes.object.isRequired,
27 theme: PropTypes.object.isRequired,
28 texts: PropTypes.array,
29 };
30
31 static defaultProps = { 30 static defaultProps = {
32 texts: textList, 31 texts: textList,
33 }; 32 };
@@ -36,18 +35,20 @@ class AppLoader extends Component {
36 step: 0, 35 step: 0,
37 }; 36 };
38 37
39 interval = null; 38 interval: NodeJS.Timeout | null = null;
40 39
41 componentDidMount() { 40 componentDidMount() {
42 this.interval = setInterval(() => { 41 this.interval = setInterval(() => {
43 this.setState(prevState => ({ 42 this.setState((prevState: { step: number }) => ({
44 step: prevState.step === textList.length - 1 ? 0 : prevState.step + 1, 43 step: prevState.step === textList.length - 1 ? 0 : prevState.step + 1,
45 })); 44 }));
46 }, 2500); 45 }, 2500);
47 } 46 }
48 47
49 componentWillUnmount() { 48 componentWillUnmount() {
50 clearInterval(this.interval); 49 if (this.interval) {
50 clearInterval(this.interval);
51 }
51 } 52 }
52 53
53 render() { 54 render() {
diff --git a/src/components/ui/AppLoader/styles.js b/src/components/ui/AppLoader/styles.ts
index 9891e0387..9891e0387 100644
--- a/src/components/ui/AppLoader/styles.js
+++ b/src/components/ui/AppLoader/styles.ts
diff --git a/src/components/ui/Button.js b/src/components/ui/Button.js
deleted file mode 100644
index f6c9fd3d3..000000000
--- a/src/components/ui/Button.js
+++ /dev/null
@@ -1,95 +0,0 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer, inject } from 'mobx-react';
4import Loader from 'react-loader';
5import classnames from 'classnames';
6
7@inject('stores')
8@observer
9class Button extends Component {
10 static propTypes = {
11 className: PropTypes.string,
12 label: PropTypes.string.isRequired,
13 disabled: PropTypes.bool,
14 onClick: PropTypes.func,
15 type: PropTypes.string,
16 buttonType: PropTypes.string,
17 loaded: PropTypes.bool,
18 htmlForm: PropTypes.string,
19 stores: PropTypes.shape({
20 settings: PropTypes.shape({
21 app: PropTypes.shape({
22 accentColor: PropTypes.string.isRequired,
23 }).isRequired,
24 }).isRequired,
25 }).isRequired,
26 };
27
28 static defaultProps = {
29 className: null,
30 disabled: false,
31 onClick: () => {},
32 type: 'button',
33 buttonType: '',
34 loaded: true,
35 htmlForm: '',
36 };
37
38 element = null;
39
40 render() {
41 const {
42 label,
43 className,
44 disabled,
45 onClick,
46 type,
47 buttonType,
48 loaded,
49 htmlForm,
50 } = this.props;
51
52 const buttonProps = {
53 className: classnames({
54 'franz-form__button': true,
55 [`franz-form__button--${buttonType}`]: buttonType,
56 [`${className}`]: className,
57 }),
58 type,
59 };
60
61 if (disabled) {
62 buttonProps.disabled = true;
63 }
64
65 if (onClick) {
66 buttonProps.onClick = onClick;
67 }
68
69 if (htmlForm) {
70 buttonProps.form = htmlForm;
71 }
72
73 return (
74 // disabling rule as button has type defined in `buttonProps`
75 /* eslint-disable react/button-has-type */
76 <button {...buttonProps}>
77 <Loader
78 loaded={loaded}
79 lines={10}
80 scale={0.4}
81 color={
82 buttonType !== 'secondary'
83 ? '#FFF'
84 : this.props.stores.settings.app.accentColor
85 }
86 component="span"
87 />
88 {label}
89 </button>
90 /* eslint-enable react/button-has-type */
91 );
92 }
93}
94
95export default Button;
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
new file mode 100644
index 000000000..aac080fda
--- /dev/null
+++ b/src/components/ui/Button.tsx
@@ -0,0 +1,73 @@
1import { Component } from 'react';
2import { observer, inject } from 'mobx-react';
3import Loader from 'react-loader';
4import classnames from 'classnames';
5import { FerdiStores } from '../../stores.types';
6
7type Props = {
8 className: string;
9 label: string;
10 disabled: boolean;
11 onClick: () => void;
12 type: 'button' | 'submit' | 'reset';
13 buttonType: string;
14 loaded: boolean;
15 htmlForm: string;
16 stores: FerdiStores;
17};
18
19@inject('stores')
20@observer
21class Button extends Component<Props> {
22 static defaultProps = {
23 className: null,
24 disabled: false,
25 onClick: () => {},
26 type: 'button',
27 buttonType: '',
28 loaded: true,
29 htmlForm: '',
30 };
31
32 render() {
33 const {
34 label,
35 className,
36 disabled,
37 onClick,
38 type,
39 buttonType,
40 loaded,
41 htmlForm,
42 } = this.props;
43
44 return (
45 <button
46 className={classnames({
47 'franz-form__button': true,
48 [`franz-form__button--${buttonType}`]: buttonType,
49 [`${className}`]: className,
50 })}
51 disabled={disabled}
52 type={type}
53 onClick={onClick}
54 form={htmlForm}
55 >
56 <Loader
57 loaded={loaded}
58 lines={10}
59 scale={0.4}
60 color={
61 buttonType !== 'secondary'
62 ? '#FFF'
63 : this.props.stores.settings.app.accentColor
64 }
65 component="span"
66 />
67 {label}
68 </button>
69 );
70 }
71}
72
73export default Button;
diff --git a/src/components/ui/FAB.js b/src/components/ui/FAB.js
deleted file mode 100644
index a3aa06bc9..000000000
--- a/src/components/ui/FAB.js
+++ /dev/null
@@ -1,65 +0,0 @@
1/**
2 * Floating Action Button (FAB)
3 */
4import React, { Component } from 'react';
5import PropTypes from 'prop-types';
6import { observer } from 'mobx-react';
7import classnames from 'classnames';
8
9import { oneOrManyChildElements } from '../../prop-types';
10
11@observer
12class Button extends Component {
13 static propTypes = {
14 className: PropTypes.string,
15 disabled: PropTypes.bool,
16 onClick: PropTypes.func,
17 type: PropTypes.string,
18 children: oneOrManyChildElements.isRequired,
19 htmlForm: PropTypes.string,
20 };
21
22 static defaultProps = {
23 className: null,
24 disabled: false,
25 onClick: () => {},
26 type: 'button',
27 htmlForm: '',
28 };
29
30 element = null;
31
32 render() {
33 const { className, disabled, onClick, type, children, htmlForm } =
34 this.props;
35
36 const buttonProps = {
37 className: classnames({
38 ferdi__fab: true,
39 [`${className}`]: className,
40 }),
41 type,
42 };
43
44 if (disabled) {
45 buttonProps.disabled = true;
46 }
47
48 if (onClick) {
49 buttonProps.onClick = onClick;
50 }
51
52 if (htmlForm) {
53 buttonProps.form = htmlForm;
54 }
55
56 return (
57 // disabling rule as button has type defined in `buttonProps`
58 <button {...buttonProps} type="button">
59 {children}
60 </button>
61 );
62 }
63}
64
65export default Button;
diff --git a/src/components/ui/FAB.tsx b/src/components/ui/FAB.tsx
new file mode 100644
index 000000000..583c9d556
--- /dev/null
+++ b/src/components/ui/FAB.tsx
@@ -0,0 +1,51 @@
1/**
2 * Floating Action Button (FAB)
3 */
4import { Component, ReactChildren } from 'react';
5import { observer } from 'mobx-react';
6import classnames from 'classnames';
7
8type Props = {
9 className: string;
10 disabled: boolean;
11 onClick: () => void;
12 type: string;
13 children: ReactChildren;
14 htmlForm: string;
15};
16
17@observer
18class Button extends Component<Props> {
19 static defaultProps = {
20 disabled: false,
21 onClick: () => {},
22 type: 'button',
23 htmlForm: '',
24 };
25
26 element = null;
27
28 render() {
29 const { className, disabled, onClick, type, children, htmlForm } =
30 this.props;
31
32 const buttonProps = {
33 className: classnames({
34 ferdi__fab: true,
35 [`${className}`]: className,
36 }),
37 type,
38 disabled,
39 onClick,
40 form: htmlForm,
41 };
42
43 return (
44 <button {...buttonProps} type="button">
45 {children}
46 </button>
47 );
48 }
49}
50
51export default Button;
diff --git a/src/components/ui/FullscreenLoader/index.js b/src/components/ui/FullscreenLoader/index.js
index ab5e2f365..f5943f3f3 100644
--- a/src/components/ui/FullscreenLoader/index.js
+++ b/src/components/ui/FullscreenLoader/index.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import injectSheet, { withTheme } from 'react-jss'; 4import injectSheet, { withTheme } from 'react-jss';
diff --git a/src/components/ui/FullscreenLoader/styles.js b/src/components/ui/FullscreenLoader/styles.ts
index 64d24e4ce..64d24e4ce 100644
--- a/src/components/ui/FullscreenLoader/styles.js
+++ b/src/components/ui/FullscreenLoader/styles.ts
diff --git a/src/components/ui/ImageUpload.js b/src/components/ui/ImageUpload.tsx
index 49aff389b..4b25be502 100644
--- a/src/components/ui/ImageUpload.js
+++ b/src/components/ui/ImageUpload.tsx
@@ -1,23 +1,21 @@
1import React, { Component, Fragment } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 2import { observer } from 'mobx-react';
4import { Field } from 'mobx-react-form'; 3import { Field } from 'mobx-react-form';
5import classnames from 'classnames'; 4import classnames from 'classnames';
6import Dropzone from 'react-dropzone'; 5import Dropzone, { DropzoneRef } from 'react-dropzone';
7import { isWindows } from '../../environment'; 6import { isWindows } from '../../environment';
8 7
9@observer 8type Props = {
10class ImageUpload extends Component { 9 field: typeof Field;
11 static propTypes = { 10 className: string;
12 field: PropTypes.instanceOf(Field).isRequired, 11 multiple: boolean;
13 className: PropTypes.string, 12 textDelete: string;
14 multiple: PropTypes.bool, 13 textUpload: string;
15 textDelete: PropTypes.string.isRequired, 14};
16 textUpload: PropTypes.string.isRequired,
17 };
18 15
16@observer
17class ImageUpload extends Component<Props> {
19 static defaultProps = { 18 static defaultProps = {
20 className: null,
21 multiple: false, 19 multiple: false,
22 }; 20 };
23 21
@@ -25,7 +23,7 @@ class ImageUpload extends Component {
25 path: null, 23 path: null,
26 }; 24 };
27 25
28 dropzoneRef = null; 26 dropzoneRef: DropzoneRef | null = null;
29 27
30 onDrop(acceptedFiles) { 28 onDrop(acceptedFiles) {
31 const { field } = this.props; 29 const { field } = this.props;
diff --git a/src/components/ui/InfoBar.js b/src/components/ui/InfoBar.js
index dc6be10da..3311a949f 100644
--- a/src/components/ui/InfoBar.js
+++ b/src/components/ui/InfoBar.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import classnames from 'classnames'; 4import classnames from 'classnames';
diff --git a/src/components/ui/Infobox.js b/src/components/ui/Infobox.js
index 9e34bf110..b88b01bd8 100644
--- a/src/components/ui/Infobox.js
+++ b/src/components/ui/Infobox.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import classnames from 'classnames'; 4import classnames from 'classnames';
diff --git a/src/components/ui/Input.js b/src/components/ui/Input.js
index 43fab10ee..8d37d7a05 100644
--- a/src/components/ui/Input.js
+++ b/src/components/ui/Input.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { Field } from 'mobx-react-form'; 4import { Field } from 'mobx-react-form';
diff --git a/src/components/ui/Link.js b/src/components/ui/Link.js
index 94db3f842..40766c984 100644
--- a/src/components/ui/Link.js
+++ b/src/components/ui/Link.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4import { RouterStore } from 'mobx-react-router'; 4import { RouterStore } from 'mobx-react-router';
diff --git a/src/components/ui/Loader.js b/src/components/ui/Loader.tsx
index 46c1390bf..1173c11e7 100644
--- a/src/components/ui/Loader.js
+++ b/src/components/ui/Loader.tsx
@@ -1,31 +1,22 @@
1import React, { Component } from 'react'; 1import { Component, ReactChildren } from 'react';
2import { observer, inject } from 'mobx-react'; 2import { observer, inject } from 'mobx-react';
3import PropTypes from 'prop-types';
4import Loader from 'react-loader'; 3import Loader from 'react-loader';
5 4
6import { oneOrManyChildElements } from '../../prop-types'; 5import { FerdiStores } from '../../stores.types';
6
7type Props = {
8 children: ReactChildren;
9 loaded: boolean;
10 className: string;
11 color: string;
12 stores: FerdiStores;
13};
7 14
8@inject('stores') 15@inject('stores')
9@observer 16@observer
10class LoaderComponent extends Component { 17class LoaderComponent extends Component<Props> {
11 static propTypes = {
12 children: oneOrManyChildElements,
13 loaded: PropTypes.bool,
14 className: PropTypes.string,
15 color: PropTypes.string,
16 stores: PropTypes.shape({
17 settings: PropTypes.shape({
18 app: PropTypes.shape({
19 accentColor: PropTypes.string.isRequired,
20 }).isRequired,
21 }).isRequired,
22 }).isRequired,
23 };
24
25 static defaultProps = { 18 static defaultProps = {
26 children: null,
27 loaded: false, 19 loaded: false,
28 className: '',
29 color: 'ACCENT', 20 color: 'ACCENT',
30 }; 21 };
31 22
@@ -40,7 +31,6 @@ class LoaderComponent extends Component {
40 return ( 31 return (
41 <Loader 32 <Loader
42 loaded={loaded} 33 loaded={loaded}
43 // lines={10}
44 width={4} 34 width={4}
45 scale={0.6} 35 scale={0.6}
46 color={color} 36 color={color}
diff --git a/src/components/ui/Modal/index.js b/src/components/ui/Modal/index.tsx
index 3c7c66c59..f2f4461b8 100644
--- a/src/components/ui/Modal/index.js
+++ b/src/components/ui/Modal/index.tsx
@@ -1,28 +1,25 @@
1import React, { Component } from 'react'; 1import { Component, ReactChildren } from 'react';
2import ReactModal from 'react-modal'; 2import ReactModal from 'react-modal';
3import PropTypes from 'prop-types';
4import classnames from 'classnames'; 3import classnames from 'classnames';
5import injectCSS from 'react-jss'; 4import injectCSS from 'react-jss';
6import { Icon } from '@meetfranz/ui';
7
8import { mdiClose } from '@mdi/js'; 5import { mdiClose } from '@mdi/js';
6
7import { Icon } from '../icon';
9import styles from './styles'; 8import styles from './styles';
10 9
11// ReactModal.setAppElement('#root'); 10type Props = {
11 children: ReactChildren;
12 className: string;
13 classes: any;
14 isOpen: boolean;
15 portal: string;
16 close: () => void;
17 shouldCloseOnOverlayClick: boolean;
18 showClose: boolean;
19};
12 20
13@injectCSS(styles) 21@injectCSS(styles)
14class Modal extends Component { 22class Modal extends Component<Props> {
15 static propTypes = {
16 children: PropTypes.node.isRequired,
17 className: PropTypes.string,
18 classes: PropTypes.object.isRequired,
19 isOpen: PropTypes.bool.isRequired,
20 portal: PropTypes.string,
21 close: PropTypes.func.isRequired,
22 shouldCloseOnOverlayClick: PropTypes.bool,
23 showClose: PropTypes.bool,
24 };
25
26 static defaultProps = { 23 static defaultProps = {
27 className: null, 24 className: null,
28 portal: 'modal-portal', 25 portal: 'modal-portal',
diff --git a/src/components/ui/Modal/styles.js b/src/components/ui/Modal/styles.ts
index f32c075ce..c2bebf9bb 100644
--- a/src/components/ui/Modal/styles.js
+++ b/src/components/ui/Modal/styles.ts
@@ -1,4 +1,4 @@
1export default (theme) => ({ 1export default theme => ({
2 component: { 2 component: {
3 zIndex: 500, 3 zIndex: 500,
4 position: 'absolute', 4 position: 'absolute',
diff --git a/src/components/ui/Radio.js b/src/components/ui/Radio.tsx
index 65a777ff1..594ea70e4 100644
--- a/src/components/ui/Radio.js
+++ b/src/components/ui/Radio.tsx
@@ -1,20 +1,18 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 2import { observer } from 'mobx-react';
4import { Field } from 'mobx-react-form'; 3import { Field } from 'mobx-react-form';
5import classnames from 'classnames'; 4import classnames from 'classnames';
6 5
7@observer 6type Props = {
8class Radio extends Component { 7 field: typeof Field;
9 static propTypes = { 8 className: string;
10 field: PropTypes.instanceOf(Field).isRequired, 9 focus: boolean;
11 className: PropTypes.string, 10 showLabel: boolean;
12 focus: PropTypes.bool, 11};
13 showLabel: PropTypes.bool,
14 };
15 12
13@observer
14class Radio extends Component<Props> {
16 static defaultProps = { 15 static defaultProps = {
17 className: null,
18 focus: false, 16 focus: false,
19 showLabel: true, 17 showLabel: true,
20 }; 18 };
@@ -28,6 +26,7 @@ class Radio extends Component {
28 } 26 }
29 27
30 focus() { 28 focus() {
29 // @ts-expect-error Object is possibly 'null'.
31 this.inputElement.focus(); 30 this.inputElement.focus();
32 } 31 }
33 32
diff --git a/src/components/ui/SearchInput.js b/src/components/ui/SearchInput.tsx
index 2d760beab..af55b0e11 100644
--- a/src/components/ui/SearchInput.js
+++ b/src/components/ui/SearchInput.tsx
@@ -1,23 +1,22 @@
1import React, { Component } from 'react'; 1import { ChangeEvent, Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 2import { observer } from 'mobx-react';
4import classnames from 'classnames'; 3import classnames from 'classnames';
5import { debounce } from 'lodash'; 4import { debounce } from 'lodash';
6 5
7@observer 6type Props = {
8class SearchInput extends Component { 7 value: string;
9 static propTypes = { 8 placeholder: string;
10 value: PropTypes.string, 9 className: string;
11 placeholder: PropTypes.string, 10 onChange: (e: ChangeEvent<HTMLInputElement>) => void;
12 className: PropTypes.string, 11 onReset: () => void;
13 onChange: PropTypes.func, 12 name: string;
14 onReset: PropTypes.func, 13 throttle: boolean;
15 name: PropTypes.string, 14 throttleDelay: number;
16 throttle: PropTypes.bool, 15 autoFocus: boolean;
17 throttleDelay: PropTypes.number, 16};
18 autoFocus: PropTypes.bool,
19 };
20 17
18@observer
19class SearchInput extends Component<Props> {
21 static defaultProps = { 20 static defaultProps = {
22 value: '', 21 value: '',
23 placeholder: '', 22 placeholder: '',
@@ -32,7 +31,7 @@ class SearchInput extends Component {
32 31
33 input = null; 32 input = null;
34 33
35 constructor(props) { 34 constructor(props: Props) {
36 super(props); 35 super(props);
37 36
38 this.state = { 37 this.state = {
@@ -49,24 +48,27 @@ class SearchInput extends Component {
49 const { autoFocus } = this.props; 48 const { autoFocus } = this.props;
50 49
51 if (autoFocus) { 50 if (autoFocus) {
51 // @ts-expect-error Object is possibly 'null'.
52 this.input.focus(); 52 this.input.focus();
53 } 53 }
54 } 54 }
55 55
56 onChange(e) { 56 onChange(e: ChangeEvent<HTMLInputElement>) {
57 const { throttle, onChange } = this.props; 57 const { throttle, onChange } = this.props;
58 const { value } = e.target; 58 const { value } = e.target;
59 this.setState({ value }); 59 this.setState({ value });
60 60
61 if (throttle) { 61 if (throttle) {
62 e.persist(); 62 e.persist();
63 // @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'ChangeEvent<HTMLInputElement>'.
63 this.throttledOnChange(value); 64 this.throttledOnChange(value);
64 } else { 65 } else {
66 // @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'ChangeEvent<HTMLInputElement>'.
65 onChange(value); 67 onChange(value);
66 } 68 }
67 } 69 }
68 70
69 throttledOnChange(e) { 71 throttledOnChange(e: ChangeEvent<HTMLInputElement>) {
70 const { onChange } = this.props; 72 const { onChange } = this.props;
71 73
72 onChange(e); 74 onChange(e);
@@ -81,6 +83,7 @@ class SearchInput extends Component {
81 83
82 render() { 84 render() {
83 const { className, name, placeholder } = this.props; 85 const { className, name, placeholder } = this.props;
86 // @ts-expect-error Property 'value' does not exist on type 'Readonly<{}>'.
84 const { value } = this.state; 87 const { value } = this.state;
85 88
86 return ( 89 return (
@@ -94,6 +97,7 @@ class SearchInput extends Component {
94 value={value} 97 value={value}
95 onChange={e => this.onChange(e)} 98 onChange={e => this.onChange(e)}
96 ref={ref => { 99 ref={ref => {
100 // @ts-expect-error Type 'HTMLInputElement | null' is not assignable to type 'null'.
97 this.input = ref; 101 this.input = ref;
98 }} 102 }}
99 /> 103 />
diff --git a/src/components/ui/Select.js b/src/components/ui/Select.js
index 5ac7ddd6d..a560da332 100644
--- a/src/components/ui/Select.js
+++ b/src/components/ui/Select.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { createRef, Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { Field } from 'mobx-react-form'; 4import { Field } from 'mobx-react-form';
@@ -24,7 +24,7 @@ class Select extends Component {
24 constructor(props) { 24 constructor(props) {
25 super(props); 25 super(props);
26 26
27 this.element = React.createRef(); 27 this.element = createRef();
28 } 28 }
29 29
30 multipleChange() { 30 multipleChange() {
diff --git a/src/components/ui/ServiceIcon.js b/src/components/ui/ServiceIcon.js
index b2dadeac3..f067f8955 100644
--- a/src/components/ui/ServiceIcon.js
+++ b/src/components/ui/ServiceIcon.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss'; 4import injectSheet from 'react-jss';
@@ -6,7 +6,7 @@ import classnames from 'classnames';
6 6
7import ServiceModel from '../../models/Service'; 7import ServiceModel from '../../models/Service';
8 8
9const styles = (theme) => ({ 9const styles = theme => ({
10 root: { 10 root: {
11 height: 'auto', 11 height: 'auto',
12 }, 12 },
@@ -24,7 +24,8 @@ const styles = (theme) => ({
24 }, 24 },
25}); 25});
26 26
27@injectSheet(styles) @observer 27@injectSheet(styles)
28@observer
28class ServiceIcon extends Component { 29class ServiceIcon extends Component {
29 static propTypes = { 30 static propTypes = {
30 classes: PropTypes.object.isRequired, 31 classes: PropTypes.object.isRequired,
@@ -37,19 +38,10 @@ class ServiceIcon extends Component {
37 }; 38 };
38 39
39 render() { 40 render() {
40 const { 41 const { classes, className, service } = this.props;
41 classes,
42 className,
43 service,
44 } = this.props;
45 42
46 return ( 43 return (
47 <div 44 <div className={classnames([classes.root, className])}>
48 className={classnames([
49 classes.root,
50 className,
51 ])}
52 >
53 <img 45 <img
54 src={service.icon} 46 src={service.icon}
55 className={classnames([ 47 className={classnames([
diff --git a/src/components/ui/Slider.js b/src/components/ui/Slider.js
index 6f17eae00..dea6e0563 100644
--- a/src/components/ui/Slider.js
+++ b/src/components/ui/Slider.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import classnames from 'classnames'; 4import classnames from 'classnames';
diff --git a/src/components/ui/StatusBarTargetUrl.js b/src/components/ui/StatusBarTargetUrl.js
index ff4e8c795..38b436742 100644
--- a/src/components/ui/StatusBarTargetUrl.js
+++ b/src/components/ui/StatusBarTargetUrl.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import classnames from 'classnames'; 4import classnames from 'classnames';
diff --git a/src/components/ui/Tabs/TabItem.tsx b/src/components/ui/Tabs/TabItem.tsx
index bd613ddc7..9fcc3c41e 100644
--- a/src/components/ui/Tabs/TabItem.tsx
+++ b/src/components/ui/Tabs/TabItem.tsx
@@ -1,3 +1 @@
1import React from 'react';
2
3export const TabItem = ({ children }) => <>{children}</>; export const TabItem = ({ children }) => <>{children}</>;
diff --git a/src/components/ui/Tabs/Tabs.js b/src/components/ui/Tabs/Tabs.js
index 195398708..77803974b 100644
--- a/src/components/ui/Tabs/Tabs.js
+++ b/src/components/ui/Tabs/Tabs.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Children, Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import classnames from 'classnames'; 4import classnames from 'classnames';
@@ -36,7 +36,7 @@ class Tab extends Component {
36 return ( 36 return (
37 <div className="content-tabs"> 37 <div className="content-tabs">
38 <div className="content-tabs__tabs"> 38 <div className="content-tabs__tabs">
39 {React.Children.map(children, (child, i) => ( 39 {Children.map(children, (child, i) => (
40 <button 40 <button
41 key="{i}" 41 key="{i}"
42 className={classnames({ 42 className={classnames({
@@ -51,7 +51,7 @@ class Tab extends Component {
51 ))} 51 ))}
52 </div> 52 </div>
53 <div className="content-tabs__content"> 53 <div className="content-tabs__content">
54 {React.Children.map(children, (child, i) => ( 54 {Children.map(children, (child, i) => (
55 <div 55 <div
56 key="{i}" 56 key="{i}"
57 className={classnames({ 57 className={classnames({
diff --git a/src/components/ui/Toggle.js b/src/components/ui/Toggle.js
index bd7bc242d..dfc319735 100644
--- a/src/components/ui/Toggle.js
+++ b/src/components/ui/Toggle.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import classnames from 'classnames'; 4import classnames from 'classnames';
diff --git a/src/components/ui/ToggleRaw.js b/src/components/ui/ToggleRaw.js
index 1fde879ac..74292a870 100644
--- a/src/components/ui/ToggleRaw.js
+++ b/src/components/ui/ToggleRaw.js
@@ -1,7 +1,7 @@
1/** 1/**
2 * "Raw" Toggle - for usage without a MobX Form element 2 * "Raw" Toggle - for usage without a MobX Form element
3 */ 3 */
4import React, { Component } from 'react'; 4import { Component } from 'react';
5import PropTypes from 'prop-types'; 5import PropTypes from 'prop-types';
6import { observer } from 'mobx-react'; 6import { observer } from 'mobx-react';
7import classnames from 'classnames'; 7import classnames from 'classnames';
diff --git a/src/components/ui/WebviewLoader/index.js b/src/components/ui/WebviewLoader/index.js
index 8f4499e7b..8d4513172 100644
--- a/src/components/ui/WebviewLoader/index.js
+++ b/src/components/ui/WebviewLoader/index.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss'; 4import injectSheet from 'react-jss';
diff --git a/src/components/ui/WebviewLoader/styles.js b/src/components/ui/WebviewLoader/styles.ts
index 5d58011fe..dbd75db8a 100644
--- a/src/components/ui/WebviewLoader/styles.js
+++ b/src/components/ui/WebviewLoader/styles.ts
@@ -1,4 +1,4 @@
1export default (theme) => ({ 1export default theme => ({
2 component: { 2 component: {
3 background: theme.colorWebviewLoaderBackground, 3 background: theme.colorWebviewLoaderBackground,
4 padding: 20, 4 padding: 20,
diff --git a/src/components/ui/badge/ProBadge.tsx b/src/components/ui/badge/ProBadge.tsx
new file mode 100644
index 000000000..dc1e76f7f
--- /dev/null
+++ b/src/components/ui/badge/ProBadge.tsx
@@ -0,0 +1,64 @@
1import { mdiStar } from '@mdi/js';
2import classnames from 'classnames';
3import { Component } from 'react';
4import injectStyle from 'react-jss';
5
6import { Theme } from '../../../themes';
7import { Icon } from '../icon';
8import { Badge } from './index';
9import { IWithStyle } from '../typings/generic';
10
11interface IProps extends IWithStyle {
12 badgeClasses?: string;
13 iconClasses?: string;
14 inverted?: boolean;
15 className?: string;
16}
17
18const styles = (theme: Theme) => ({
19 badge: {
20 height: 'auto',
21 padding: [4, 6, 2, 7],
22 borderRadius: theme.borderRadiusSmall,
23 },
24 invertedBadge: {
25 background: theme.styleTypes.primary.contrast,
26 color: theme.styleTypes.primary.accent,
27 },
28 icon: {
29 fill: theme.styleTypes.primary.contrast,
30 },
31 invertedIcon: {
32 fill: theme.styleTypes.primary.accent,
33 },
34});
35
36class ProBadgeComponent extends Component<IProps> {
37 render() {
38 const { classes, badgeClasses, iconClasses, inverted, className } =
39 this.props;
40
41 return (
42 <Badge
43 type="primary"
44 className={classnames([
45 classes.badge,
46 inverted && classes.invertedBadge,
47 badgeClasses,
48 className,
49 ])}
50 >
51 <Icon
52 icon={mdiStar}
53 className={classnames([
54 classes.icon,
55 inverted && classes.invertedIcon,
56 iconClasses,
57 ])}
58 />
59 </Badge>
60 );
61 }
62}
63
64export const ProBadge = injectStyle(styles)(ProBadgeComponent);
diff --git a/src/components/ui/badge/index.tsx b/src/components/ui/badge/index.tsx
new file mode 100644
index 000000000..61bede937
--- /dev/null
+++ b/src/components/ui/badge/index.tsx
@@ -0,0 +1,71 @@
1import classnames from 'classnames';
2import { Component, ReactNode } from 'react';
3import injectStyle from 'react-jss';
4
5import { Theme } from '../../../themes';
6import { IWithStyle } from '../typings/generic';
7
8interface IProps extends IWithStyle {
9 type: string;
10 className?: string;
11 children: ReactNode;
12}
13
14const badgeStyles = (theme: Theme) => {
15 const styles = {};
16 Object.keys(theme.styleTypes).map(style => {
17 Object.assign(styles, {
18 [style]: {
19 background: theme.styleTypes[style].accent,
20 color: theme.styleTypes[style].contrast,
21 border: theme.styleTypes[style].border,
22 },
23 });
24 });
25
26 return styles;
27};
28
29const styles = (theme: Theme) => ({
30 badge: {
31 display: 'inline-block',
32 padding: [3, 8, 4],
33 fontSize: theme.badgeFontSize,
34 borderRadius: theme.badgeBorderRadius,
35 margin: [0, 4],
36
37 '&:first-child': {
38 marginLeft: 0,
39 },
40
41 '&:last-child': {
42 marginRight: 0,
43 },
44 },
45 ...badgeStyles(theme),
46});
47
48class BadgeComponent extends Component<IProps> {
49 public static defaultProps = {
50 type: 'primary',
51 };
52
53 render() {
54 const { classes, children, type, className } = this.props;
55
56 return (
57 <div
58 className={classnames({
59 [classes.badge]: true,
60 [classes[type]]: true,
61 [`${className}`]: className,
62 })}
63 data-type="franz-badge"
64 >
65 {children}
66 </div>
67 );
68 }
69}
70
71export const Badge = injectStyle(styles)(BadgeComponent);
diff --git a/src/components/ui/button/index.tsx b/src/components/ui/button/index.tsx
new file mode 100644
index 000000000..12e5e4449
--- /dev/null
+++ b/src/components/ui/button/index.tsx
@@ -0,0 +1,265 @@
1import Icon from '@mdi/react';
2import classnames from 'classnames';
3import { Property } from 'csstype';
4import { Component, MouseEvent } from 'react';
5import injectStyle, { withTheme } from 'react-jss';
6import Loader from 'react-loader';
7
8import { Theme } from '../../../themes';
9import { IFormField, IWithStyle } from '../typings/generic';
10
11type ButtonType =
12 | 'primary'
13 | 'secondary'
14 | 'success'
15 | 'danger'
16 | 'warning'
17 | 'inverted';
18
19interface IProps extends IFormField, IWithStyle {
20 className?: string;
21 disabled?: boolean;
22 id?: string;
23 type?: 'button' | 'reset' | 'submit' | undefined;
24 onClick: (
25 event: MouseEvent<HTMLButtonElement> | MouseEvent<HTMLAnchorElement>,
26 ) => void;
27 buttonType?: ButtonType;
28 stretch?: boolean;
29 loaded?: boolean;
30 busy?: boolean;
31 icon?: string;
32 href?: string;
33 target?: string;
34}
35
36let buttonTransition: string = 'none';
37let loaderContainerTransition: string = 'none';
38
39if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
40 buttonTransition = 'background .5s, opacity 0.3s';
41 loaderContainerTransition = 'all 0.3s';
42}
43
44const styles = (theme: Theme) => ({
45 button: {
46 borderRadius: theme.borderRadiusSmall,
47 border: 'none',
48 display: 'inline-flex',
49 position: 'relative' as Property.Position,
50 transition: buttonTransition,
51 textAlign: 'center' as Property.TextAlign,
52 outline: 'none',
53 alignItems: 'center',
54 padding: 0,
55 width: (props: IProps) =>
56 (props.stretch ? '100%' : 'auto') as Property.Width<string>,
57 fontSize: theme.uiFontSize,
58 textDecoration: 'none',
59
60 '&:hover': {
61 opacity: 0.8,
62 },
63 '&:active': {
64 opacity: 0.5,
65 transition: 'none',
66 },
67 },
68 label: {
69 margin: '10px 20px',
70 width: '100%',
71 display: 'flex',
72 alignItems: 'center',
73 justifyContent: 'center',
74 },
75 primary: {
76 background: theme.buttonPrimaryBackground,
77 color: theme.buttonPrimaryTextColor,
78
79 '& svg': {
80 fill: theme.buttonPrimaryTextColor,
81 },
82 },
83 secondary: {
84 background: theme.buttonSecondaryBackground,
85 color: theme.buttonSecondaryTextColor,
86
87 '& svg': {
88 fill: theme.buttonSecondaryTextColor,
89 },
90 },
91 success: {
92 background: theme.buttonSuccessBackground,
93 color: theme.buttonSuccessTextColor,
94
95 '& svg': {
96 fill: theme.buttonSuccessTextColor,
97 },
98 },
99 danger: {
100 background: theme.buttonDangerBackground,
101 color: theme.buttonDangerTextColor,
102
103 '& svg': {
104 fill: theme.buttonDangerTextColor,
105 },
106 },
107 warning: {
108 background: theme.buttonWarningBackground,
109 color: theme.buttonWarningTextColor,
110
111 '& svg': {
112 fill: theme.buttonWarningTextColor,
113 },
114 },
115 inverted: {
116 background: theme.buttonInvertedBackground,
117 color: theme.buttonInvertedTextColor,
118 border: theme.buttonInvertedBorder,
119
120 '& svg': {
121 fill: theme.buttonInvertedTextColor,
122 },
123 },
124 disabled: {
125 opacity: theme.inputDisabledOpacity,
126 },
127 loader: {
128 position: 'relative' as Property.Position,
129 width: 20,
130 height: 18,
131 zIndex: 9999,
132 },
133 loaderContainer: {
134 width: (props: IProps): string => (!props.busy ? '0' : '40px'),
135 height: 20,
136 overflow: 'hidden',
137 transition: loaderContainerTransition,
138 marginLeft: (props: IProps): number => (!props.busy ? 10 : 20),
139 marginRight: (props: IProps): number => (!props.busy ? -10 : -20),
140 position: (props: IProps): Property.Position =>
141 props.stretch ? 'absolute' : 'inherit',
142 },
143 icon: {
144 margin: [1, 10, 0, -5],
145 },
146});
147
148class ButtonComponent extends Component<IProps> {
149 public static defaultProps = {
150 type: 'button',
151 disabled: false,
152 onClick: () => null,
153 buttonType: 'primary' as ButtonType,
154 stretch: false,
155 busy: false,
156 };
157
158 state = {
159 busy: false,
160 };
161
162 componentWillMount() {
163 this.setState({ busy: this.props.busy });
164 }
165
166 componentWillReceiveProps(nextProps: IProps) {
167 if (nextProps.busy !== this.props.busy) {
168 if (this.props.busy) {
169 setTimeout(() => {
170 this.setState({ busy: nextProps.busy });
171 }, 300);
172 } else {
173 this.setState({ busy: nextProps.busy });
174 }
175 }
176 }
177
178 render() {
179 const {
180 classes,
181 className,
182 theme,
183 disabled,
184 id,
185 label,
186 type,
187 onClick,
188 buttonType,
189 loaded,
190 icon,
191 href,
192 target,
193 } = this.props;
194
195 const { busy } = this.state;
196
197 let showLoader = false;
198 if (loaded) {
199 showLoader = !loaded;
200 console.warn(
201 'Ferdi Button prop `loaded` will be deprecated in the future. Please use `busy` instead',
202 );
203 }
204 if (busy) {
205 showLoader = busy;
206 }
207
208 const content = (
209 <>
210 <div className={classes.loaderContainer}>
211 {showLoader && (
212 <Loader
213 loaded={false}
214 width={4}
215 scale={0.45}
216 color={theme.buttonLoaderColor[buttonType!]}
217 parentClassName={classes.loader}
218 />
219 )}
220 </div>
221 <div className={classes.label}>
222 {icon && <Icon path={icon} size={0.8} className={classes.icon} />}
223 {label}
224 </div>
225 </>
226 );
227
228 const wrapperComponent = !href ? (
229 <button
230 id={id}
231 type={type}
232 onClick={onClick}
233 className={classnames({
234 [`${classes.button}`]: true,
235 [`${classes[buttonType as ButtonType]}`]: true,
236 [`${classes.disabled}`]: disabled,
237 [`${className}`]: className,
238 })}
239 disabled={disabled}
240 data-type="franz-button"
241 >
242 {content}
243 </button>
244 ) : (
245 <a
246 href={href}
247 target={target}
248 onClick={onClick}
249 className={classnames({
250 [`${classes.button}`]: true,
251 [`${classes[buttonType as ButtonType]}`]: true,
252 [`${className}`]: className,
253 })}
254 rel={target === '_blank' ? 'noopener' : ''}
255 data-type="franz-button"
256 >
257 {content}
258 </a>
259 );
260
261 return wrapperComponent;
262 }
263}
264
265export const Button = injectStyle(styles)(withTheme(ButtonComponent));
diff --git a/src/components/ui/effects/Appear.js b/src/components/ui/effects/Appear.js
deleted file mode 100644
index 183181f8f..000000000
--- a/src/components/ui/effects/Appear.js
+++ /dev/null
@@ -1,47 +0,0 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
4
5export default class Appear extends Component {
6 static propTypes = {
7 // eslint-disable-next-line react/forbid-prop-types
8 children: PropTypes.any.isRequired,
9 transitionName: PropTypes.string,
10 className: PropTypes.string,
11 };
12
13 static defaultProps = {
14 transitionName: 'fadeIn',
15 className: '',
16 };
17
18 state = {
19 mounted: false,
20 };
21
22 componentDidMount() {
23 this.setState({ mounted: true });
24 }
25
26 render() {
27 const { children, transitionName, className } = this.props;
28
29 if (!this.state.mounted) {
30 return null;
31 }
32
33 return (
34 <ReactCSSTransitionGroup
35 transitionName={transitionName}
36 transitionAppear
37 transitionLeave
38 transitionAppearTimeout={1500}
39 transitionEnterTimeout={1500}
40 transitionLeaveTimeout={1500}
41 className={className}
42 >
43 {children}
44 </ReactCSSTransitionGroup>
45 );
46 }
47}
diff --git a/src/components/ui/effects/Appear.tsx b/src/components/ui/effects/Appear.tsx
new file mode 100644
index 000000000..117c02f97
--- /dev/null
+++ b/src/components/ui/effects/Appear.tsx
@@ -0,0 +1,39 @@
1import { ReactChildren, useEffect, useState } from 'react';
2import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
3
4type Props = {
5 children: ReactChildren;
6 transitionName: string;
7 className: string;
8};
9const Appear = ({
10 children,
11 transitionName = 'fadeIn',
12 className = '',
13}: Props) => {
14 const [mounted, setMounted] = useState(false);
15
16 useEffect(() => {
17 setMounted(true);
18 }, []);
19
20 if (!mounted) {
21 return null;
22 }
23
24 return (
25 <ReactCSSTransitionGroup
26 transitionName={transitionName}
27 transitionAppear
28 transitionLeave
29 transitionAppearTimeout={1500}
30 transitionEnterTimeout={1500}
31 transitionLeaveTimeout={1500}
32 className={className}
33 >
34 {children}
35 </ReactCSSTransitionGroup>
36 );
37};
38
39export default Appear;
diff --git a/src/components/ui/error/index.tsx b/src/components/ui/error/index.tsx
new file mode 100644
index 000000000..8439bfc8b
--- /dev/null
+++ b/src/components/ui/error/index.tsx
@@ -0,0 +1,20 @@
1import { Classes } from 'jss';
2import { Component } from 'react';
3import injectSheet from 'react-jss';
4
5import styles from './styles';
6
7interface IProps {
8 classes: Classes;
9 message: string;
10}
11
12class ErrorComponent extends Component<IProps> {
13 render() {
14 const { classes, message } = this.props;
15
16 return <p className={classes.message}>{message}</p>;
17 }
18}
19
20export const Error = injectSheet(styles)(ErrorComponent);
diff --git a/src/components/ui/error/styles.ts b/src/components/ui/error/styles.ts
new file mode 100644
index 000000000..9da95705a
--- /dev/null
+++ b/src/components/ui/error/styles.ts
@@ -0,0 +1,9 @@
1import { Theme } from '../../../themes';
2
3export default (theme: Theme) => ({
4 message: {
5 color: theme.brandDanger,
6 margin: '5px 0 0',
7 fontSize: theme.uiFontSize,
8 },
9});
diff --git a/src/components/ui/headline/index.tsx b/src/components/ui/headline/index.tsx
new file mode 100644
index 000000000..ea2949102
--- /dev/null
+++ b/src/components/ui/headline/index.tsx
@@ -0,0 +1,70 @@
1import classnames from 'classnames';
2import { Component, createElement, ReactNode } from 'react';
3import injectStyle from 'react-jss';
4
5import { Theme } from '../../../themes';
6import { IWithStyle, Omit } from '../typings/generic';
7
8interface IProps extends IWithStyle {
9 level?: number;
10 className?: string;
11 children: string | ReactNode;
12 id?: string;
13}
14
15const styles = (theme: Theme) => ({
16 headline: {
17 fontWeight: 'lighter',
18 color: theme.colorText,
19 marginTop: 0,
20 marginBottom: 10,
21 textAlign: 'left',
22 },
23 h1: {
24 fontSize: 30,
25 marginTop: 0,
26 },
27 h2: {
28 fontSize: 20,
29 },
30 h3: {
31 fontSize: 18,
32 },
33 h4: {
34 fontSize: theme.uiFontSize,
35 },
36});
37
38class HeadlineComponent extends Component<IProps> {
39 render() {
40 const { classes, level, className, children, id } = this.props;
41
42 return createElement(
43 `h${level}`,
44 {
45 id,
46 className: classnames({
47 [classes.headline]: true,
48 [classes[level ? `h${level}` : 'h1']]: true,
49 [`${className}`]: className,
50 }),
51 'data-type': 'franz-headline',
52 },
53 children,
54 );
55 }
56}
57
58const Headline = injectStyle(styles)(HeadlineComponent);
59
60const createH = (level: number) => (props: Omit<IProps, 'classes' | 'theme'>) =>
61 (
62 <Headline level={level} {...props}>
63 {props.children}
64 </Headline>
65 );
66
67export const H1 = createH(1);
68export const H2 = createH(2);
69export const H3 = createH(3);
70export const H4 = createH(4);
diff --git a/src/components/ui/icon/index.tsx b/src/components/ui/icon/index.tsx
new file mode 100644
index 000000000..85bb61d13
--- /dev/null
+++ b/src/components/ui/icon/index.tsx
@@ -0,0 +1,46 @@
1import MdiIcon from '@mdi/react';
2import classnames from 'classnames';
3import { Component } from 'react';
4import injectStyle from 'react-jss';
5
6import { Theme } from '../../../themes';
7import { IWithStyle } from '../typings/generic';
8
9interface IProps extends IWithStyle {
10 icon: string;
11 size?: number;
12 className?: string;
13}
14
15const styles = (theme: Theme) => ({
16 icon: {
17 fill: theme.colorText,
18 },
19});
20
21class IconComponent extends Component<IProps> {
22 public static defaultProps = {
23 size: 1,
24 };
25
26 render() {
27 const { classes, icon, size, className } = this.props;
28
29 if (!icon) {
30 console.warn('No Icon specified');
31 }
32
33 return (
34 <MdiIcon
35 path={icon}
36 size={size}
37 className={classnames({
38 [classes.icon]: true,
39 [`${className}`]: className,
40 })}
41 />
42 );
43 }
44}
45
46export const Icon = injectStyle(styles)(IconComponent);
diff --git a/src/components/ui/infobox/index.tsx b/src/components/ui/infobox/index.tsx
new file mode 100644
index 000000000..87940c4d4
--- /dev/null
+++ b/src/components/ui/infobox/index.tsx
@@ -0,0 +1,205 @@
1import { mdiClose } from '@mdi/js';
2import classnames from 'classnames';
3import { Component, ReactNode } from 'react';
4import injectStyle from 'react-jss';
5
6import { Theme } from '../../../themes';
7import { Icon } from '../icon';
8import { IWithStyle } from '../typings/generic';
9
10interface IProps extends IWithStyle {
11 icon?: string;
12 type?: string;
13 dismissable?: boolean;
14 onDismiss?: () => void;
15 onUnmount?: () => void;
16 ctaOnClick?: () => void;
17 ctaLabel?: string;
18 ctaLoading?: boolean;
19 children: ReactNode;
20 className: string;
21}
22
23interface IState {
24 isDismissing: boolean;
25 dismissed: boolean;
26}
27
28const buttonStyles = (theme: Theme) => {
29 const styles = {};
30 Object.keys(theme.styleTypes).map(style => {
31 Object.assign(styles, {
32 [style]: {
33 background: theme.styleTypes[style].accent,
34 color: theme.styleTypes[style].contrast,
35 border: theme.styleTypes[style].border,
36
37 '& svg': {
38 fill: theme.styleTypes[style].contrast,
39 },
40 },
41 });
42 });
43
44 return styles;
45};
46
47const infoBoxTransition: string = 'none';
48const ctaTransition: string = 'none';
49
50// TODO: Not sure why, but this location alone, the `dinwo` is not defined - and it throws an error thus aborting the startup sequence of ferdi
51// if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
52// infoBoxTransition = 'all 0.5s';
53// ctaTransition = 'opacity 0.3s';
54// }
55
56const styles = (theme: Theme) => ({
57 wrapper: {
58 position: 'relative',
59 overflow: 'hidden',
60 height: 'auto',
61 marginBottom: 30,
62 },
63 infobox: {
64 alignItems: 'center',
65 borderRadius: theme.borderRadiusSmall,
66 display: 'flex',
67 height: 'auto',
68 padding: '15px 20px',
69 top: 0,
70 transition: infoBoxTransition,
71 opacity: 1,
72 },
73 dismissing: {
74 // position: 'absolute',
75 marginTop: -100,
76 opacity: 0,
77 },
78 content: {
79 flex: 1,
80 },
81 icon: {
82 marginRight: 10,
83 },
84 close: {
85 color: (props: IProps) =>
86 theme.styleTypes[props.type ? props.type : 'primary'].contrast,
87 marginRight: -5,
88 border: 0,
89 background: 'none',
90 },
91 cta: {
92 borderColor: (props: IProps) =>
93 theme.styleTypes[props.type ? props.type : 'primary'].contrast,
94 borderRadius: theme.borderRadiusSmall,
95 borderStyle: 'solid',
96 borderWidth: 1,
97 background: 'none',
98 color: (props: IProps) =>
99 theme.styleTypes[props.type ? props.type : 'primary'].contrast,
100 marginLeft: 15,
101 padding: [4, 10],
102 fontSize: theme.uiFontSize,
103 transition: ctaTransition,
104
105 '&:hover': {
106 opacity: 0.6,
107 },
108 },
109 ...buttonStyles(theme),
110});
111
112class InfoboxComponent extends Component<IProps, IState> {
113 public static defaultProps = {
114 type: 'primary',
115 dismissable: false,
116 ctaOnClick: () => {},
117 onDismiss: () => {},
118 ctaLabel: '',
119 ctaLoading: false,
120 };
121
122 state = {
123 isDismissing: false,
124 dismissed: false,
125 };
126
127 dismiss() {
128 const { onDismiss } = this.props;
129
130 this.setState({
131 isDismissing: true,
132 });
133
134 if (onDismiss) {
135 onDismiss();
136 }
137
138 setTimeout(() => {
139 this.setState({
140 dismissed: true,
141 });
142 }, 3000);
143 }
144
145 componentWillUnmount(): void {
146 const { onUnmount } = this.props;
147 if (onUnmount) onUnmount();
148 }
149
150 render() {
151 const {
152 classes,
153 children,
154 icon,
155 type,
156 ctaLabel,
157 ctaOnClick,
158 dismissable,
159 className,
160 } = this.props;
161
162 const { isDismissing, dismissed } = this.state;
163
164 if (dismissed) {
165 return null;
166 }
167
168 return (
169 <div
170 className={classnames({
171 [classes.wrapper]: true,
172 [`${className}`]: className,
173 })}
174 >
175 <div
176 className={classnames({
177 [classes.infobox]: true,
178 [classes[`${type}`]]: type,
179 [classes.dismissing]: isDismissing,
180 })}
181 data-type="franz-infobox"
182 >
183 {icon && <Icon icon={icon} className={classes.icon} />}
184 <div className={classes.content}>{children}</div>
185 {ctaLabel && (
186 <button className={classes.cta} onClick={ctaOnClick} type="button">
187 {ctaLabel}
188 </button>
189 )}
190 {dismissable && (
191 <button
192 type="button"
193 onClick={this.dismiss.bind(this)}
194 className={classes.close}
195 >
196 <Icon icon={mdiClose} />
197 </button>
198 )}
199 </div>
200 </div>
201 );
202 }
203}
204
205export const Infobox = injectStyle(styles)(InfoboxComponent);
diff --git a/src/components/ui/input/index.tsx b/src/components/ui/input/index.tsx
new file mode 100644
index 000000000..0b16fe688
--- /dev/null
+++ b/src/components/ui/input/index.tsx
@@ -0,0 +1,208 @@
1import { mdiEye, mdiEyeOff } from '@mdi/js';
2import Icon from '@mdi/react';
3import classnames from 'classnames';
4import { Component, createRef, InputHTMLAttributes } from 'react';
5import injectSheet from 'react-jss';
6
7import { IFormField, IWithStyle } from '../typings/generic';
8
9import { Error } from '../error';
10import { Label } from '../label';
11import { Wrapper } from '../wrapper';
12import { scorePasswordFunc } from './scorePassword';
13
14import styles from './styles';
15
16interface IData {
17 [index: string]: string;
18}
19
20interface IProps
21 extends InputHTMLAttributes<HTMLInputElement>,
22 IFormField,
23 IWithStyle {
24 focus?: boolean;
25 prefix?: string;
26 suffix?: string;
27 scorePassword?: boolean;
28 showPasswordToggle?: boolean;
29 data: IData;
30 inputClassName?: string;
31 onEnterKey?: Function;
32}
33
34interface IState {
35 showPassword: boolean;
36 passwordScore: number;
37}
38
39class InputComponent extends Component<IProps, IState> {
40 static defaultProps = {
41 focus: false,
42 onChange: () => {},
43 onBlur: () => {},
44 onFocus: () => {},
45 scorePassword: false,
46 showLabel: true,
47 showPasswordToggle: false,
48 type: 'text',
49 disabled: false,
50 };
51
52 state = {
53 passwordScore: 0,
54 showPassword: false,
55 };
56
57 private inputRef = createRef<HTMLInputElement>();
58
59 componentDidMount() {
60 const { focus, data } = this.props;
61
62 if (this.inputRef && this.inputRef.current) {
63 if (focus) {
64 this.inputRef.current.focus();
65 }
66
67 if (data) {
68 Object.keys(data).map(
69 key => (this.inputRef.current!.dataset[key] = data[key]),
70 );
71 }
72 }
73 }
74
75 onChange(e: React.ChangeEvent<HTMLInputElement>) {
76 const { scorePassword, onChange } = this.props;
77
78 if (onChange) {
79 onChange(e);
80 }
81
82 if (this.inputRef && this.inputRef.current && scorePassword) {
83 this.setState({
84 passwordScore: scorePasswordFunc(this.inputRef.current.value),
85 });
86 }
87 }
88
89 onInputKeyPress(e: React.KeyboardEvent) {
90 if (e.key === 'Enter') {
91 const { onEnterKey } = this.props;
92 onEnterKey && onEnterKey();
93 }
94 }
95
96 render() {
97 const {
98 classes,
99 className,
100 disabled,
101 error,
102 id,
103 inputClassName,
104 label,
105 prefix,
106 scorePassword,
107 suffix,
108 showLabel,
109 showPasswordToggle,
110 type,
111 value,
112 name,
113 placeholder,
114 spellCheck,
115 onBlur,
116 onFocus,
117 min,
118 max,
119 step,
120 required,
121 noMargin,
122 } = this.props;
123
124 const { showPassword, passwordScore } = this.state;
125
126 const inputType = type === 'password' && showPassword ? 'text' : type;
127
128 return (
129 <Wrapper
130 className={className}
131 identifier="franz-input"
132 noMargin={noMargin}
133 >
134 <Label
135 title={label}
136 showLabel={showLabel}
137 htmlFor={id}
138 className={classes.label}
139 isRequired={required}
140 >
141 <div
142 className={classnames({
143 [`${inputClassName}`]: inputClassName,
144 [`${classes.hasPasswordScore}`]: scorePassword,
145 [`${classes.wrapper}`]: true,
146 [`${classes.disabled}`]: disabled,
147 [`${classes.hasError}`]: error,
148 })}
149 >
150 {prefix && <span className={classes.prefix}>{prefix}</span>}
151 <input
152 id={id}
153 type={inputType}
154 name={name}
155 value={value as string}
156 placeholder={placeholder}
157 spellCheck={spellCheck}
158 className={classes.input}
159 ref={this.inputRef}
160 onChange={this.onChange.bind(this)}
161 onFocus={onFocus}
162 onBlur={onBlur}
163 disabled={disabled}
164 onKeyPress={this.onInputKeyPress.bind(this)}
165 min={min}
166 max={max}
167 step={step}
168 />
169 {suffix && <span className={classes.suffix}>{suffix}</span>}
170 {showPasswordToggle && (
171 <button
172 type="button"
173 className={classes.formModifier}
174 onClick={() =>
175 this.setState(prevState => ({
176 showPassword: !prevState.showPassword,
177 }))
178 }
179 tabIndex={-1}
180 >
181 <Icon path={!showPassword ? mdiEye : mdiEyeOff} size={1} />
182 </button>
183 )}
184 </div>
185 {scorePassword && (
186 <div
187 className={classnames({
188 [`${classes.passwordScore}`]: true,
189 [`${classes.hasError}`]: error,
190 })}
191 >
192 <meter
193 value={passwordScore < 5 ? 5 : passwordScore}
194 low={30}
195 high={75}
196 optimum={100}
197 max={100}
198 />
199 </div>
200 )}
201 </Label>
202 {error && <Error message={error} />}
203 </Wrapper>
204 );
205 }
206}
207
208export const Input = injectSheet(styles)(InputComponent);
diff --git a/src/components/ui/input/scorePassword.ts b/src/components/ui/input/scorePassword.ts
new file mode 100644
index 000000000..59502e2b0
--- /dev/null
+++ b/src/components/ui/input/scorePassword.ts
@@ -0,0 +1,42 @@
1interface ILetters {
2 [key: string]: number;
3}
4
5interface IVariations {
6 [index: string]: boolean;
7 digits: boolean;
8 lower: boolean;
9 nonWords: boolean;
10 upper: boolean;
11}
12
13export function scorePasswordFunc(password: string): number {
14 let score = 0;
15 if (!password) {
16 return score;
17 }
18
19 // award every unique letter until 5 repetitions
20 const letters: ILetters = {};
21 for (const element of password) {
22 letters[element] = (letters[element] || 0) + 1;
23 score += 5 / letters[element];
24 }
25
26 // bonus points for mixing it up
27 const variations: IVariations = {
28 digits: /\d/.test(password),
29 lower: /[a-z]/.test(password),
30 nonWords: /\W/.test(password),
31 upper: /[A-Z]/.test(password),
32 };
33
34 let variationCount = 0;
35 for (const key of Object.keys(variations)) {
36 variationCount += variations[key] === true ? 1 : 0;
37 }
38
39 score += (variationCount - 1) * 10;
40
41 return Math.round(score);
42}
diff --git a/src/components/ui/input/styles.ts b/src/components/ui/input/styles.ts
new file mode 100644
index 000000000..04c1b3991
--- /dev/null
+++ b/src/components/ui/input/styles.ts
@@ -0,0 +1,102 @@
1import { Property } from 'csstype';
2
3import { Theme } from '../../../themes';
4
5const prefixStyles = (theme: Theme) => ({
6 background: theme.inputPrefixBackground,
7 color: theme.inputPrefixColor,
8 lineHeight: `${theme.inputHeight}px`,
9 padding: '0 10px',
10 fontSize: theme.uiFontSize,
11});
12
13export default (theme: Theme) => ({
14 label: {
15 '& > div': {
16 marginTop: 5,
17 },
18 },
19 disabled: {
20 opacity: theme.inputDisabledOpacity,
21 },
22 formModifier: {
23 background: 'none',
24 border: 0,
25 borderLeft: theme.inputBorder,
26 padding: '4px 20px 0',
27 outline: 'none',
28
29 '&:active': {
30 opacity: 0.5,
31 },
32
33 '& svg': {
34 fill: theme.inputModifierColor,
35 },
36 },
37 input: {
38 background: 'none',
39 border: 0,
40 fontSize: theme.uiFontSize,
41 outline: 'none',
42 padding: 8,
43 width: '100%',
44 color: theme.inputColor,
45
46 '&::placeholder': {
47 color: theme.inputPlaceholderColor,
48 },
49 },
50 passwordScore: {
51 background: theme.inputScorePasswordBackground,
52 border: theme.inputBorder,
53 borderTopWidth: 0,
54 borderBottomLeftRadius: theme.borderRadiusSmall,
55 borderBottomRightRadius: theme.borderRadiusSmall,
56 display: 'block',
57 flexBasis: '100%',
58 height: 5,
59 overflow: 'hidden',
60
61 '& meter': {
62 display: 'block',
63 height: '100%',
64 width: '100%',
65
66 '&::-webkit-meter-bar': {
67 background: 'none',
68 },
69
70 '&::-webkit-meter-even-less-good-value': {
71 background: theme.brandDanger,
72 },
73
74 '&::-webkit-meter-suboptimum-value': {
75 background: theme.brandWarning,
76 },
77
78 '&::-webkit-meter-optimum-value': {
79 background: theme.brandSuccess,
80 },
81 },
82 },
83 prefix: prefixStyles(theme),
84 suffix: prefixStyles(theme),
85 wrapper: {
86 background: theme.inputBackground,
87 border: theme.inputBorder,
88 borderRadius: theme.borderRadiusSmall,
89 boxSizing: 'border-box' as Property.BoxSizing,
90 display: 'flex',
91 height: theme.inputHeight,
92 order: 1,
93 width: '100%',
94 },
95 hasPasswordScore: {
96 borderBottomLeftRadius: 0,
97 borderBottomRightRadius: 0,
98 },
99 hasError: {
100 borderColor: theme.brandDanger,
101 },
102});
diff --git a/src/components/ui/label/index.tsx b/src/components/ui/label/index.tsx
new file mode 100644
index 000000000..4d86f23f7
--- /dev/null
+++ b/src/components/ui/label/index.tsx
@@ -0,0 +1,52 @@
1import classnames from 'classnames';
2import { Classes } from 'jss';
3import { Component, LabelHTMLAttributes } from 'react';
4import injectSheet from 'react-jss';
5
6import { IFormField } from '../typings/generic';
7
8import styles from './styles';
9
10interface ILabel extends IFormField, LabelHTMLAttributes<HTMLLabelElement> {
11 classes: Classes;
12 isRequired: boolean;
13}
14
15class LabelComponent extends Component<ILabel> {
16 static defaultProps = {
17 showLabel: true,
18 };
19
20 render() {
21 const {
22 title,
23 showLabel,
24 classes,
25 className,
26 children,
27 htmlFor,
28 isRequired,
29 } = this.props;
30
31 if (!showLabel) return children;
32
33 return (
34 <label
35 className={classnames({
36 [`${className}`]: className,
37 })}
38 htmlFor={htmlFor}
39 >
40 {showLabel && (
41 <span className={classes.label}>
42 {title}
43 {isRequired && ' *'}
44 </span>
45 )}
46 <div className={classes.content}>{children}</div>
47 </label>
48 );
49 }
50}
51
52export const Label = injectSheet(styles)(LabelComponent);
diff --git a/src/components/ui/label/styles.ts b/src/components/ui/label/styles.ts
new file mode 100644
index 000000000..faa44ae5b
--- /dev/null
+++ b/src/components/ui/label/styles.ts
@@ -0,0 +1,12 @@
1import { Theme } from '../../../themes';
2
3export default (theme: Theme) => ({
4 content: {},
5 label: {
6 color: theme.labelColor,
7 fontSize: theme.uiFontSize,
8 },
9 hasError: {
10 color: theme.brandDanger,
11 },
12});
diff --git a/src/components/ui/loader/index.tsx b/src/components/ui/loader/index.tsx
new file mode 100644
index 000000000..0607bd48b
--- /dev/null
+++ b/src/components/ui/loader/index.tsx
@@ -0,0 +1,44 @@
1import classnames from 'classnames';
2import { Component } from 'react';
3import injectStyle, { withTheme } from 'react-jss';
4import ReactLoader from 'react-loader';
5
6import { IWithStyle } from '../typings/generic';
7
8interface IProps extends IWithStyle {
9 className?: string;
10 color?: string;
11}
12
13const styles = () => ({
14 container: {
15 position: 'relative',
16 height: 60,
17 },
18});
19
20class LoaderComponent extends Component<IProps> {
21 render() {
22 const { classes, className, color, theme } = this.props;
23
24 return (
25 <div
26 className={classnames({
27 [classes.container]: true,
28 [`${className}`]: className,
29 })}
30 data-type="franz-loader"
31 >
32 <ReactLoader
33 loaded={false}
34 width={4}
35 scale={0.75}
36 color={color || theme.colorText}
37 parentClassName={classes.loader}
38 />
39 </div>
40 );
41 }
42}
43
44export const Loader = injectStyle(styles)(withTheme(LoaderComponent));
diff --git a/src/components/ui/select/index.tsx b/src/components/ui/select/index.tsx
new file mode 100644
index 000000000..2605503a3
--- /dev/null
+++ b/src/components/ui/select/index.tsx
@@ -0,0 +1,460 @@
1import {
2 mdiArrowRightDropCircleOutline,
3 mdiCloseCircle,
4 mdiMagnify,
5} from '@mdi/js';
6import Icon from '@mdi/react';
7import classnames from 'classnames';
8import { ChangeEvent, Component, createRef } from 'react';
9import injectStyle from 'react-jss';
10
11import { Theme } from '../../../themes';
12import { IFormField, IWithStyle } from '../typings/generic';
13
14import { Error } from '../error';
15import { Label } from '../label';
16import { Wrapper } from '../wrapper';
17
18interface IOptions {
19 [index: string]: string;
20}
21
22interface IData {
23 [index: string]: string;
24}
25
26interface IProps extends IFormField, IWithStyle {
27 actionText: string;
28 className?: string;
29 inputClassName?: string;
30 defaultValue?: string;
31 disabled?: boolean;
32 id?: string;
33 name: string;
34 options: IOptions;
35 value: string;
36 onChange: (event: ChangeEvent<HTMLInputElement>) => void;
37 showSearch: boolean;
38 data: IData;
39}
40
41interface IState {
42 open: boolean;
43 value: string;
44 needle: string;
45 selected: number;
46 options: IOptions;
47}
48
49let popupTransition: string = 'none';
50let toggleTransition: string = 'none';
51
52if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
53 popupTransition = 'all 0.3s';
54 toggleTransition = 'transform 0.3s';
55}
56
57const styles = (theme: Theme) => ({
58 select: {
59 background: theme.selectBackground,
60 border: theme.selectBorder,
61 borderRadius: theme.borderRadiusSmall,
62 height: theme.selectHeight,
63 fontSize: theme.uiFontSize,
64 width: '100%',
65 display: 'flex',
66 alignItems: 'center',
67 textAlign: 'left',
68 color: theme.selectColor,
69 },
70 label: {
71 '& > div': {
72 marginTop: 5,
73 },
74 },
75 popup: {
76 opacity: 0,
77 height: 0,
78 overflowX: 'scroll',
79 border: theme.selectBorder,
80 borderTop: 0,
81 transition: popupTransition,
82 },
83 open: {
84 opacity: 1,
85 height: 350,
86 background: theme.selectPopupBackground,
87 },
88 option: {
89 padding: 10,
90 borderBottom: theme.selectOptionBorder,
91 color: theme.selectOptionColor,
92
93 '&:hover': {
94 background: theme.selectOptionItemHover,
95 color: theme.selectOptionItemHoverColor,
96 },
97 '&:active': {
98 background: theme.selectOptionItemActive,
99 color: theme.selectOptionItemActiveColor,
100 },
101 },
102 selected: {
103 background: theme.selectOptionItemActive,
104 color: theme.selectOptionItemActiveColor,
105 },
106 toggle: {
107 marginLeft: 'auto',
108 fill: theme.selectToggleColor,
109 transition: toggleTransition,
110 },
111 toggleOpened: {
112 transform: 'rotateZ(90deg)',
113 },
114 searchContainer: {
115 display: 'flex',
116 background: theme.selectSearchBackground,
117 alignItems: 'center',
118 paddingLeft: 10,
119 color: theme.selectColor,
120
121 '& svg': {
122 fill: theme.selectSearchColor,
123 },
124 },
125 search: {
126 border: 0,
127 width: '100%',
128 fontSize: theme.uiFontSize,
129 background: 'none',
130 marginLeft: 10,
131 padding: [10, 0],
132 color: theme.selectSearchColor,
133 },
134 clearNeedle: {
135 background: 'none',
136 border: 0,
137 },
138 focused: {
139 fontWeight: 'bold',
140 background: theme.selectOptionItemHover,
141 color: theme.selectOptionItemHoverColor,
142 },
143 hasError: {
144 borderColor: theme.brandDanger,
145 },
146 disabled: {
147 opacity: theme.selectDisabledOpacity,
148 },
149});
150
151class SelectComponent extends Component<IProps> {
152 public static defaultProps = {
153 onChange: () => {},
154 showLabel: true,
155 disabled: false,
156 error: '',
157 };
158
159 state = {
160 open: false,
161 value: '',
162 needle: '',
163 selected: 0,
164 options: null,
165 };
166
167 private componentRef = createRef<HTMLDivElement>();
168
169 private inputRef = createRef<HTMLInputElement>();
170
171 private searchInputRef = createRef<HTMLInputElement>();
172
173 private scrollContainerRef = createRef<HTMLDivElement>();
174
175 private activeOptionRef = createRef<HTMLDivElement>();
176
177 private keyListener: any;
178
179 componentWillReceiveProps(nextProps: IProps) {
180 if (nextProps.value && nextProps.value !== this.props.value) {
181 this.setState({
182 value: nextProps.value,
183 });
184 }
185 }
186
187 componentDidUpdate() {
188 const { open } = this.state;
189
190 if (this.searchInputRef && this.searchInputRef.current && open) {
191 this.searchInputRef.current.focus();
192 }
193 }
194
195 componentDidMount() {
196 if (this.inputRef && this.inputRef.current) {
197 const { data } = this.props;
198
199 if (data) {
200 Object.keys(data).map(
201 key => (this.inputRef.current!.dataset[key] = data[key]),
202 );
203 }
204 }
205
206 window.addEventListener('keydown', this.arrowKeysHandler.bind(this), false);
207 }
208
209 componentWillMount() {
210 const { value } = this.props;
211
212 if (this.componentRef && this.componentRef.current) {
213 this.componentRef.current.removeEventListener(
214 'keydown',
215 this.keyListener,
216 );
217 }
218
219 if (value) {
220 this.setState({
221 value,
222 });
223 }
224
225 this.setFilter();
226 }
227
228 componentWillUnmount() {
229 // eslint-disable-next-line unicorn/no-invalid-remove-event-listener
230 window.removeEventListener('keydown', this.arrowKeysHandler.bind(this));
231 }
232
233 setFilter(needle = '') {
234 const { options } = this.props;
235
236 let filteredOptions = {};
237 if (needle) {
238 Object.keys(options).map(key => {
239 if (
240 key.toLocaleLowerCase().startsWith(needle.toLocaleLowerCase()) ||
241 options[key]
242 .toLocaleLowerCase()
243 .startsWith(needle.toLocaleLowerCase())
244 ) {
245 Object.assign(filteredOptions, {
246 [`${key}`]: options[key],
247 });
248 }
249 });
250 } else {
251 filteredOptions = options;
252 }
253
254 this.setState({
255 needle,
256 options: filteredOptions,
257 selected: 0,
258 });
259 }
260
261 select(key: string) {
262 this.setState(() => ({
263 value: key,
264 open: false,
265 }));
266
267 this.setFilter();
268
269 if (this.props.onChange) {
270 this.props.onChange(key as any);
271 }
272 }
273
274 arrowKeysHandler(e: KeyboardEvent) {
275 const { selected, open, options } = this.state;
276
277 if (!open) return;
278
279 if (e.keyCode === 38 || e.keyCode === 40) {
280 e.preventDefault();
281 }
282
283 if (this.componentRef && this.componentRef.current) {
284 if (e.keyCode === 38 && selected > 0) {
285 this.setState((state: IState) => ({
286 selected: state.selected - 1,
287 }));
288 } else if (
289 e.keyCode === 40 &&
290 selected < Object.keys(options!).length - 1
291 ) {
292 this.setState((state: IState) => ({
293 selected: state.selected + 1,
294 }));
295 } else if (e.keyCode === 13) {
296 this.select(Object.keys(options!)[selected]);
297 }
298
299 if (
300 this.activeOptionRef &&
301 this.activeOptionRef.current &&
302 this.scrollContainerRef &&
303 this.scrollContainerRef.current
304 ) {
305 const containerTopOffset = this.scrollContainerRef.current.offsetTop;
306 const optionTopOffset = this.activeOptionRef.current.offsetTop;
307
308 const topOffset = optionTopOffset - containerTopOffset;
309
310 this.scrollContainerRef.current.scrollTop = topOffset - 35;
311 }
312 }
313
314 switch (e.keyCode) {
315 case 37:
316 case 39:
317 case 38:
318 case 40: // Arrow keys
319 case 32:
320 break; // Space
321 default:
322 break; // do not block other keys
323 }
324 }
325
326 render() {
327 const {
328 actionText,
329 classes,
330 className,
331 defaultValue,
332 disabled,
333 error,
334 id,
335 inputClassName,
336 name,
337 label,
338 showLabel,
339 showSearch,
340 onChange,
341 required,
342 } = this.props;
343
344 const { open, needle, value, selected, options } = this.state;
345
346 let selection = '';
347 if (!value && defaultValue && options![defaultValue]) {
348 selection = options![defaultValue];
349 } else if (value && options![value]) {
350 selection = options![value];
351 } else {
352 selection = actionText;
353 }
354
355 return (
356 <Wrapper className={className} identifier="franz-select">
357 <Label
358 title={label}
359 showLabel={showLabel}
360 htmlFor={id}
361 className={classes.label}
362 isRequired={required}
363 >
364 <div
365 className={classnames({
366 [`${classes.hasError}`]: error,
367 [`${classes.disabled}`]: disabled,
368 })}
369 ref={this.componentRef}
370 >
371 <button
372 type="button"
373 className={classnames({
374 [`${inputClassName}`]: inputClassName,
375 [`${classes.select}`]: true,
376 [`${classes.hasError}`]: error,
377 })}
378 onClick={
379 !disabled
380 ? () =>
381 this.setState((state: IState) => ({
382 open: !state.open,
383 }))
384 : () => {}
385 }
386 >
387 {selection}
388 <Icon
389 path={mdiArrowRightDropCircleOutline}
390 size={0.8}
391 className={classnames({
392 [`${classes.toggle}`]: true,
393 [`${classes.toggleOpened}`]: open,
394 })}
395 />
396 </button>
397 {showSearch && open && (
398 <div className={classes.searchContainer}>
399 <Icon path={mdiMagnify} size={0.8} />
400 <input
401 type="text"
402 value={needle}
403 onChange={e => this.setFilter(e.currentTarget.value)}
404 placeholder="Search"
405 className={classes.search}
406 ref={this.searchInputRef}
407 />
408 {needle && (
409 <button
410 type="button"
411 className={classes.clearNeedle}
412 onClick={() => this.setFilter()}
413 >
414 <Icon path={mdiCloseCircle} size={0.7} />
415 </button>
416 )}
417 </div>
418 )}
419 <div
420 className={classnames({
421 [`${classes.popup}`]: true,
422 [`${classes.open}`]: open,
423 })}
424 ref={this.scrollContainerRef}
425 >
426 {Object.keys(options!).map((key, i) => (
427 <div
428 key={key}
429 onClick={() => this.select(key)}
430 className={classnames({
431 [`${classes.option}`]: true,
432 [`${classes.selected}`]: options![key] === selection,
433 [`${classes.focused}`]: selected === i,
434 })}
435 onMouseOver={() => this.setState({ selected: i })}
436 ref={selected === i ? this.activeOptionRef : null}
437 >
438 {options![key]}
439 </div>
440 ))}
441 </div>
442 </div>
443 <input
444 className={classes.input}
445 id={id}
446 name={name}
447 type="hidden"
448 defaultValue={value}
449 onChange={onChange}
450 disabled={disabled}
451 ref={this.inputRef}
452 />
453 </Label>
454 {error && <Error message={error} />}
455 </Wrapper>
456 );
457 }
458}
459
460export const Select = injectStyle(styles)(SelectComponent);
diff --git a/src/components/ui/textarea/index.tsx b/src/components/ui/textarea/index.tsx
new file mode 100644
index 000000000..1b16698eb
--- /dev/null
+++ b/src/components/ui/textarea/index.tsx
@@ -0,0 +1,126 @@
1import classnames from 'classnames';
2import { Component, createRef, TextareaHTMLAttributes } from 'react';
3import injectSheet from 'react-jss';
4
5import { IFormField, IWithStyle } from '../typings/generic';
6
7import { Error } from '../error';
8import { Label } from '../label';
9import { Wrapper } from '../wrapper';
10
11import styles from './styles';
12
13interface IData {
14 [index: string]: string;
15}
16
17interface IProps
18 extends TextareaHTMLAttributes<HTMLTextAreaElement>,
19 IFormField,
20 IWithStyle {
21 focus?: boolean;
22 data: IData;
23 textareaClassName?: string;
24}
25
26class TextareaComponent extends Component<IProps> {
27 static defaultProps = {
28 focus: false,
29 onChange: () => {},
30 onBlur: () => {},
31 onFocus: () => {},
32 showLabel: true,
33 disabled: false,
34 rows: 5,
35 };
36
37 private textareaRef = createRef<HTMLTextAreaElement>();
38
39 componentDidMount() {
40 const { data } = this.props;
41
42 if (this.textareaRef && this.textareaRef.current && data) {
43 Object.keys(data).map(
44 key => (this.textareaRef.current!.dataset[key] = data[key]),
45 );
46 }
47 }
48
49 onChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
50 const { onChange } = this.props;
51
52 if (onChange) {
53 onChange(e);
54 }
55 }
56
57 render() {
58 const {
59 classes,
60 className,
61 disabled,
62 error,
63 id,
64 textareaClassName,
65 label,
66 showLabel,
67 value,
68 name,
69 placeholder,
70 spellCheck,
71 onBlur,
72 onFocus,
73 minLength,
74 maxLength,
75 required,
76 rows,
77 noMargin,
78 } = this.props;
79
80 return (
81 <Wrapper
82 className={className}
83 identifier="franz-textarea"
84 noMargin={noMargin}
85 >
86 <Label
87 title={label}
88 showLabel={showLabel}
89 htmlFor={id}
90 className={classes.label}
91 isRequired={required}
92 >
93 <div
94 className={classnames({
95 [`${textareaClassName}`]: textareaClassName,
96 [`${classes.wrapper}`]: true,
97 [`${classes.disabled}`]: disabled,
98 [`${classes.hasError}`]: error,
99 })}
100 >
101 <textarea
102 id={id}
103 name={name}
104 placeholder={placeholder}
105 spellCheck={spellCheck}
106 className={classes.textarea}
107 ref={this.textareaRef}
108 onChange={this.onChange.bind(this)}
109 onFocus={onFocus}
110 onBlur={onBlur}
111 disabled={disabled}
112 minLength={minLength}
113 maxLength={maxLength}
114 rows={rows}
115 >
116 {value}
117 </textarea>
118 </div>
119 </Label>
120 {error && <Error message={error} />}
121 </Wrapper>
122 );
123 }
124}
125
126export const Textarea = injectSheet(styles)(TextareaComponent);
diff --git a/src/components/ui/textarea/styles.ts b/src/components/ui/textarea/styles.ts
new file mode 100644
index 000000000..36fc2a82e
--- /dev/null
+++ b/src/components/ui/textarea/styles.ts
@@ -0,0 +1,54 @@
1import { Property } from 'csstype';
2
3import { Theme } from '../../../themes';
4
5export default (theme: Theme) => ({
6 label: {
7 '& > div': {
8 marginTop: 5,
9 },
10 },
11 disabled: {
12 opacity: theme.inputDisabledOpacity,
13 },
14 formModifier: {
15 background: 'none',
16 border: 0,
17 borderLeft: theme.inputBorder,
18 padding: '4px 20px 0',
19 outline: 'none',
20
21 '&:active': {
22 opacity: 0.5,
23 },
24
25 '& svg': {
26 fill: theme.inputModifierColor,
27 },
28 },
29 textarea: {
30 background: 'none',
31 border: 0,
32 fontSize: theme.uiFontSize,
33 outline: 'none',
34 padding: 8,
35 width: '100%',
36 color: theme.inputColor,
37
38 '&::placeholder': {
39 color: theme.inputPlaceholderColor,
40 },
41 },
42 wrapper: {
43 background: theme.inputBackground,
44 border: theme.inputBorder,
45 borderRadius: theme.borderRadiusSmall,
46 boxSizing: 'border-box' as Property.BoxSizing,
47 display: 'flex',
48 order: 1,
49 width: '100%',
50 },
51 hasError: {
52 borderColor: theme.brandDanger,
53 },
54});
diff --git a/src/components/ui/toggle/index.tsx b/src/components/ui/toggle/index.tsx
new file mode 100644
index 000000000..7b6ba147f
--- /dev/null
+++ b/src/components/ui/toggle/index.tsx
@@ -0,0 +1,125 @@
1import classnames from 'classnames';
2import { Property } from 'csstype';
3import { Component, InputHTMLAttributes } from 'react';
4import injectStyle from 'react-jss';
5
6import { Theme } from '../../../themes';
7import { IFormField, IWithStyle } from '../typings/generic';
8
9import { Error } from '../error';
10import { Label } from '../label';
11import { Wrapper } from '../wrapper';
12
13interface IProps
14 extends InputHTMLAttributes<HTMLInputElement>,
15 IFormField,
16 IWithStyle {
17 className?: string;
18}
19
20let buttonTransition: string = 'none';
21
22if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
23 buttonTransition = 'all .5s';
24}
25
26const styles = (theme: Theme) => ({
27 toggle: {
28 background: theme.toggleBackground,
29 borderRadius: theme.borderRadius,
30 height: theme.toggleHeight,
31 position: 'relative' as Property.Position,
32 width: theme.toggleWidth,
33 },
34 button: {
35 background: theme.toggleButton,
36 borderRadius: '100%',
37 boxShadow: '0 1px 4px rgba(0, 0, 0, .3)',
38 width: theme.toggleHeight - 2,
39 height: theme.toggleHeight - 2,
40 left: 1,
41 top: 1,
42 position: 'absolute' as Property.Position,
43 transition: buttonTransition,
44 },
45 buttonActive: {
46 background: theme.toggleButtonActive,
47 left: theme.toggleWidth - theme.toggleHeight + 1,
48 },
49 input: {
50 visibility: 'hidden' as any,
51 },
52 disabled: {
53 opacity: theme.inputDisabledOpacity,
54 },
55 toggleLabel: {
56 display: 'flex',
57 alignItems: 'center',
58
59 '& > span': {
60 order: 1,
61 marginLeft: 15,
62 },
63 },
64});
65
66class ToggleComponent extends Component<IProps> {
67 public static defaultProps = {
68 onChange: () => {},
69 showLabel: true,
70 disabled: false,
71 error: '',
72 };
73
74 render() {
75 const {
76 classes,
77 className,
78 disabled,
79 error,
80 id,
81 label,
82 showLabel,
83 checked,
84 value,
85 onChange,
86 } = this.props;
87
88 return (
89 <Wrapper className={className} identifier="franz-toggle">
90 <Label
91 title={label}
92 showLabel={showLabel}
93 htmlFor={id}
94 className={classes.toggleLabel}
95 >
96 <div
97 className={classnames({
98 [`${classes.toggle}`]: true,
99 [`${classes.disabled}`]: disabled,
100 })}
101 >
102 <div
103 className={classnames({
104 [`${classes.button}`]: true,
105 [`${classes.buttonActive}`]: checked,
106 })}
107 />
108 <input
109 className={classes.input}
110 id={id}
111 type="checkbox"
112 checked={checked}
113 value={value}
114 onChange={onChange}
115 disabled={disabled}
116 />
117 </div>
118 </Label>
119 {error && <Error message={error} />}
120 </Wrapper>
121 );
122 }
123}
124
125export const Toggle = injectStyle(styles)(ToggleComponent);
diff --git a/src/components/ui/typings/generic.ts b/src/components/ui/typings/generic.ts
new file mode 100644
index 000000000..65b996d59
--- /dev/null
+++ b/src/components/ui/typings/generic.ts
@@ -0,0 +1,19 @@
1import { Classes } from 'jss';
2
3import { Theme } from '../../../themes';
4
5export interface IFormField {
6 showLabel?: boolean;
7 label?: string;
8 error?: string;
9 required?: boolean;
10 noMargin?: boolean;
11}
12
13export interface IWithStyle {
14 classes: Classes;
15 theme: Theme;
16}
17
18export type Merge<M, N> = Omit<M, Extract<keyof M, keyof N>> & N;
19export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
diff --git a/src/components/ui/wrapper/index.tsx b/src/components/ui/wrapper/index.tsx
new file mode 100644
index 000000000..ffcd6fe0b
--- /dev/null
+++ b/src/components/ui/wrapper/index.tsx
@@ -0,0 +1,37 @@
1import classnames from 'classnames';
2import { Component, ReactNode } from 'react';
3import injectStyle from 'react-jss';
4import { IWithStyle } from '../typings/generic';
5
6interface IProps extends IWithStyle {
7 children: ReactNode;
8 className?: string;
9 identifier: string;
10 noMargin?: boolean;
11}
12
13const styles = {
14 container: {
15 marginBottom: (props: IProps) => (props.noMargin ? 0 : 20),
16 },
17};
18
19class WrapperComponent extends Component<IProps> {
20 render() {
21 const { children, classes, className, identifier } = this.props;
22
23 return (
24 <div
25 className={classnames({
26 [`${classes.container}`]: true,
27 [`${className}`]: className,
28 })}
29 data-type={identifier}
30 >
31 {children}
32 </div>
33 );
34 }
35}
36
37export const Wrapper = injectStyle(styles)(WrapperComponent);
diff --git a/src/components/util/ErrorBoundary/index.js b/src/components/util/ErrorBoundary/index.js
index 9c789e981..cddcd91c2 100644
--- a/src/components/util/ErrorBoundary/index.js
+++ b/src/components/util/ErrorBoundary/index.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import injectSheet from 'react-jss'; 3import injectSheet from 'react-jss';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
diff --git a/src/components/util/ErrorBoundary/styles.js b/src/components/util/ErrorBoundary/styles.js
index 51b36fdf3..0960546ff 100644
--- a/src/components/util/ErrorBoundary/styles.js
+++ b/src/components/util/ErrorBoundary/styles.js
@@ -1,4 +1,4 @@
1export default (theme) => ({ 1export default theme => ({
2 component: { 2 component: {
3 display: 'flex', 3 display: 'flex',
4 width: '100%', 4 width: '100%',
diff --git a/src/config.ts b/src/config.ts
index fe9145021..dfedbe6f5 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -2,7 +2,7 @@
2 2
3import ms from 'ms'; 3import ms from 'ms';
4 4
5import { DEFAULT_ACCENT_COLOR } from '@meetfranz/theme'; 5export const DEFAULT_ACCENT_COLOR = '#7266F0';
6 6
7export const CHECK_INTERVAL = ms('1h'); // How often should we perform checks 7export const CHECK_INTERVAL = ms('1h'); // How often should we perform checks
8 8
@@ -180,6 +180,7 @@ export const CUSTOM_WEBSITE_RECIPE_ID = 'franz-custom-website';
180export const DEFAULT_SERVICE_ORDER = 99; // something high enough that it gets added to the end of the already-added services on the left sidebar 180export const DEFAULT_SERVICE_ORDER = 99; // something high enough that it gets added to the end of the already-added services on the left sidebar
181 181
182export const DEFAULT_APP_SETTINGS = { 182export const DEFAULT_APP_SETTINGS = {
183 autoLaunchOnStart: false,
183 autoLaunchInBackground: false, 184 autoLaunchInBackground: false,
184 runInBackground: true, 185 runInBackground: true,
185 reloadAfterResume: true, 186 reloadAfterResume: true,
@@ -198,11 +199,11 @@ export const DEFAULT_APP_SETTINGS = {
198 spellcheckerLanguage: 'en-us', 199 spellcheckerLanguage: 'en-us',
199 darkMode: false, 200 darkMode: false,
200 splitMode: false, 201 splitMode: false,
201 locale: '',
202 fallbackLocale: 'en-US', 202 fallbackLocale: 'en-US',
203 beta: false, 203 beta: false,
204 isAppMuted: false, 204 isAppMuted: false,
205 enableGPUAcceleration: true, 205 enableGPUAcceleration: true,
206 enableGlobalHideShortcut: false,
206 207
207 // Ferdi specific options 208 // Ferdi specific options
208 server: LIVE_FERDI_API, 209 server: LIVE_FERDI_API,
@@ -233,4 +234,28 @@ export const DEFAULT_APP_SETTINGS = {
233 useVerticalStyle: false, 234 useVerticalStyle: false,
234 alwaysShowWorkspaces: false, 235 alwaysShowWorkspaces: false,
235 liftSingleInstanceLock: false, 236 liftSingleInstanceLock: false,
237 enableLongPressServiceHint: false,
238 proxyFeatureEnabled: false,
239 onlyShowFavoritesInUnreadCount: false
240};
241
242export const DEFAULT_SERVICE_SETTINGS = {
243 isEnabled: true,
244 isHibernationEnabled: false,
245 isWakeUpEnabled: true,
246 isNotificationEnabled: true,
247 isBadgeEnabled: true,
248 isMuted: false,
249 customIcon: false,
250 isDarkModeEnabled: false,
251 // Note: Do NOT change these default values. If they change, then the corresponding changes in the recipes needs to be done
252 hasDirectMessages: true,
253 hasIndirectMessages: false,
254 hasNotificationSound: false,
255 hasTeamId: false,
256 hasCustomUrl: false,
257 hasHostedOption: false,
258 allowFavoritesDelineationInUnreadCount: false,
259 disablewebsecurity: false,
260 autoHibernate: false,
236}; 261};
diff --git a/src/containers/auth/AuthLayoutContainer.js b/src/containers/auth/AuthLayoutContainer.js
index 7673ab9c7..aa36e3969 100644
--- a/src/containers/auth/AuthLayoutContainer.js
+++ b/src/containers/auth/AuthLayoutContainer.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4import { ThemeProvider } from 'react-jss'; 4import { ThemeProvider } from 'react-jss';
diff --git a/src/containers/auth/ChangeServerScreen.js b/src/containers/auth/ChangeServerScreen.js
index a8910e7b1..dcc913c39 100644
--- a/src/containers/auth/ChangeServerScreen.js
+++ b/src/containers/auth/ChangeServerScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4import { RouterStore } from 'mobx-react-router'; 4import { RouterStore } from 'mobx-react-router';
diff --git a/src/containers/auth/ImportScreen.js b/src/containers/auth/ImportScreen.js
index ce786bdb8..46e2d41f0 100644
--- a/src/containers/auth/ImportScreen.js
+++ b/src/containers/auth/ImportScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4import { RouterStore } from 'mobx-react-router'; 4import { RouterStore } from 'mobx-react-router';
diff --git a/src/containers/auth/InviteScreen.js b/src/containers/auth/InviteScreen.js
index d9d52a57c..e252242ae 100644
--- a/src/containers/auth/InviteScreen.js
+++ b/src/containers/auth/InviteScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4import Invite from '../../components/auth/Invite'; 4import Invite from '../../components/auth/Invite';
diff --git a/src/containers/auth/LockedScreen.js b/src/containers/auth/LockedScreen.js
index a49549731..945e41284 100644
--- a/src/containers/auth/LockedScreen.js
+++ b/src/containers/auth/LockedScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4import Locked from '../../components/auth/Locked'; 4import Locked from '../../components/auth/Locked';
diff --git a/src/containers/auth/LoginScreen.js b/src/containers/auth/LoginScreen.js
index cab73316b..3f8c67fa8 100644
--- a/src/containers/auth/LoginScreen.js
+++ b/src/containers/auth/LoginScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4import Login from '../../components/auth/Login'; 4import Login from '../../components/auth/Login';
diff --git a/src/containers/auth/PasswordScreen.js b/src/containers/auth/PasswordScreen.js
index 86a746b9b..836d4cc88 100644
--- a/src/containers/auth/PasswordScreen.js
+++ b/src/containers/auth/PasswordScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4import Password from '../../components/auth/Password'; 4import Password from '../../components/auth/Password';
diff --git a/src/containers/auth/SetupAssistantScreen.js b/src/containers/auth/SetupAssistantScreen.js
index efda2e9d2..8cdd95a88 100644
--- a/src/containers/auth/SetupAssistantScreen.js
+++ b/src/containers/auth/SetupAssistantScreen.js
@@ -1,5 +1,5 @@
1/* eslint-disable no-await-in-loop */ 1/* eslint-disable no-await-in-loop */
2import React, { Component } from 'react'; 2import { Component } from 'react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import { inject, observer } from 'mobx-react'; 4import { inject, observer } from 'mobx-react';
5 5
diff --git a/src/containers/auth/SignupScreen.js b/src/containers/auth/SignupScreen.js
index b20a7fd62..3b19f3c50 100644
--- a/src/containers/auth/SignupScreen.js
+++ b/src/containers/auth/SignupScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4 4
diff --git a/src/containers/auth/WelcomeScreen.js b/src/containers/auth/WelcomeScreen.js
index d169e5f0f..7a23d9ba9 100644
--- a/src/containers/auth/WelcomeScreen.js
+++ b/src/containers/auth/WelcomeScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4 4
diff --git a/src/containers/layout/AppLayoutContainer.js b/src/containers/layout/AppLayoutContainer.js
index 3e92b0610..0d566525d 100644
--- a/src/containers/layout/AppLayoutContainer.js
+++ b/src/containers/layout/AppLayoutContainer.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Children, Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4import { ThemeProvider } from 'react-jss'; 4import { ThemeProvider } from 'react-jss';
@@ -8,7 +8,6 @@ import RecipesStore from '../../stores/RecipesStore';
8import ServicesStore from '../../stores/ServicesStore'; 8import ServicesStore from '../../stores/ServicesStore';
9import FeaturesStore from '../../stores/FeaturesStore'; 9import FeaturesStore from '../../stores/FeaturesStore';
10import UIStore from '../../stores/UIStore'; 10import UIStore from '../../stores/UIStore';
11import NewsStore from '../../stores/NewsStore';
12import SettingsStore from '../../stores/SettingsStore'; 11import SettingsStore from '../../stores/SettingsStore';
13import UserStore from '../../stores/UserStore'; 12import UserStore from '../../stores/UserStore';
14import RequestStore from '../../stores/RequestStore'; 13import RequestStore from '../../stores/RequestStore';
@@ -38,7 +37,6 @@ class AppLayoutContainer extends Component {
38 features, 37 features,
39 services, 38 services,
40 ui, 39 ui,
41 news,
42 settings, 40 settings,
43 globalError, 41 globalError,
44 requests, 42 requests,
@@ -63,8 +61,6 @@ class AppLayoutContainer extends Component {
63 awake, 61 awake,
64 } = this.props.actions.service; 62 } = this.props.actions.service;
65 63
66 const { hide } = this.props.actions.news;
67
68 const { retryRequiredRequests } = this.props.actions.requests; 64 const { retryRequiredRequests } = this.props.actions.requests;
69 65
70 const { installUpdate, toggleMuteApp } = this.props.actions.app; 66 const { installUpdate, toggleMuteApp } = this.props.actions.app;
@@ -95,10 +91,11 @@ class AppLayoutContainer extends Component {
95 91
96 const workspacesDrawer = ( 92 const workspacesDrawer = (
97 <WorkspaceDrawer 93 <WorkspaceDrawer
94 // eslint-disable-next-line no-confusing-arrow
98 getServicesForWorkspace={workspace => 95 getServicesForWorkspace={workspace =>
99 (workspace 96 workspace
100 ? workspaceStore.getWorkspaceServices(workspace).map(s => s.name) 97 ? workspaceStore.getWorkspaceServices(workspace).map(s => s.name)
101 : services.all.map(s => s.name)) 98 : services.all.map(s => s.name)
102 } 99 }
103 /> 100 />
104 ); 101 );
@@ -158,8 +155,6 @@ class AppLayoutContainer extends Component {
158 sidebar={sidebar} 155 sidebar={sidebar}
159 workspacesDrawer={workspacesDrawer} 156 workspacesDrawer={workspacesDrawer}
160 services={servicesContainer} 157 services={servicesContainer}
161 news={news.latest}
162 removeNewsItem={hide}
163 reloadServicesAfterUpdate={() => window.location.reload()} 158 reloadServicesAfterUpdate={() => window.location.reload()}
164 installAppUpdate={installUpdate} 159 installAppUpdate={installUpdate}
165 globalError={globalError.error} 160 globalError={globalError.error}
@@ -168,7 +163,7 @@ class AppLayoutContainer extends Component {
168 retryRequiredRequests={retryRequiredRequests} 163 retryRequiredRequests={retryRequiredRequests}
169 areRequiredRequestsLoading={requests.areRequiredRequestsLoading} 164 areRequiredRequestsLoading={requests.areRequiredRequestsLoading}
170 > 165 >
171 {React.Children.count(children) > 0 ? children : null} 166 {Children.count(children) > 0 ? children : null}
172 </AppLayout> 167 </AppLayout>
173 </ThemeProvider> 168 </ThemeProvider>
174 ); 169 );
@@ -182,7 +177,6 @@ AppLayoutContainer.wrappedComponent.propTypes = {
182 recipes: PropTypes.instanceOf(RecipesStore).isRequired, 177 recipes: PropTypes.instanceOf(RecipesStore).isRequired,
183 app: PropTypes.instanceOf(AppStore).isRequired, 178 app: PropTypes.instanceOf(AppStore).isRequired,
184 ui: PropTypes.instanceOf(UIStore).isRequired, 179 ui: PropTypes.instanceOf(UIStore).isRequired,
185 news: PropTypes.instanceOf(NewsStore).isRequired,
186 settings: PropTypes.instanceOf(SettingsStore).isRequired, 180 settings: PropTypes.instanceOf(SettingsStore).isRequired,
187 user: PropTypes.instanceOf(UserStore).isRequired, 181 user: PropTypes.instanceOf(UserStore).isRequired,
188 requests: PropTypes.instanceOf(RequestStore).isRequired, 182 requests: PropTypes.instanceOf(RequestStore).isRequired,
@@ -191,7 +185,6 @@ AppLayoutContainer.wrappedComponent.propTypes = {
191 }).isRequired, 185 }).isRequired,
192 actions: PropTypes.shape({ 186 actions: PropTypes.shape({
193 service: PropTypes.instanceOf(ServicesStore).isRequired, 187 service: PropTypes.instanceOf(ServicesStore).isRequired,
194 news: PropTypes.instanceOf(NewsStore).isRequired,
195 ui: PropTypes.instanceOf(UIStore).isRequired, 188 ui: PropTypes.instanceOf(UIStore).isRequired,
196 app: PropTypes.instanceOf(AppStore).isRequired, 189 app: PropTypes.instanceOf(AppStore).isRequired,
197 requests: PropTypes.instanceOf(RequestStore).isRequired, 190 requests: PropTypes.instanceOf(RequestStore).isRequired,
diff --git a/src/containers/settings/AccountScreen.js b/src/containers/settings/AccountScreen.js
index 1515fc22b..f7c9b8164 100644
--- a/src/containers/settings/AccountScreen.js
+++ b/src/containers/settings/AccountScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4 4
diff --git a/src/containers/settings/EditServiceScreen.js b/src/containers/settings/EditServiceScreen.js
index dee7e7cff..b84c0d5bb 100644
--- a/src/containers/settings/EditServiceScreen.js
+++ b/src/containers/settings/EditServiceScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
@@ -23,6 +23,7 @@ import { config as proxyFeature } from '../../features/serviceProxy';
23import { SPELLCHECKER_LOCALES } from '../../i18n/languages'; 23import { SPELLCHECKER_LOCALES } from '../../i18n/languages';
24 24
25import globalMessages from '../../i18n/globalMessages'; 25import globalMessages from '../../i18n/globalMessages';
26import { DEFAULT_APP_SETTINGS, DEFAULT_SERVICE_SETTINGS } from '../../config';
26 27
27const messages = defineMessages({ 28const messages = defineMessages({
28 name: { 29 name: {
@@ -37,6 +38,10 @@ const messages = defineMessages({
37 id: 'settings.service.form.enableHibernation', 38 id: 'settings.service.form.enableHibernation',
38 defaultMessage: 'Enable hibernation', 39 defaultMessage: 'Enable hibernation',
39 }, 40 },
41 enableWakeUp: {
42 id: 'settings.service.form.enableWakeUp',
43 defaultMessage: 'Enable wake up',
44 },
40 enableNotification: { 45 enableNotification: {
41 id: 'settings.service.form.enableNotification', 46 id: 'settings.service.form.enableNotification',
42 defaultMessage: 'Enable notifications', 47 defaultMessage: 'Enable notifications',
@@ -171,7 +176,7 @@ class EditServiceScreen extends Component {
171 isEnabled: { 176 isEnabled: {
172 label: intl.formatMessage(messages.enableService), 177 label: intl.formatMessage(messages.enableService),
173 value: service.isEnabled, 178 value: service.isEnabled,
174 default: true, 179 default: DEFAULT_SERVICE_SETTINGS.isEnabled,
175 }, 180 },
176 isHibernationEnabled: { 181 isHibernationEnabled: {
177 label: intl.formatMessage(messages.enableHibernation), 182 label: intl.formatMessage(messages.enableHibernation),
@@ -179,22 +184,27 @@ class EditServiceScreen extends Component {
179 action !== 'edit' 184 action !== 'edit'
180 ? recipe.autoHibernate 185 ? recipe.autoHibernate
181 : service.isHibernationEnabled, 186 : service.isHibernationEnabled,
182 default: true, 187 default: DEFAULT_SERVICE_SETTINGS.isHibernationEnabled,
188 },
189 isWakeUpEnabled: {
190 label: intl.formatMessage(messages.enableWakeUp),
191 value: service.isWakeUpEnabled,
192 default: DEFAULT_SERVICE_SETTINGS.isWakeUpEnabled,
183 }, 193 },
184 isNotificationEnabled: { 194 isNotificationEnabled: {
185 label: intl.formatMessage(messages.enableNotification), 195 label: intl.formatMessage(messages.enableNotification),
186 value: service.isNotificationEnabled, 196 value: service.isNotificationEnabled,
187 default: true, 197 default: DEFAULT_SERVICE_SETTINGS.isNotificationEnabled,
188 }, 198 },
189 isBadgeEnabled: { 199 isBadgeEnabled: {
190 label: intl.formatMessage(messages.enableBadge), 200 label: intl.formatMessage(messages.enableBadge),
191 value: service.isBadgeEnabled, 201 value: service.isBadgeEnabled,
192 default: true, 202 default: DEFAULT_SERVICE_SETTINGS.isBadgeEnabled,
193 }, 203 },
194 isMuted: { 204 isMuted: {
195 label: intl.formatMessage(messages.enableAudio), 205 label: intl.formatMessage(messages.enableAudio),
196 value: !service.isMuted, 206 value: !service.isMuted,
197 default: true, 207 default: DEFAULT_SERVICE_SETTINGS.isMuted,
198 }, 208 },
199 customIcon: { 209 customIcon: {
200 label: intl.formatMessage(messages.icon), 210 label: intl.formatMessage(messages.icon),
@@ -288,7 +298,7 @@ class EditServiceScreen extends Component {
288 isIndirectMessageBadgeEnabled: { 298 isIndirectMessageBadgeEnabled: {
289 label: intl.formatMessage(messages.indirectMessages), 299 label: intl.formatMessage(messages.indirectMessages),
290 value: service.isIndirectMessageBadgeEnabled, 300 value: service.isIndirectMessageBadgeEnabled,
291 default: true, 301 default: DEFAULT_SERVICE_SETTINGS.hasIndirectMessages,
292 }, 302 },
293 }); 303 });
294 } 304 }
@@ -298,7 +308,7 @@ class EditServiceScreen extends Component {
298 onlyShowFavoritesInUnreadCount: { 308 onlyShowFavoritesInUnreadCount: {
299 label: intl.formatMessage(messages.onlyShowFavoritesInUnreadCount), 309 label: intl.formatMessage(messages.onlyShowFavoritesInUnreadCount),
300 value: service.onlyShowFavoritesInUnreadCount, 310 value: service.onlyShowFavoritesInUnreadCount,
301 default: false, 311 default: DEFAULT_APP_SETTINGS.onlyShowFavoritesInUnreadCount,
302 }, 312 },
303 }); 313 });
304 } 314 }
@@ -314,7 +324,7 @@ class EditServiceScreen extends Component {
314 isEnabled: { 324 isEnabled: {
315 label: intl.formatMessage(messages.enableProxy), 325 label: intl.formatMessage(messages.enableProxy),
316 value: serviceProxyConfig.isEnabled, 326 value: serviceProxyConfig.isEnabled,
317 default: false, 327 default: DEFAULT_APP_SETTINGS.proxyFeatureEnabled,
318 }, 328 },
319 host: { 329 host: {
320 label: intl.formatMessage(messages.proxyHost), 330 label: intl.formatMessage(messages.proxyHost),
diff --git a/src/containers/settings/EditSettingsScreen.js b/src/containers/settings/EditSettingsScreen.js
index 1b05644f9..de0714870 100644
--- a/src/containers/settings/EditSettingsScreen.js
+++ b/src/containers/settings/EditSettingsScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
@@ -177,6 +177,10 @@ const messages = defineMessages({
177 id: 'settings.app.form.iconSize', 177 id: 'settings.app.form.iconSize',
178 defaultMessage: 'Service icon size', 178 defaultMessage: 'Service icon size',
179 }, 179 },
180 enableLongPressServiceHint: {
181 id: 'settings.app.form.enableLongPressServiceHint',
182 defaultMessage: 'Enable service shortcut hint on long press',
183 },
180 useVerticalStyle: { 184 useVerticalStyle: {
181 id: 'settings.app.form.useVerticalStyle', 185 id: 'settings.app.form.useVerticalStyle',
182 defaultMessage: 'Use horizontal style', 186 defaultMessage: 'Use horizontal style',
@@ -209,6 +213,10 @@ const messages = defineMessages({
209 id: 'settings.app.form.enableGPUAcceleration', 213 id: 'settings.app.form.enableGPUAcceleration',
210 defaultMessage: 'Enable GPU Acceleration', 214 defaultMessage: 'Enable GPU Acceleration',
211 }, 215 },
216 enableGlobalHideShortcut: {
217 id: 'settings.app.form.enableGlobalHideShortcut',
218 defaultMessage: 'Enable Global shortcut to hide Ferdi',
219 },
212 beta: { 220 beta: {
213 id: 'settings.app.form.beta', 221 id: 'settings.app.form.beta',
214 defaultMessage: 'Include beta versions', 222 defaultMessage: 'Include beta versions',
@@ -289,6 +297,9 @@ class EditSettingsScreen extends Component {
289 scheduledDNDStart: settingsData.scheduledDNDStart, 297 scheduledDNDStart: settingsData.scheduledDNDStart,
290 scheduledDNDEnd: settingsData.scheduledDNDEnd, 298 scheduledDNDEnd: settingsData.scheduledDNDEnd,
291 enableGPUAcceleration: Boolean(settingsData.enableGPUAcceleration), 299 enableGPUAcceleration: Boolean(settingsData.enableGPUAcceleration),
300 enableGlobalHideShortcut: Boolean(
301 settingsData.enableGlobalHideShortcut,
302 ),
292 showDisabledServices: Boolean(settingsData.showDisabledServices), 303 showDisabledServices: Boolean(settingsData.showDisabledServices),
293 darkMode: Boolean(settingsData.darkMode), 304 darkMode: Boolean(settingsData.darkMode),
294 adaptableDarkMode: Boolean(settingsData.adaptableDarkMode), 305 adaptableDarkMode: Boolean(settingsData.adaptableDarkMode),
@@ -296,6 +307,9 @@ class EditSettingsScreen extends Component {
296 splitMode: Boolean(settingsData.splitMode), 307 splitMode: Boolean(settingsData.splitMode),
297 serviceRibbonWidth: Number(settingsData.serviceRibbonWidth), 308 serviceRibbonWidth: Number(settingsData.serviceRibbonWidth),
298 iconSize: Number(settingsData.iconSize), 309 iconSize: Number(settingsData.iconSize),
310 enableLongPressServiceHint: Boolean(
311 settingsData.enableLongPressServiceHint,
312 ),
299 useVerticalStyle: Boolean(settingsData.useVerticalStyle), 313 useVerticalStyle: Boolean(settingsData.useVerticalStyle),
300 alwaysShowWorkspaces: Boolean(settingsData.alwaysShowWorkspaces), 314 alwaysShowWorkspaces: Boolean(settingsData.alwaysShowWorkspaces),
301 accentColor: settingsData.accentColor, 315 accentColor: settingsData.accentColor,
@@ -503,7 +517,7 @@ class EditSettingsScreen extends Component {
503 lockingFeatureEnabled: { 517 lockingFeatureEnabled: {
504 label: intl.formatMessage(messages.enableLock), 518 label: intl.formatMessage(messages.enableLock),
505 value: settings.all.app.lockingFeatureEnabled || false, 519 value: settings.all.app.lockingFeatureEnabled || false,
506 default: false, 520 default: DEFAULT_APP_SETTINGS.lockingFeatureEnabled,
507 }, 521 },
508 lockedPassword: { 522 lockedPassword: {
509 label: intl.formatMessage(messages.lockPassword), 523 label: intl.formatMessage(messages.lockPassword),
@@ -525,7 +539,7 @@ class EditSettingsScreen extends Component {
525 scheduledDNDEnabled: { 539 scheduledDNDEnabled: {
526 label: intl.formatMessage(messages.scheduledDNDEnabled), 540 label: intl.formatMessage(messages.scheduledDNDEnabled),
527 value: settings.all.app.scheduledDNDEnabled || false, 541 value: settings.all.app.scheduledDNDEnabled || false,
528 default: false, 542 default: DEFAULT_APP_SETTINGS.scheduledDNDEnabled,
529 }, 543 },
530 scheduledDNDStart: { 544 scheduledDNDStart: {
531 label: intl.formatMessage(messages.scheduledDNDStart), 545 label: intl.formatMessage(messages.scheduledDNDStart),
@@ -603,6 +617,11 @@ class EditSettingsScreen extends Component {
603 default: DEFAULT_APP_SETTINGS.iconSize, 617 default: DEFAULT_APP_SETTINGS.iconSize,
604 options: iconSizes, 618 options: iconSizes,
605 }, 619 },
620 enableLongPressServiceHint: {
621 label: intl.formatMessage(messages.enableLongPressServiceHint),
622 value: settings.all.app.enableLongPressServiceHint,
623 default: DEFAULT_APP_SETTINGS.enableLongPressServiceHint,
624 },
606 useVerticalStyle: { 625 useVerticalStyle: {
607 label: intl.formatMessage(messages.useVerticalStyle), 626 label: intl.formatMessage(messages.useVerticalStyle),
608 value: settings.all.app.useVerticalStyle, 627 value: settings.all.app.useVerticalStyle,
@@ -623,6 +642,11 @@ class EditSettingsScreen extends Component {
623 value: settings.all.app.enableGPUAcceleration, 642 value: settings.all.app.enableGPUAcceleration,
624 default: DEFAULT_APP_SETTINGS.enableGPUAcceleration, 643 default: DEFAULT_APP_SETTINGS.enableGPUAcceleration,
625 }, 644 },
645 enableGlobalHideShortcut: {
646 label: intl.formatMessage(messages.enableGlobalHideShortcut),
647 value: settings.all.app.enableGlobalHideShortcut,
648 default: DEFAULT_APP_SETTINGS.enableGlobalHideShortcut,
649 },
626 locale: { 650 locale: {
627 label: intl.formatMessage(messages.language), 651 label: intl.formatMessage(messages.language),
628 value: app.locale, 652 value: app.locale,
diff --git a/src/containers/settings/EditUserScreen.js b/src/containers/settings/EditUserScreen.js
index ca1363c59..1dc0aa8e6 100644
--- a/src/containers/settings/EditUserScreen.js
+++ b/src/containers/settings/EditUserScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
diff --git a/src/containers/settings/InviteScreen.js b/src/containers/settings/InviteScreen.js
index bf393f42f..592b4b11c 100644
--- a/src/containers/settings/InviteScreen.js
+++ b/src/containers/settings/InviteScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4 4
diff --git a/src/containers/settings/RecipesScreen.js b/src/containers/settings/RecipesScreen.js
index 2e60ceefa..7f55e54c5 100644
--- a/src/containers/settings/RecipesScreen.js
+++ b/src/containers/settings/RecipesScreen.js
@@ -1,5 +1,5 @@
1import { readJsonSync } from 'fs-extra'; 1import { readJsonSync } from 'fs-extra';
2import React, { Component } from 'react'; 2import { Component } from 'react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import { autorun } from 'mobx'; 4import { autorun } from 'mobx';
5import { inject, observer } from 'mobx-react'; 5import { inject, observer } from 'mobx-react';
@@ -130,21 +130,21 @@ class RecipesScreen extends Component {
130 130
131 const allRecipes = this.state.needle 131 const allRecipes = this.state.needle
132 ? this.prepareRecipes([ 132 ? this.prepareRecipes([
133 // All search recipes from server 133 // All search recipes from server
134 ...recipePreviews.searchResults, 134 ...recipePreviews.searchResults,
135 // All search recipes from local recipes 135 // All search recipes from local recipes
136 ...this.createPreviews( 136 ...this.createPreviews(
137 this.customRecipes.filter( 137 this.customRecipes.filter(
138 service => 138 service =>
139 service.name 139 service.name
140 .toLowerCase() 140 .toLowerCase()
141 .includes(this.state.needle.toLowerCase()) || 141 .includes(this.state.needle.toLowerCase()) ||
142 (service.aliases || []).some(alias => 142 (service.aliases || []).some(alias =>
143 alias.toLowerCase().includes(this.state.needle.toLowerCase()), 143 alias.toLowerCase().includes(this.state.needle.toLowerCase()),
144 ), 144 ),
145 ),
145 ), 146 ),
146 ), 147 ]).sort(this._sortByName)
147 ]).sort(this._sortByName)
148 : recipeFilter; 148 : recipeFilter;
149 149
150 const customWebsiteRecipe = recipePreviews.all.find( 150 const customWebsiteRecipe = recipePreviews.all.find(
diff --git a/src/containers/settings/ServicesScreen.js b/src/containers/settings/ServicesScreen.js
index c9dfc68d0..a657b6e6c 100644
--- a/src/containers/settings/ServicesScreen.js
+++ b/src/containers/settings/ServicesScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4import { RouterStore } from 'mobx-react-router'; 4import { RouterStore } from 'mobx-react-router';
diff --git a/src/containers/settings/SettingsWindow.js b/src/containers/settings/SettingsWindow.js
index e03c4c1d2..35db3a434 100644
--- a/src/containers/settings/SettingsWindow.js
+++ b/src/containers/settings/SettingsWindow.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import ReactDOM from 'react-dom'; 2import ReactDOM from 'react-dom';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import { observer, inject } from 'mobx-react'; 4import { observer, inject } from 'mobx-react';
diff --git a/src/containers/settings/SupportScreen.js b/src/containers/settings/SupportScreen.js
index 646f672ce..d3600f8ea 100644
--- a/src/containers/settings/SupportScreen.js
+++ b/src/containers/settings/SupportScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import { inject } from 'mobx-react'; 2import { inject } from 'mobx-react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4 4
diff --git a/src/containers/settings/TeamScreen.js b/src/containers/settings/TeamScreen.js
index ea447469b..928262a59 100644
--- a/src/containers/settings/TeamScreen.js
+++ b/src/containers/settings/TeamScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4 4
diff --git a/src/electron-util.ts b/src/electron-util.ts
index f4b26cb10..3c395ab10 100644
--- a/src/electron-util.ts
+++ b/src/electron-util.ts
@@ -1,23 +1,28 @@
1// Enhanced from: https://github.com/dertieran/electron-util/blob/replace-remote/source/api.js 1// Enhanced from: https://github.com/dertieran/electron-util/blob/replace-remote/source/api.js
2 2
3import electron from 'electron'; 3import * as electron from 'electron';
4import { initialize } from '@electron/remote/main'; 4import { initialize } from '@electron/remote/main';
5 5
6export const initializeRemote = () => { 6export const initializeRemote = () => {
7 if (process.type !== 'browser') { 7 if (process.type !== 'browser') {
8 throw new Error('The remote api must be initialized from the main process.'); 8 throw new Error(
9 'The remote api must be initialized from the main process.',
10 );
9 } 11 }
10 12
11 initialize(); 13 initialize();
12}; 14};
13 15
14export const remote = new Proxy({}, { 16export const remote = new Proxy(
15 get: (_target, property) => { 17 {},
16 // eslint-disable-next-line global-require 18 {
17 const remote = require('@electron/remote'); 19 get: (_target, property) => {
18 return remote[property]; 20 // eslint-disable-next-line global-require
21 const remote = require('@electron/remote');
22 return remote[property];
23 },
19 }, 24 },
20}); 25);
21 26
22export const api = new Proxy(electron, { 27export const api = new Proxy(electron, {
23 get: (target, property) => { 28 get: (target, property) => {
diff --git a/src/electron/exception.ts b/src/electron/exception.ts
index 0065e2604..ada98d17b 100644
--- a/src/electron/exception.ts
+++ b/src/electron/exception.ts
@@ -1,4 +1,4 @@
1process.on('uncaughtException', (err) => { 1process.on('uncaughtException', err => {
2 // handle the error safely 2 // handle the error safely
3 console.error(err); 3 console.error(err);
4}); 4});
diff --git a/src/electron/ipc-api/appIndicator.ts b/src/electron/ipc-api/appIndicator.ts
index a51ed8161..bd5f6a68f 100644
--- a/src/electron/ipc-api/appIndicator.ts
+++ b/src/electron/ipc-api/appIndicator.ts
@@ -1,4 +1,4 @@
1import { app, ipcMain } from 'electron'; 1import { app, ipcMain, BrowserWindow, Tray } from 'electron';
2import { join } from 'path'; 2import { join } from 'path';
3import { autorun } from 'mobx'; 3import { autorun } from 'mobx';
4import { isMac, isWindows, isLinux } from '../../environment'; 4import { isMac, isWindows, isLinux } from '../../environment';
@@ -21,29 +21,38 @@ function getAsset(type: 'tray' | 'taskbar', asset: string) {
21 ); 21 );
22} 22}
23 23
24export default params => { 24export default (params: {
25 mainWindow: BrowserWindow;
26 settings: any;
27 trayIcon: Tray;
28}) => {
25 autorun(() => { 29 autorun(() => {
26 isTrayIconEnabled = params.settings.app.get('enableSystemTray'); 30 isTrayIconEnabled = params.settings.app.get('enableSystemTray');
27 31
28 if (!isTrayIconEnabled) { 32 if (!isTrayIconEnabled) {
33 // @ts-expect-error Property 'hide' does not exist on type 'Tray'.
29 params.trayIcon.hide(); 34 params.trayIcon.hide();
30 } else if (isTrayIconEnabled) { 35 } else if (isTrayIconEnabled) {
36 // @ts-expect-error Property 'show' does not exist on type 'Tray'.
31 params.trayIcon.show(); 37 params.trayIcon.show();
32 } 38 }
33 }); 39 });
34 40
35 ipcMain.on('updateAppIndicator', (_event, args) => { 41 ipcMain.on('updateAppIndicator', (_event, args) => {
36 // Flash TaskBar for windows, bounce Dock on Mac 42 // Flash TaskBar for windows, bounce Dock on Mac
37 if (!(app as any).mainWindow.isFocused() && params.settings.app.get('notifyTaskBarOnMessage')) { 43 if (
38 if (isWindows) { 44 !params.mainWindow.isFocused() &&
39 (app as any).mainWindow.flashFrame(true); 45 params.settings.app.get('notifyTaskBarOnMessage')
40 (app as any).mainWindow.once('focus', () => 46 ) {
41 (app as any).mainWindow.flashFrame(false), 47 if (isWindows) {
42 ); 48 params.mainWindow.flashFrame(true);
43 } else if (isMac) { 49 params.mainWindow.once('focus', () =>
44 app.dock.bounce('informational'); 50 params.mainWindow.flashFrame(false),
45 } 51 );
52 } else if (isMac) {
53 app.dock.bounce('informational');
46 } 54 }
55 }
47 56
48 // Update badge 57 // Update badge
49 if (isMac && typeof args.indicator === 'string') { 58 if (isMac && typeof args.indicator === 'string') {
@@ -57,6 +66,7 @@ export default params => {
57 if (isWindows) { 66 if (isWindows) {
58 if (typeof args.indicator === 'number' && args.indicator !== 0) { 67 if (typeof args.indicator === 'number' && args.indicator !== 0) {
59 params.mainWindow.setOverlayIcon( 68 params.mainWindow.setOverlayIcon(
69 // @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'NativeImage | null'.
60 getAsset( 70 getAsset(
61 'taskbar', 71 'taskbar',
62 `${INDICATOR_TASKBAR}-${ 72 `${INDICATOR_TASKBAR}-${
@@ -67,6 +77,7 @@ export default params => {
67 ); 77 );
68 } else if (typeof args.indicator === 'string') { 78 } else if (typeof args.indicator === 'string') {
69 params.mainWindow.setOverlayIcon( 79 params.mainWindow.setOverlayIcon(
80 // @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'NativeImage | null'.
70 getAsset('taskbar', `${INDICATOR_TASKBAR}-alert`), 81 getAsset('taskbar', `${INDICATOR_TASKBAR}-alert`),
71 '', 82 '',
72 ); 83 );
@@ -76,6 +87,7 @@ export default params => {
76 } 87 }
77 88
78 // Update Tray 89 // Update Tray
90 // @ts-expect-error Property 'setIndicator' does not exist on type 'Tray'.
79 params.trayIcon.setIndicator(args.indicator); 91 params.trayIcon.setIndicator(args.indicator);
80 }); 92 });
81}; 93};
diff --git a/src/electron/ipc-api/index.ts b/src/electron/ipc-api/index.ts
index f03f61517..1f69c04ee 100644
--- a/src/electron/ipc-api/index.ts
+++ b/src/electron/ipc-api/index.ts
@@ -12,7 +12,7 @@ import focusState from './focusState';
12export default (params: { 12export default (params: {
13 mainWindow: BrowserWindow; 13 mainWindow: BrowserWindow;
14 settings: any; 14 settings: any;
15 tray: Tray; 15 trayIcon: Tray;
16}) => { 16}) => {
17 settings(params); 17 settings(params);
18 sessionStorage(); 18 sessionStorage();
diff --git a/src/electron/ipc-api/localServer.ts b/src/electron/ipc-api/localServer.ts
index 7ee642101..04ddc976a 100644
--- a/src/electron/ipc-api/localServer.ts
+++ b/src/electron/ipc-api/localServer.ts
@@ -1,12 +1,12 @@
1import { ipcMain, BrowserWindow } from 'electron'; 1import { ipcMain, BrowserWindow } from 'electron';
2import net from 'net'; 2import { createServer } from 'net';
3import { LOCAL_HOSTNAME, LOCAL_PORT } from '../../config'; 3import { LOCAL_HOSTNAME, LOCAL_PORT } from '../../config';
4import { userDataPath } from '../../environment-remote'; 4import { userDataPath } from '../../environment-remote';
5import { server } from '../../internal-server/start'; 5import { server } from '../../internal-server/start';
6 6
7const portInUse = (port: number): Promise<boolean> => 7const portInUse = (port: number): Promise<boolean> =>
8 new Promise(resolve => { 8 new Promise(resolve => {
9 const server = net.createServer(socket => { 9 const server = createServer(socket => {
10 socket.write('Echo server\r\n'); 10 socket.write('Echo server\r\n');
11 socket.pipe(socket); 11 socket.pipe(socket);
12 }); 12 });
diff --git a/src/electron/ipc-api/sessionStorage.ts b/src/electron/ipc-api/sessionStorage.ts
index 3eda568a1..1ff0a51ea 100644
--- a/src/electron/ipc-api/sessionStorage.ts
+++ b/src/electron/ipc-api/sessionStorage.ts
@@ -6,7 +6,11 @@ const debug = require('debug')('Ferdi:ipcApi:sessionStorage');
6 6
7function deduceSession(serviceId: string | undefined | null): Session { 7function deduceSession(serviceId: string | undefined | null): Session {
8 if (serviceId) { 8 if (serviceId) {
9 return session.fromPartition(serviceId === TODOS_PARTITION_ID ? TODOS_PARTITION_ID : `persist:service-${serviceId}`); 9 return session.fromPartition(
10 serviceId === TODOS_PARTITION_ID
11 ? TODOS_PARTITION_ID
12 : `persist:service-${serviceId}`,
13 );
10 } 14 }
11 return session.defaultSession; 15 return session.defaultSession;
12} 16}
diff --git a/src/enforce-macos-app-location.ts b/src/enforce-macos-app-location.ts
index 0f858013d..0e6bf9ecc 100644
--- a/src/enforce-macos-app-location.ts
+++ b/src/enforce-macos-app-location.ts
@@ -12,11 +12,9 @@ export function enforceMacOSAppLocation() {
12 const clickedButtonIndex = api.dialog.showMessageBoxSync({ 12 const clickedButtonIndex = api.dialog.showMessageBoxSync({
13 type: 'error', 13 type: 'error',
14 message: 'Move to Applications folder?', 14 message: 'Move to Applications folder?',
15 detail: 'Ferdi must live in the Applications folder to be able to run correctly.', 15 detail:
16 buttons: [ 16 'Ferdi must live in the Applications folder to be able to run correctly.',
17 'Move to Applications folder', 17 buttons: ['Move to Applications folder', 'Quit Ferdi'],
18 'Quit Ferdi',
19 ],
20 defaultId: 0, 18 defaultId: 0,
21 cancelId: 1, 19 cancelId: 1,
22 }); 20 });
@@ -28,13 +26,13 @@ export function enforceMacOSAppLocation() {
28 26
29 api.app.moveToApplicationsFolder({ 27 api.app.moveToApplicationsFolder({
30 conflictHandler: conflict => { 28 conflictHandler: conflict => {
31 if (conflict === 'existsAndRunning') { // Can't replace the active version of the app 29 if (conflict === 'existsAndRunning') {
30 // Can't replace the active version of the app
32 api.dialog.showMessageBoxSync({ 31 api.dialog.showMessageBoxSync({
33 type: 'error', 32 type: 'error',
34 message: 'Another version of Ferdi is currently running. Quit it, then launch this version of the app again.', 33 message:
35 buttons: [ 34 'Another version of Ferdi is currently running. Quit it, then launch this version of the app again.',
36 'OK', 35 buttons: ['OK'],
37 ],
38 }); 36 });
39 37
40 api.app.quit(); 38 api.app.quit();
diff --git a/src/environment-remote.ts b/src/environment-remote.ts
index 192510f37..c87e89772 100644
--- a/src/environment-remote.ts
+++ b/src/environment-remote.ts
@@ -14,7 +14,13 @@ import {
14 LOCAL_TODOS_FRONTEND_URL, 14 LOCAL_TODOS_FRONTEND_URL,
15 PRODUCTION_TODOS_FRONTEND_URL, 15 PRODUCTION_TODOS_FRONTEND_URL,
16} from './config'; 16} from './config';
17import { chromeVersion, electronVersion, isWindows, nodeVersion, osArch } from './environment'; 17import {
18 chromeVersion,
19 electronVersion,
20 isWindows,
21 nodeVersion,
22 osArch,
23} from './environment';
18 24
19// @ts-expect-error Cannot find module './buildInfo.json' or its corresponding type declarations. 25// @ts-expect-error Cannot find module './buildInfo.json' or its corresponding type declarations.
20import * as buildInfo from './buildInfo.json'; 26import * as buildInfo from './buildInfo.json';
@@ -28,14 +34,20 @@ if (process.env.FERDI_APPDATA_DIR != null) {
28 app.setPath('appData', process.env.FERDI_APPDATA_DIR); 34 app.setPath('appData', process.env.FERDI_APPDATA_DIR);
29 app.setPath('userData', app.getPath('appData')); 35 app.setPath('userData', app.getPath('appData'));
30} else if (process.env.PORTABLE_EXECUTABLE_DIR != null) { 36} else if (process.env.PORTABLE_EXECUTABLE_DIR != null) {
31 app.setPath('appData', join(process.env.PORTABLE_EXECUTABLE_DIR, `${app.name}AppData`)); 37 app.setPath(
38 'appData',
39 join(process.env.PORTABLE_EXECUTABLE_DIR, `${app.name}AppData`),
40 );
32 app.setPath('userData', join(app.getPath('appData'), `${app.name}AppData`)); 41 app.setPath('userData', join(app.getPath('appData'), `${app.name}AppData`));
33} else if (isWindows && process.env.APPDATA != null) { 42} else if (isWindows && process.env.APPDATA != null) {
34 app.setPath('appData', process.env.APPDATA); 43 app.setPath('appData', process.env.APPDATA);
35 app.setPath('userData', join(app.getPath('appData'), app.name)); 44 app.setPath('userData', join(app.getPath('appData'), app.name));
36} 45}
37 46
38export const isDevMode = process.env.ELECTRON_IS_DEV !== undefined ? Number.parseInt(process.env.ELECTRON_IS_DEV, 10) === 1 : !app.isPackaged; 47export const isDevMode =
48 process.env.ELECTRON_IS_DEV !== undefined
49 ? Number.parseInt(process.env.ELECTRON_IS_DEV, 10) === 1
50 : !app.isPackaged;
39if (isDevMode) { 51if (isDevMode) {
40 app.setPath('userData', join(app.getPath('appData'), `${app.name}Dev`)); 52 app.setPath('userData', join(app.getPath('appData'), `${app.name}Dev`));
41} 53}
diff --git a/src/environment.ts b/src/environment.ts
index ce2a77e7a..9b727a607 100644
--- a/src/environment.ts
+++ b/src/environment.ts
@@ -1,6 +1,6 @@
1// Note: This file has now become devoid of all references to values deduced from the remote process - all those now live in the `environment-remote.js` file 1// Note: This file has now become devoid of all references to values deduced from the remote process - all those now live in the `environment-remote.js` file
2 2
3import os from 'os'; 3import { arch, release } from 'os';
4 4
5export const isMac = process.platform === 'darwin'; 5export const isMac = process.platform === 'darwin';
6export const isWindows = process.platform === 'win32'; 6export const isWindows = process.platform === 'win32';
@@ -10,9 +10,8 @@ export const electronVersion = process.versions.electron;
10export const chromeVersion = process.versions.chrome; 10export const chromeVersion = process.versions.chrome;
11export const nodeVersion = process.versions.node; 11export const nodeVersion = process.versions.node;
12 12
13export const osPlatform = os.platform(); 13export const osArch = arch();
14export const osArch = os.arch(); 14export const osRelease = release();
15export const osRelease = os.release();
16export const is64Bit = osArch.match(/64/); 15export const is64Bit = osArch.match(/64/);
17 16
18// for accelerator, show the shortform that electron/OS understands 17// for accelerator, show the shortform that electron/OS understands
diff --git a/src/features/basicAuth/Component.js b/src/features/basicAuth/Component.js
index 3cf937f98..652233e55 100644
--- a/src/features/basicAuth/Component.js
+++ b/src/features/basicAuth/Component.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import injectSheet from 'react-jss'; 3import injectSheet from 'react-jss';
4import { observer } from 'mobx-react'; 4import { observer } from 'mobx-react';
diff --git a/src/features/basicAuth/index.js b/src/features/basicAuth/index.ts
index e43d51d15..149ab6c19 100644
--- a/src/features/basicAuth/index.js
+++ b/src/features/basicAuth/index.ts
@@ -1,4 +1,4 @@
1import { ipcRenderer } from 'electron'; 1import { AuthInfo, BrowserWindow, ipcRenderer } from 'electron';
2 2
3import BasicAuthComponent from './Component'; 3import BasicAuthComponent from './Component';
4 4
@@ -11,7 +11,7 @@ const state = ModalState;
11export default function initialize() { 11export default function initialize() {
12 debug('Initialize basicAuth feature'); 12 debug('Initialize basicAuth feature');
13 13
14 window.ferdi.features.basicAuth = { 14 window['ferdi'].features.basicAuth = {
15 state, 15 state,
16 }; 16 };
17 17
@@ -23,7 +23,7 @@ export default function initialize() {
23 }); 23 });
24} 24}
25 25
26export function mainIpcHandler(mainWindow, authInfo) { 26export function mainIpcHandler(mainWindow: BrowserWindow, authInfo: AuthInfo) {
27 debug('Sending basic auth call', authInfo); 27 debug('Sending basic auth call', authInfo);
28 28
29 mainWindow.webContents.send('feature:basic-auth-request', { 29 mainWindow.webContents.send('feature:basic-auth-request', {
diff --git a/src/features/nightlyBuilds/Component.js b/src/features/nightlyBuilds/Component.js
index 814d529d9..64f782c8f 100644
--- a/src/features/nightlyBuilds/Component.js
+++ b/src/features/nightlyBuilds/Component.js
@@ -1,9 +1,9 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, inject } from 'mobx-react'; 3import { observer, inject } from 'mobx-react';
4import injectSheet from 'react-jss'; 4import injectSheet from 'react-jss';
5import { defineMessages, injectIntl } from 'react-intl'; 5import { defineMessages, injectIntl } from 'react-intl';
6import { H1 } from '@meetfranz/ui'; 6import { H1 } from '../../components/ui/headline';
7 7
8import Modal from '../../components/ui/Modal'; 8import Modal from '../../components/ui/Modal';
9import Button from '../../components/ui/Button'; 9import Button from '../../components/ui/Button';
diff --git a/src/features/nightlyBuilds/index.js b/src/features/nightlyBuilds/index.ts
index 89bcb5cb3..14afbaf1b 100644
--- a/src/features/nightlyBuilds/index.js
+++ b/src/features/nightlyBuilds/index.ts
@@ -14,26 +14,26 @@ export default function initialize() {
14 } 14 }
15 15
16 function toggleFeature() { 16 function toggleFeature() {
17 if (window.ferdi.stores.settings.app.nightly) { 17 if (window['ferdi'].stores.settings.app.nightly) {
18 window.ferdi.actions.settings.update({ 18 window['ferdi'].actions.settings.update({
19 type: 'app', 19 type: 'app',
20 data: { 20 data: {
21 nightly: false, 21 nightly: false,
22 }, 22 },
23 }); 23 });
24 window.ferdi.actions.user.update({ 24 window['ferdi'].actions.user.update({
25 userData: { 25 userData: {
26 nightly: false, 26 nightly: false,
27 }, 27 },
28 }); 28 });
29 } else { 29 } else {
30 // We need to close the settings, otherwise the modal will be drawn under the settings window 30 // We need to close the settings, otherwise the modal will be drawn under the settings window
31 window.ferdi.actions.ui.closeSettings(); 31 window['ferdi'].actions.ui.closeSettings();
32 showModal(); 32 showModal();
33 } 33 }
34 } 34 }
35 35
36 window.ferdi.features.nightlyBuilds = { 36 window['ferdi'].features.nightlyBuilds = {
37 state, 37 state,
38 showModal, 38 showModal,
39 toggleFeature, 39 toggleFeature,
diff --git a/src/features/publishDebugInfo/Component.js b/src/features/publishDebugInfo/Component.js
index 5b5036752..30bdc13b6 100644
--- a/src/features/publishDebugInfo/Component.js
+++ b/src/features/publishDebugInfo/Component.js
@@ -1,10 +1,11 @@
1import { H1 } from '@meetfranz/ui';
2import { inject, observer } from 'mobx-react'; 1import { inject, observer } from 'mobx-react';
3import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
4import React, { Component } from 'react'; 3import { Component } from 'react';
5import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
6import injectSheet from 'react-jss'; 5import injectSheet from 'react-jss';
7import { state as ModalState } from './store'; 6import { state as ModalState } from './store';
7
8import { H1 } from '../../components/ui/headline';
8import { sendAuthRequest } from '../../api/utils/auth'; 9import { sendAuthRequest } from '../../api/utils/auth';
9import Button from '../../components/ui/Button'; 10import Button from '../../components/ui/Button';
10import Input from '../../components/ui/Input'; 11import Input from '../../components/ui/Input';
diff --git a/src/features/quickSwitch/Component.js b/src/features/quickSwitch/Component.js
index f21db0ebd..d5cb9179f 100644
--- a/src/features/quickSwitch/Component.js
+++ b/src/features/quickSwitch/Component.js
@@ -1,14 +1,14 @@
1import React, { Component, createRef } from 'react'; 1import { Component, createRef } from 'react';
2import { getCurrentWindow } from '@electron/remote'; 2import { getCurrentWindow } from '@electron/remote';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import { observer, inject } from 'mobx-react'; 4import { observer, inject } from 'mobx-react';
5import { reaction } from 'mobx'; 5import { reaction } from 'mobx';
6import injectSheet from 'react-jss'; 6import injectSheet from 'react-jss';
7import { defineMessages, injectIntl } from 'react-intl'; 7import { defineMessages, injectIntl } from 'react-intl';
8import { Input } from '@meetfranz/forms';
9import { H1 } from '@meetfranz/ui';
10
11import { compact, invoke } from 'lodash'; 8import { compact, invoke } from 'lodash';
9
10import { Input } from '../../components/ui/input/index';
11import { H1 } from '../../components/ui/headline';
12import Modal from '../../components/ui/Modal'; 12import Modal from '../../components/ui/Modal';
13import { state as ModalState } from './store'; 13import { state as ModalState } from './store';
14import ServicesStore from '../../stores/ServicesStore'; 14import ServicesStore from '../../stores/ServicesStore';
diff --git a/src/features/quickSwitch/index.js b/src/features/quickSwitch/index.ts
index a16017219..e9cc36b2a 100644
--- a/src/features/quickSwitch/index.js
+++ b/src/features/quickSwitch/index.ts
@@ -12,7 +12,7 @@ export default function initialize() {
12 state.isModalVisible = true; 12 state.isModalVisible = true;
13 } 13 }
14 14
15 window.ferdi.features.quickSwitch = { 15 window['ferdi'].features.quickSwitch = {
16 state, 16 state,
17 showModal, 17 showModal,
18 }; 18 };
diff --git a/src/features/settingsWS/actions.ts b/src/features/settingsWS/actions.ts
index 631670c8a..03a398eb5 100755
--- a/src/features/settingsWS/actions.ts
+++ b/src/features/settingsWS/actions.ts
@@ -1,10 +1,13 @@
1import PropTypes from 'prop-types'; 1import PropTypes from 'prop-types';
2import { createActionsFromDefinitions } from '../../actions/lib/actions'; 2import { createActionsFromDefinitions } from '../../actions/lib/actions';
3 3
4export const settingsWSActions = createActionsFromDefinitions({ 4export const settingsWSActions = createActionsFromDefinitions(
5 greet: { 5 {
6 name: PropTypes.string.isRequired, 6 greet: {
7 name: PropTypes.string.isRequired,
8 },
7 }, 9 },
8}, PropTypes.checkPropTypes); 10 PropTypes.checkPropTypes,
11);
9 12
10export default settingsWSActions; 13export default settingsWSActions;
diff --git a/src/features/todos/components/TodosWebview.js b/src/features/todos/components/TodosWebview.js
index 2dc30cdf2..1d423544b 100644
--- a/src/features/todos/components/TodosWebview.js
+++ b/src/features/todos/components/TodosWebview.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss'; 4import injectSheet from 'react-jss';
@@ -7,14 +7,15 @@ import classnames from 'classnames';
7 7
8import { TODOS_PARTITION_ID } from '../../../config'; 8import { TODOS_PARTITION_ID } from '../../../config';
9 9
10const styles = (theme) => ({ 10const styles = theme => ({
11 root: { 11 root: {
12 background: theme.colorBackground, 12 background: theme.colorBackground,
13 position: 'relative', 13 position: 'relative',
14 borderLeft: [1, 'solid', theme.todos.todosLayer.borderLeftColor], 14 borderLeft: [1, 'solid', theme.todos.todosLayer.borderLeftColor],
15 zIndex: 300, 15 zIndex: 300,
16 16
17 transform: ({ isVisible, width, isTodosServiceActive }) => `translateX(${isVisible || isTodosServiceActive ? 0 : width}px)`, 17 transform: ({ isVisible, width, isTodosServiceActive }) =>
18 `translateX(${isVisible || isTodosServiceActive ? 0 : width}px)`,
18 19
19 '& webview': { 20 '& webview': {
20 height: '100%', 21 height: '100%',
@@ -79,7 +80,7 @@ class TodosWebview extends Component {
79 this.node.addEventListener('mouseleave', this.stopResize.bind(this)); 80 this.node.addEventListener('mouseleave', this.stopResize.bind(this));
80 } 81 }
81 82
82 startResize = (event) => { 83 startResize = event => {
83 this.setState({ 84 this.setState({
84 isDragging: true, 85 isDragging: true,
85 initialPos: event.clientX, 86 initialPos: event.clientX,
@@ -126,7 +127,7 @@ class TodosWebview extends Component {
126 startListeningToIpcMessages() { 127 startListeningToIpcMessages() {
127 const { handleClientMessage } = this.props; 128 const { handleClientMessage } = this.props;
128 if (!this.webview) return; 129 if (!this.webview) return;
129 this.webview.addEventListener('ipc-message', (e) => { 130 this.webview.addEventListener('ipc-message', e => {
130 // console.log(e); 131 // console.log(e);
131 handleClientMessage({ channel: e.channel, message: e.args[0] }); 132 handleClientMessage({ channel: e.channel, message: e.args[0] });
132 }); 133 });
@@ -159,7 +160,7 @@ class TodosWebview extends Component {
159 })} 160 })}
160 style={{ width: displayedWidth }} 161 style={{ width: displayedWidth }}
161 onMouseUp={() => this.stopResize()} 162 onMouseUp={() => this.stopResize()}
162 ref={(node) => { 163 ref={node => {
163 this.node = node; 164 this.node = node;
164 }} 165 }}
165 id="todos-panel" 166 id="todos-panel"
@@ -170,7 +171,7 @@ class TodosWebview extends Component {
170 left: delta, 171 left: delta,
171 ...(isDragging ? { width: 600, marginLeft: -200 } : {}), 172 ...(isDragging ? { width: 600, marginLeft: -200 } : {}),
172 }} // This hack is required as resizing with webviews beneath behaves quite bad 173 }} // This hack is required as resizing with webviews beneath behaves quite bad
173 onMouseDown={(e) => this.startResize(e)} 174 onMouseDown={e => this.startResize(e)}
174 /> 175 />
175 {isDragging && ( 176 {isDragging && (
176 <div 177 <div
@@ -188,7 +189,7 @@ class TodosWebview extends Component {
188 }} 189 }}
189 partition={TODOS_PARTITION_ID} 190 partition={TODOS_PARTITION_ID}
190 preload="./features/todos/preload.js" 191 preload="./features/todos/preload.js"
191 ref={(webview) => { 192 ref={webview => {
192 this.webview = webview ? webview.view : null; 193 this.webview = webview ? webview.view : null;
193 }} 194 }}
194 useragent={userAgent} 195 useragent={userAgent}
diff --git a/src/features/todos/containers/TodosScreen.js b/src/features/todos/containers/TodosScreen.js
index d05e24e56..536810d2d 100644
--- a/src/features/todos/containers/TodosScreen.js
+++ b/src/features/todos/containers/TodosScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import { observer, inject } from 'mobx-react'; 2import { observer, inject } from 'mobx-react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4 4
@@ -10,24 +10,31 @@ import { TODOS_MIN_WIDTH } from '../../../config';
10import { todoActions } from '../actions'; 10import { todoActions } from '../actions';
11import ServicesStore from '../../../stores/ServicesStore'; 11import ServicesStore from '../../../stores/ServicesStore';
12 12
13@inject('stores', 'actions') @observer 13@inject('stores', 'actions')
14@observer
14class TodosScreen extends Component { 15class TodosScreen extends Component {
15 render() { 16 render() {
16 if (!todosStore || !todosStore.isFeatureActive || todosStore.isTodosPanelForceHidden) { 17 if (
18 !todosStore ||
19 !todosStore.isFeatureActive ||
20 todosStore.isTodosPanelForceHidden
21 ) {
17 return null; 22 return null;
18 } 23 }
19 24
20 return ( 25 return (
21 <ErrorBoundary> 26 <ErrorBoundary>
22 <TodosWebview 27 <TodosWebview
23 isTodosServiceActive={this.props.stores.services.isTodosServiceActive || false} 28 isTodosServiceActive={
29 this.props.stores.services.isTodosServiceActive || false
30 }
24 isVisible={todosStore.isTodosPanelVisible} 31 isVisible={todosStore.isTodosPanelVisible}
25 togglePanel={todoActions.toggleTodosPanel} 32 togglePanel={todoActions.toggleTodosPanel}
26 handleClientMessage={todoActions.handleClientMessage} 33 handleClientMessage={todoActions.handleClientMessage}
27 setTodosWebview={(webview) => todoActions.setTodosWebview({ webview })} 34 setTodosWebview={webview => todoActions.setTodosWebview({ webview })}
28 width={todosStore.width} 35 width={todosStore.width}
29 minWidth={TODOS_MIN_WIDTH} 36 minWidth={TODOS_MIN_WIDTH}
30 resize={(width) => todoActions.resize({ width })} 37 resize={width => todoActions.resize({ width })}
31 userAgent={todosStore.userAgent} 38 userAgent={todosStore.userAgent}
32 todoUrl={todosStore.todoUrl} 39 todoUrl={todosStore.todoUrl}
33 isTodoUrlValid={todosStore.isTodoUrlValid} 40 isTodoUrlValid={todosStore.isTodoUrlValid}
diff --git a/src/features/todos/preload.js b/src/features/todos/preload.ts
index 3b86ddbc5..31d473051 100644
--- a/src/features/todos/preload.js
+++ b/src/features/todos/preload.ts
@@ -14,7 +14,7 @@ let hostMessageListener = ({ action }) => {
14 } 14 }
15}; 15};
16 16
17window.ferdi = { 17window['ferdi'] = {
18 onInitialize(ipcHostMessageListener) { 18 onInitialize(ipcHostMessageListener) {
19 hostMessageListener = ipcHostMessageListener; 19 hostMessageListener = ipcHostMessageListener;
20 ipcRenderer.sendToHost(IPC.TODOS_CLIENT_CHANNEL, { 20 ipcRenderer.sendToHost(IPC.TODOS_CLIENT_CHANNEL, {
diff --git a/src/features/todos/store.js b/src/features/todos/store.js
index ec06c279d..010a029ff 100644
--- a/src/features/todos/store.js
+++ b/src/features/todos/store.js
@@ -1,7 +1,7 @@
1import { ThemeType } from '@meetfranz/theme';
2import { computed, action, observable } from 'mobx'; 1import { computed, action, observable } from 'mobx';
3import localStorage from 'mobx-localstorage'; 2import localStorage from 'mobx-localstorage';
4 3
4import { ThemeType } from '../../themes';
5import { todoActions } from './actions'; 5import { todoActions } from './actions';
6import { 6import {
7 CUSTOM_TODO_SERVICE, 7 CUSTOM_TODO_SERVICE,
diff --git a/src/features/utils/ActionBinding.ts b/src/features/utils/ActionBinding.ts
index 787166d44..16308fae4 100644
--- a/src/features/utils/ActionBinding.ts
+++ b/src/features/utils/ActionBinding.ts
@@ -24,6 +24,5 @@ export default class ActionBinding {
24 } 24 }
25} 25}
26 26
27export const createActionBindings = (actions) => ( 27export const createActionBindings = actions =>
28 actions.map((a) => new ActionBinding(a)) 28 actions.map(a => new ActionBinding(a));
29);
diff --git a/src/features/utils/FeatureStore.test.js b/src/features/utils/FeatureStore.test.js
index 92308bf52..1995431bd 100644
--- a/src/features/utils/FeatureStore.test.js
+++ b/src/features/utils/FeatureStore.test.js
@@ -5,9 +5,12 @@ import { createActionsFromDefinitions } from '../../actions/lib/actions';
5import { createActionBindings } from './ActionBinding'; 5import { createActionBindings } from './ActionBinding';
6import { createReactions } from '../../stores/lib/Reaction'; 6import { createReactions } from '../../stores/lib/Reaction';
7 7
8const actions = createActionsFromDefinitions({ 8const actions = createActionsFromDefinitions(
9 countUp: {}, 9 {
10}, PropTypes.checkPropTypes); 10 countUp: {},
11 },
12 PropTypes.checkPropTypes,
13);
11 14
12class TestFeatureStore extends FeatureStore { 15class TestFeatureStore extends FeatureStore {
13 @observable count = 0; 16 @observable count = 0;
@@ -15,12 +18,10 @@ class TestFeatureStore extends FeatureStore {
15 reactionInvokedCount = 0; 18 reactionInvokedCount = 0;
16 19
17 start() { 20 start() {
18 this._registerActions(createActionBindings([ 21 this._registerActions(
19 [actions.countUp, this._countUp], 22 createActionBindings([[actions.countUp, this._countUp]]),
20 ])); 23 );
21 this._registerReactions(createReactions([ 24 this._registerReactions(createReactions([this._countReaction]));
22 this._countReaction,
23 ]));
24 } 25 }
25 26
26 _countUp = () => { 27 _countUp = () => {
@@ -29,7 +30,7 @@ class TestFeatureStore extends FeatureStore {
29 30
30 _countReaction = () => { 31 _countReaction = () => {
31 this.reactionInvokedCount += 1; 32 this.reactionInvokedCount += 1;
32 } 33 };
33} 34}
34 35
35describe('FeatureStore', () => { 36describe('FeatureStore', () => {
diff --git a/src/features/webControls/components/WebControls.js b/src/features/webControls/components/WebControls.js
index 97fa20dcc..5650d4cd1 100644
--- a/src/features/webControls/components/WebControls.js
+++ b/src/features/webControls/components/WebControls.js
@@ -1,8 +1,7 @@
1import React, { Component } from 'react'; 1import { createRef, Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss'; 4import injectSheet from 'react-jss';
5import { Icon } from '@meetfranz/ui';
6import { defineMessages, injectIntl } from 'react-intl'; 5import { defineMessages, injectIntl } from 'react-intl';
7 6
8import { 7import {
@@ -13,6 +12,8 @@ import {
13 mdiEarth, 12 mdiEarth,
14} from '@mdi/js'; 13} from '@mdi/js';
15 14
15import { Icon } from '../../../components/ui/icon';
16
16const messages = defineMessages({ 17const messages = defineMessages({
17 goHome: { 18 goHome: {
18 id: 'webControls.goHome', 19 id: 'webControls.goHome',
@@ -121,7 +122,7 @@ class WebControls extends Component {
121 } 122 }
122 } 123 }
123 124
124 inputRef = React.createRef(); 125 inputRef = createRef();
125 126
126 state = { 127 state = {
127 inputUrl: '', 128 inputUrl: '',
diff --git a/src/features/webControls/containers/WebControlsScreen.js b/src/features/webControls/containers/WebControlsScreen.js
index 0273bb13e..6fba5db86 100644
--- a/src/features/webControls/containers/WebControlsScreen.js
+++ b/src/features/webControls/containers/WebControlsScreen.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import { observer, inject } from 'mobx-react'; 2import { observer, inject } from 'mobx-react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4 4
diff --git a/src/features/workspaces/components/CreateWorkspaceForm.js b/src/features/workspaces/components/CreateWorkspaceForm.js
index c9b05b87f..75f6d9f4a 100644
--- a/src/features/workspaces/components/CreateWorkspaceForm.js
+++ b/src/features/workspaces/components/CreateWorkspaceForm.js
@@ -1,9 +1,11 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
5import { Input, Button } from '@meetfranz/forms';
6import injectSheet from 'react-jss'; 5import injectSheet from 'react-jss';
6
7import { Input } from '../../../components/ui/input/index';
8import { Button } from '../../../components/ui/button/index';
7import Form from '../../../lib/Form'; 9import Form from '../../../lib/Form';
8import { required } from '../../../helpers/validation-helpers'; 10import { required } from '../../../helpers/validation-helpers';
9import { workspaceStore } from '../index'; 11import { workspaceStore } from '../index';
diff --git a/src/features/workspaces/components/EditWorkspaceForm.js b/src/features/workspaces/components/EditWorkspaceForm.js
index f562733dd..fa3ea4289 100644
--- a/src/features/workspaces/components/EditWorkspaceForm.js
+++ b/src/features/workspaces/components/EditWorkspaceForm.js
@@ -1,11 +1,12 @@
1import React, { Component, Fragment } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
5import { Link } from 'react-router'; 5import { Link } from 'react-router';
6import { Input, Button } from '@meetfranz/forms';
7import injectSheet from 'react-jss'; 6import injectSheet from 'react-jss';
8 7
8import { Input } from '../../../components/ui/input/index';
9import { Button } from '../../../components/ui/button/index';
9import Workspace from '../models/Workspace'; 10import Workspace from '../models/Workspace';
10import Service from '../../../models/Service'; 11import Service from '../../../models/Service';
11import Form from '../../../lib/Form'; 12import Form from '../../../lib/Form';
diff --git a/src/features/workspaces/components/WorkspaceDrawer.js b/src/features/workspaces/components/WorkspaceDrawer.js
index 3dac77bc2..590efacd0 100644
--- a/src/features/workspaces/components/WorkspaceDrawer.js
+++ b/src/features/workspaces/components/WorkspaceDrawer.js
@@ -1,12 +1,14 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss'; 4import injectSheet from 'react-jss';
5import { defineMessages, injectIntl } from 'react-intl'; 5import { defineMessages, injectIntl } from 'react-intl';
6import { H1, Icon } from '@meetfranz/ui';
7import ReactTooltip from 'react-tooltip'; 6import ReactTooltip from 'react-tooltip';
8 7
9import { mdiPlusBox, mdiCog } from '@mdi/js'; 8import { mdiPlusBox, mdiCog } from '@mdi/js';
9
10import { H1 } from '../../../components/ui/headline';
11import { Icon } from '../../../components/ui/icon';
10import WorkspaceDrawerItem from './WorkspaceDrawerItem'; 12import WorkspaceDrawerItem from './WorkspaceDrawerItem';
11import { workspaceActions } from '../actions'; 13import { workspaceActions } from '../actions';
12import { workspaceStore } from '../index'; 14import { workspaceStore } from '../index';
diff --git a/src/features/workspaces/components/WorkspaceDrawerItem.js b/src/features/workspaces/components/WorkspaceDrawerItem.js
index 4afb9c108..d3c9fa767 100644
--- a/src/features/workspaces/components/WorkspaceDrawerItem.js
+++ b/src/features/workspaces/components/WorkspaceDrawerItem.js
@@ -1,5 +1,5 @@
1import { Menu } from '@electron/remote'; 1import { Menu } from '@electron/remote';
2import React, { Component } from 'react'; 2import { Component } from 'react';
3import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
4import { observer } from 'mobx-react'; 4import { observer } from 'mobx-react';
5import injectSheet from 'react-jss'; 5import injectSheet from 'react-jss';
@@ -118,14 +118,12 @@ class WorkspaceDrawerItem extends Component {
118 isActive ? classes.isActiveItem : null, 118 isActive ? classes.isActiveItem : null,
119 ])} 119 ])}
120 onClick={onClick} 120 onClick={onClick}
121 onContextMenu={() => 121 onContextMenu={() => onContextMenuEditClick && contextMenu.popup()}
122 onContextMenuEditClick && contextMenu.popup()
123 }
124 data-tip={`${ 122 data-tip={`${
125 shortcutIndex <= 9 123 shortcutIndex <= 9
126 ? `(${cmdOrCtrlShortcutKey(false)}+${altKey( 124 ? `(${cmdOrCtrlShortcutKey(false)}+${altKey(
127 false, 125 false,
128 )}+${shortcutIndex})` 126 )}+${shortcutIndex})`
129 : '' 127 : ''
130 }`} 128 }`}
131 > 129 >
diff --git a/src/features/workspaces/components/WorkspaceItem.js b/src/features/workspaces/components/WorkspaceItem.tsx
index ec7b19add..6fb02d2f5 100644
--- a/src/features/workspaces/components/WorkspaceItem.js
+++ b/src/features/workspaces/components/WorkspaceItem.tsx
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss'; 4import injectSheet from 'react-jss';
@@ -16,9 +16,15 @@ const styles = theme => ({
16 columnName: {}, 16 columnName: {},
17}); 17});
18 18
19type Props = {
20 classes: any;
21 workspace: any;
22 onItemClick: (workspace) => void;
23};
24
19@injectSheet(styles) 25@injectSheet(styles)
20@observer 26@observer
21class WorkspaceItem extends Component { 27class WorkspaceItem extends Component<Props> {
22 static propTypes = { 28 static propTypes = {
23 classes: PropTypes.object.isRequired, 29 classes: PropTypes.object.isRequired,
24 workspace: PropTypes.instanceOf(Workspace).isRequired, 30 workspace: PropTypes.instanceOf(Workspace).isRequired,
diff --git a/src/features/workspaces/components/WorkspaceServiceListItem.js b/src/features/workspaces/components/WorkspaceServiceListItem.tsx
index f6e2a2786..6e012eb1e 100644
--- a/src/features/workspaces/components/WorkspaceServiceListItem.js
+++ b/src/features/workspaces/components/WorkspaceServiceListItem.tsx
@@ -1,14 +1,12 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 2import { observer } from 'mobx-react';
4import injectSheet from 'react-jss'; 3import injectSheet from 'react-jss';
5import classnames from 'classnames'; 4import classnames from 'classnames';
6import { Toggle } from '@meetfranz/forms';
7 5
8import Service from '../../../models/Service'; 6import { Toggle } from '../../../components/ui/toggle/index';
9import ServiceIcon from '../../../components/ui/ServiceIcon'; 7import ServiceIcon from '../../../components/ui/ServiceIcon';
10 8
11const styles = (theme) => ({ 9const styles = theme => ({
12 listItem: { 10 listItem: {
13 height: theme.workspaces.settings.listItems.height, 11 height: theme.workspaces.settings.listItems.height,
14 borderBottom: `1px solid ${theme.workspaces.settings.listItems.borderColor}`, 12 borderBottom: `1px solid ${theme.workspaces.settings.listItems.borderColor}`,
@@ -31,29 +29,22 @@ const styles = (theme) => ({
31 }, 29 },
32}); 30});
33 31
34@injectSheet(styles) @observer 32type Props = {
35class WorkspaceServiceListItem extends Component { 33 classes: any;
36 static propTypes = { 34 isInWorkspace: boolean;
37 classes: PropTypes.object.isRequired, 35 onToggle: () => void;
38 isInWorkspace: PropTypes.bool.isRequired, 36 service: any;
39 onToggle: PropTypes.func.isRequired, 37};
40 service: PropTypes.instanceOf(Service).isRequired,
41 };
42 38
39@injectSheet(styles)
40@observer
41class WorkspaceServiceListItem extends Component<Props> {
43 render() { 42 render() {
44 const { 43 const { classes, isInWorkspace, onToggle, service } = this.props;
45 classes,
46 isInWorkspace,
47 onToggle,
48 service,
49 } = this.props;
50 44
51 return ( 45 return (
52 <div className={classes.listItem}> 46 <div className={classes.listItem}>
53 <ServiceIcon 47 <ServiceIcon className={classes.serviceIcon} service={service} />
54 className={classes.serviceIcon}
55 service={service}
56 />
57 <span 48 <span
58 className={classnames([ 49 className={classnames([
59 classes.label, 50 classes.label,
diff --git a/src/features/workspaces/components/WorkspaceSwitchingIndicator.js b/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
index 33a82cf4b..ff4e9475a 100644
--- a/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
+++ b/src/features/workspaces/components/WorkspaceSwitchingIndicator.js
@@ -1,11 +1,11 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import injectSheet from 'react-jss'; 4import injectSheet from 'react-jss';
5import classnames from 'classnames'; 5import classnames from 'classnames';
6import { Loader } from '@meetfranz/ui';
7import { defineMessages, injectIntl } from 'react-intl'; 6import { defineMessages, injectIntl } from 'react-intl';
8 7
8import { Loader } from '../../../components/ui/loader/index';
9import { workspaceStore } from '../index'; 9import { workspaceStore } from '../index';
10 10
11const messages = defineMessages({ 11const messages = defineMessages({
diff --git a/src/features/workspaces/components/WorkspacesDashboard.js b/src/features/workspaces/components/WorkspacesDashboard.js
index 49552df6b..6e0d98ffb 100644
--- a/src/features/workspaces/components/WorkspacesDashboard.js
+++ b/src/features/workspaces/components/WorkspacesDashboard.js
@@ -1,11 +1,12 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes, inject } from 'mobx-react'; 3import { observer, PropTypes as MobxPropTypes, inject } from 'mobx-react';
4import { defineMessages, injectIntl } from 'react-intl'; 4import { defineMessages, injectIntl } from 'react-intl';
5import injectSheet from 'react-jss'; 5import injectSheet from 'react-jss';
6import { Infobox } from '@meetfranz/ui';
7 6
8import { mdiCheckboxMarkedCircleOutline } from '@mdi/js'; 7import { mdiCheckboxMarkedCircleOutline } from '@mdi/js';
8
9import { Infobox } from '../../../components/ui/infobox/index';
9import Loader from '../../../components/ui/Loader'; 10import Loader from '../../../components/ui/Loader';
10import WorkspaceItem from './WorkspaceItem'; 11import WorkspaceItem from './WorkspaceItem';
11import CreateWorkspaceForm from './CreateWorkspaceForm'; 12import CreateWorkspaceForm from './CreateWorkspaceForm';
@@ -40,7 +41,8 @@ const messages = defineMessages({
40 }, 41 },
41 workspaceFeatureInfo: { 42 workspaceFeatureInfo: {
42 id: 'settings.workspaces.workspaceFeatureInfo', 43 id: 'settings.workspaces.workspaceFeatureInfo',
43 defaultMessage: 'Ferdi Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time. You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.', 44 defaultMessage:
45 'Ferdi Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time. You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.',
44 }, 46 },
45 workspaceFeatureHeadline: { 47 workspaceFeatureHeadline: {
46 id: 'settings.workspaces.workspaceFeatureHeadline', 48 id: 'settings.workspaces.workspaceFeatureHeadline',
diff --git a/src/features/workspaces/containers/EditWorkspaceScreen.js b/src/features/workspaces/containers/EditWorkspaceScreen.tsx
index ba7606031..8e8f8179d 100644
--- a/src/features/workspaces/containers/EditWorkspaceScreen.js
+++ b/src/features/workspaces/containers/EditWorkspaceScreen.tsx
@@ -1,26 +1,26 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import { inject, observer } from 'mobx-react'; 2import { inject, observer } from 'mobx-react';
3import PropTypes from 'prop-types';
4 3
5import ErrorBoundary from '../../../components/util/ErrorBoundary'; 4import ErrorBoundary from '../../../components/util/ErrorBoundary';
6import EditWorkspaceForm from '../components/EditWorkspaceForm'; 5import EditWorkspaceForm from '../components/EditWorkspaceForm';
7import ServicesStore from '../../../stores/ServicesStore';
8import Workspace from '../models/Workspace'; 6import Workspace from '../models/Workspace';
9import { workspaceStore } from '../index'; 7import { workspaceStore } from '../index';
10import { deleteWorkspaceRequest, updateWorkspaceRequest } from '../api'; 8import { deleteWorkspaceRequest, updateWorkspaceRequest } from '../api';
11import WorkspacesStore from '../store'; 9import { ServicesStore, WorkspacesStore } from '../../../stores.types';
12 10
13@inject('stores', 'actions') @observer 11type Props = {
14class EditWorkspaceScreen extends Component { 12 actions: {
15 static propTypes = { 13 workspaces: WorkspacesStore;
16 actions: PropTypes.shape({
17 workspaces: PropTypes.instanceOf(WorkspacesStore),
18 }).isRequired,
19 stores: PropTypes.shape({
20 services: PropTypes.instanceOf(ServicesStore).isRequired,
21 }).isRequired,
22 }; 14 };
15 stores: {
16 services: ServicesStore;
17 };
18};
23 19
20@inject('stores', 'actions')
21@observer
22class EditWorkspaceScreen extends Component<Props> {
23 // @ts-expect-error Not all code paths return a value.
24 onDelete = () => { 24 onDelete = () => {
25 const { workspaceBeingEdited } = workspaceStore; 25 const { workspaceBeingEdited } = workspaceStore;
26 const { actions } = this.props; 26 const { actions } = this.props;
@@ -28,12 +28,14 @@ class EditWorkspaceScreen extends Component {
28 actions.workspaces.delete({ workspace: workspaceBeingEdited }); 28 actions.workspaces.delete({ workspace: workspaceBeingEdited });
29 }; 29 };
30 30
31 onSave = (values) => { 31 onSave = values => {
32 const { workspaceBeingEdited } = workspaceStore; 32 const { workspaceBeingEdited } = workspaceStore;
33 const { actions } = this.props; 33 const { actions } = this.props;
34 const workspace = new Workspace( 34 const workspace = new Workspace({
35 ({ saving: true, ...workspaceBeingEdited, ...values }), 35 saving: true,
36 ); 36 ...workspaceBeingEdited,
37 ...values,
38 });
37 actions.workspaces.update({ workspace }); 39 actions.workspaces.update({ workspace });
38 }; 40 };
39 41
diff --git a/src/features/workspaces/containers/WorkspacesScreen.js b/src/features/workspaces/containers/WorkspacesScreen.tsx
index 4828658f9..a07e92439 100644
--- a/src/features/workspaces/containers/WorkspacesScreen.js
+++ b/src/features/workspaces/containers/WorkspacesScreen.tsx
@@ -1,6 +1,5 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import { inject, observer } from 'mobx-react'; 2import { inject, observer } from 'mobx-react';
3import PropTypes from 'prop-types';
4import WorkspacesDashboard from '../components/WorkspacesDashboard'; 3import WorkspacesDashboard from '../components/WorkspacesDashboard';
5import ErrorBoundary from '../../../components/util/ErrorBoundary'; 4import ErrorBoundary from '../../../components/util/ErrorBoundary';
6import { workspaceStore } from '../index'; 5import { workspaceStore } from '../index';
@@ -10,16 +9,17 @@ import {
10 getUserWorkspacesRequest, 9 getUserWorkspacesRequest,
11 updateWorkspaceRequest, 10 updateWorkspaceRequest,
12} from '../api'; 11} from '../api';
13import WorkspacesStore from '../store'; 12import { WorkspacesStore } from '../../../stores.types';
14 13
15@inject('stores', 'actions') @observer 14type Props = {
16class WorkspacesScreen extends Component { 15 actions: {
17 static propTypes = { 16 workspaces: WorkspacesStore;
18 actions: PropTypes.shape({
19 workspaces: PropTypes.instanceOf(WorkspacesStore),
20 }).isRequired,
21 }; 17 };
18};
22 19
20@inject('stores', 'actions')
21@observer
22class WorkspacesScreen extends Component<Props> {
23 render() { 23 render() {
24 const { actions } = this.props; 24 const { actions } = this.props;
25 return ( 25 return (
@@ -30,8 +30,8 @@ class WorkspacesScreen extends Component {
30 createWorkspaceRequest={createWorkspaceRequest} 30 createWorkspaceRequest={createWorkspaceRequest}
31 deleteWorkspaceRequest={deleteWorkspaceRequest} 31 deleteWorkspaceRequest={deleteWorkspaceRequest}
32 updateWorkspaceRequest={updateWorkspaceRequest} 32 updateWorkspaceRequest={updateWorkspaceRequest}
33 onCreateWorkspaceSubmit={(data) => actions.workspaces.create(data)} 33 onCreateWorkspaceSubmit={data => actions.workspaces.create(data)}
34 onWorkspaceClick={(w) => actions.workspaces.edit({ workspace: w })} 34 onWorkspaceClick={w => actions.workspaces.edit({ workspace: w })}
35 /> 35 />
36 </ErrorBoundary> 36 </ErrorBoundary>
37 ); 37 );
diff --git a/src/features/workspaces/store.js b/src/features/workspaces/store.js
index db2b69f99..0fa43b723 100644
--- a/src/features/workspaces/store.js
+++ b/src/features/workspaces/store.js
@@ -124,7 +124,7 @@ export default class WorkspacesStore extends FeatureStore {
124 this.isFeatureActive = false; 124 this.isFeatureActive = false;
125 } 125 }
126 126
127 filterServicesByActiveWorkspace = (services) => { 127 filterServicesByActiveWorkspace = services => {
128 const { activeWorkspace, isFeatureActive } = this; 128 const { activeWorkspace, isFeatureActive } = this;
129 if (isFeatureActive && activeWorkspace) { 129 if (isFeatureActive && activeWorkspace) {
130 return this.getWorkspaceServices(activeWorkspace); 130 return this.getWorkspaceServices(activeWorkspace);
@@ -134,14 +134,14 @@ export default class WorkspacesStore extends FeatureStore {
134 134
135 getWorkspaceServices(workspace) { 135 getWorkspaceServices(workspace) {
136 const { services } = this.stores; 136 const { services } = this.stores;
137 return workspace.services.map((id) => services.one(id)).filter((s) => !!s); 137 return workspace.services.map(id => services.one(id)).filter(s => !!s);
138 } 138 }
139 139
140 // ========== PRIVATE METHODS ========= // 140 // ========== PRIVATE METHODS ========= //
141 141
142 _getWorkspaceById = (id) => this.workspaces.find((w) => w.id === id); 142 _getWorkspaceById = id => this.workspaces.find(w => w.id === id);
143 143
144 _updateSettings = (changes) => { 144 _updateSettings = changes => {
145 localStorage.setItem('workspaces', { 145 localStorage.setItem('workspaces', {
146 ...this.settings, 146 ...this.settings,
147 ...changes, 147 ...changes,
@@ -191,9 +191,15 @@ export default class WorkspacesStore extends FeatureStore {
191 this.isSwitchingWorkspace = false; 191 this.isSwitchingWorkspace = false;
192 this.nextWorkspace = null; 192 this.nextWorkspace = null;
193 if (this.stores.settings.app.splitMode) { 193 if (this.stores.settings.app.splitMode) {
194 const serviceNames = new Set(this.getWorkspaceServices(workspace).map(service => service.name)); 194 const serviceNames = new Set(
195 for (const wrapper of document.querySelectorAll('.services__webview-wrapper')) { 195 this.getWorkspaceServices(workspace).map(service => service.name),
196 wrapper.style.display = serviceNames.has(wrapper.dataset.name) ? '' : 'none'; 196 );
197 for (const wrapper of document.querySelectorAll(
198 '.services__webview-wrapper',
199 )) {
200 wrapper.style.display = serviceNames.has(wrapper.dataset.name)
201 ? ''
202 : 'none';
197 } 203 }
198 } 204 }
199 }, 1000); 205 }, 1000);
@@ -212,7 +218,9 @@ export default class WorkspacesStore extends FeatureStore {
212 setTimeout(() => { 218 setTimeout(() => {
213 this.isSwitchingWorkspace = false; 219 this.isSwitchingWorkspace = false;
214 if (this.stores.settings.app.splitMode) { 220 if (this.stores.settings.app.splitMode) {
215 for (const wrapper of document.querySelectorAll('.services__webview-wrapper')) { 221 for (const wrapper of document.querySelectorAll(
222 '.services__webview-wrapper',
223 )) {
216 wrapper.style.display = ''; 224 wrapper.style.display = '';
217 } 225 }
218 } 226 }
@@ -262,7 +270,8 @@ export default class WorkspacesStore extends FeatureStore {
262 const activeService = this.stores.services.active; 270 const activeService = this.stores.services.active;
263 const workspaceServices = this.getWorkspaceServices(this.activeWorkspace); 271 const workspaceServices = this.getWorkspaceServices(this.activeWorkspace);
264 if (workspaceServices.length <= 0) return; 272 if (workspaceServices.length <= 0) return;
265 const isActiveServiceInWorkspace = workspaceServices.includes(activeService); 273 const isActiveServiceInWorkspace =
274 workspaceServices.includes(activeService);
266 if (!isActiveServiceInWorkspace) { 275 if (!isActiveServiceInWorkspace) {
267 this.actions.service.setActive({ 276 this.actions.service.setActive({
268 serviceId: workspaceServices[0].id, 277 serviceId: workspaceServices[0].id,
@@ -288,8 +297,10 @@ export default class WorkspacesStore extends FeatureStore {
288 const isWorkspaceSettingsRoute = router.location.pathname.includes( 297 const isWorkspaceSettingsRoute = router.location.pathname.includes(
289 WORKSPACES_ROUTES.ROOT, 298 WORKSPACES_ROUTES.ROOT,
290 ); 299 );
291 const isSwitchingToSettingsRoute = !this.isSettingsRouteActive && isWorkspaceSettingsRoute; 300 const isSwitchingToSettingsRoute =
292 const isLeavingSettingsRoute = !isWorkspaceSettingsRoute && this.isSettingsRouteActive; 301 !this.isSettingsRouteActive && isWorkspaceSettingsRoute;
302 const isLeavingSettingsRoute =
303 !isWorkspaceSettingsRoute && this.isSettingsRouteActive;
293 304
294 if (isSwitchingToSettingsRoute) { 305 if (isSwitchingToSettingsRoute) {
295 this.isSettingsRouteActive = true; 306 this.isSettingsRouteActive = true;
@@ -300,8 +311,8 @@ export default class WorkspacesStore extends FeatureStore {
300 } else if (isLeavingSettingsRoute) { 311 } else if (isLeavingSettingsRoute) {
301 this.isSettingsRouteActive = false; 312 this.isSettingsRouteActive = false;
302 if ( 313 if (
303 !this._wasDrawerOpenBeforeSettingsRoute 314 !this._wasDrawerOpenBeforeSettingsRoute &&
304 && this.isWorkspaceDrawerOpen 315 this.isWorkspaceDrawerOpen
305 ) { 316 ) {
306 workspaceActions.toggleWorkspaceDrawer(); 317 workspaceActions.toggleWorkspaceDrawer();
307 } 318 }
@@ -311,14 +322,15 @@ export default class WorkspacesStore extends FeatureStore {
311 _cleanupInvalidServiceReferences = () => { 322 _cleanupInvalidServiceReferences = () => {
312 const { services } = this.stores; 323 const { services } = this.stores;
313 const { allServicesRequest } = services; 324 const { allServicesRequest } = services;
314 const servicesHaveBeenLoaded = allServicesRequest.wasExecuted && !allServicesRequest.isError; 325 const servicesHaveBeenLoaded =
326 allServicesRequest.wasExecuted && !allServicesRequest.isError;
315 // Loop through all workspaces and remove invalid service ids (locally) 327 // Loop through all workspaces and remove invalid service ids (locally)
316 for (const workspace of this.workspaces) { 328 for (const workspace of this.workspaces) {
317 for (const serviceId of workspace.services) { 329 for (const serviceId of workspace.services) {
318 if ( 330 if (
319 servicesHaveBeenLoaded 331 servicesHaveBeenLoaded &&
320 && !services.one(serviceId) 332 !services.one(serviceId) &&
321 && serviceId !== KEEP_WS_LOADED_USID 333 serviceId !== KEEP_WS_LOADED_USID
322 ) { 334 ) {
323 workspace.services.remove(serviceId); 335 workspace.services.remove(serviceId);
324 } 336 }
diff --git a/src/helpers/array-helpers.ts b/src/helpers/array-helpers.ts
index ae5d8d99f..3f8806176 100644
--- a/src/helpers/array-helpers.ts
+++ b/src/helpers/array-helpers.ts
@@ -1,4 +1,5 @@
1export const shuffleArray = (arr: any[]) => arr 1export const shuffleArray = (arr: any[]) =>
2 .map((a) => [Math.random(), a]) 2 arr
3 .sort((a, b) => a[0] - b[0]) 3 .map(a => [Math.random(), a])
4 .map((a) => a[1]); 4 .sort((a, b) => a[0] - b[0])
5 .map(a => a[1]);
diff --git a/src/helpers/async-helpers.ts b/src/helpers/async-helpers.ts
index aae3c3928..6b1f24b5a 100644
--- a/src/helpers/async-helpers.ts
+++ b/src/helpers/async-helpers.ts
@@ -1,5 +1,3 @@
1/* eslint-disable import/prefer-default-export */
2
3export function sleep(ms: number = 0) { 1export function sleep(ms: number = 0) {
4 return new Promise((r) => setTimeout(r, ms)); 2 return new Promise(r => setTimeout(r, ms));
5} 3}
diff --git a/src/helpers/i18n-helpers.ts b/src/helpers/i18n-helpers.ts
index ec7dc8e98..e6c66c7d3 100644
--- a/src/helpers/i18n-helpers.ts
+++ b/src/helpers/i18n-helpers.ts
@@ -1,52 +1,44 @@
1export function getLocale({ 1export function getLocale({ locale, locales, fallbackLocale }) {
2 locale, locales, defaultLocale, fallbackLocale, 2 if (!locale) {
3}) { 3 return fallbackLocale;
4 let localeStr = locale; 4 }
5 if (locales[locale] === undefined) { 5
6 if (!locales[locale]) {
6 let localeFuzzy: string | undefined; 7 let localeFuzzy: string | undefined;
7 for (const localStr of Object.keys(locales)) { 8 for (const localStr of Object.keys(locales)) {
8 if (locales && Object.hasOwnProperty.call(locales, localStr) && locale.slice(0, 2) === localStr.slice(0, 2)) { 9 if (locale.slice(0, 2) === localStr.slice(0, 2)) {
9 localeFuzzy = localStr; 10 localeFuzzy = localStr;
10 } 11 }
11 } 12 }
12 13
13 if (localeFuzzy !== undefined) { 14 if (localeFuzzy) {
14 localeStr = localeFuzzy; 15 return localeFuzzy;
15 } 16 }
16 } 17 }
17 18
18 if (locales[localeStr] === undefined) { 19 return locale;
19 localeStr = defaultLocale;
20 }
21
22 if (!localeStr) {
23 localeStr = fallbackLocale;
24 }
25
26 return localeStr;
27} 20}
28 21
29export function getSelectOptions({ 22export function getSelectOptions({
30 locales, resetToDefaultText = '', automaticDetectionText = '', sort = true, 23 locales,
24 resetToDefaultText = '',
25 automaticDetectionText = '',
26 sort = true,
31}) { 27}) {
32 const options: object[] = []; 28 const options: object[] = [];
33 29
34 if (resetToDefaultText) { 30 if (resetToDefaultText) {
35 options.push( 31 options.push({
36 { 32 value: '',
37 value: '', 33 label: resetToDefaultText,
38 label: resetToDefaultText, 34 });
39 },
40 );
41 } 35 }
42 36
43 if (automaticDetectionText) { 37 if (automaticDetectionText) {
44 options.push( 38 options.push({
45 { 39 value: 'automatic',
46 value: 'automatic', 40 label: automaticDetectionText,
47 label: automaticDetectionText, 41 });
48 },
49 );
50 } 42 }
51 43
52 options.push({ 44 options.push({
diff --git a/src/helpers/password-helpers.ts b/src/helpers/password-helpers.ts
index e5d9a4a25..053321bbf 100644
--- a/src/helpers/password-helpers.ts
+++ b/src/helpers/password-helpers.ts
@@ -1,7 +1,7 @@
1import crypto from 'crypto'; 1import { createHash, BinaryLike } from 'crypto';
2 2
3export function hash(password: crypto.BinaryLike) { 3export function hash(password: BinaryLike) {
4 return crypto.createHash('sha256').update(password).digest('base64'); 4 return createHash('sha256').update(password).digest('base64');
5} 5}
6 6
7export function scorePassword(password: string) { 7export function scorePassword(password: string) {
diff --git a/src/helpers/routing-helpers.ts b/src/helpers/routing-helpers.ts
index 18169f01b..46895aa6b 100644
--- a/src/helpers/routing-helpers.ts
+++ b/src/helpers/routing-helpers.ts
@@ -1,3 +1,4 @@
1import RouteParser from 'route-parser'; 1import RouteParser from 'route-parser';
2 2
3export const matchRoute = (pattern: string, path: string) => new RouteParser(pattern).match(path); 3export const matchRoute = (pattern: string, path: string) =>
4 new RouteParser(pattern).match(path);
diff --git a/src/helpers/schedule-helpers.ts b/src/helpers/schedule-helpers.ts
index 55b7c1e6f..37caffd79 100644
--- a/src/helpers/schedule-helpers.ts
+++ b/src/helpers/schedule-helpers.ts
@@ -1,17 +1,9 @@
1/* eslint-disable import/prefer-default-export */
2
3export function isInTimeframe(start: string, end: string) { 1export function isInTimeframe(start: string, end: string) {
4 const [ 2 const [startHourStr, startMinuteStr] = start.split(':');
5 startHourStr,
6 startMinuteStr,
7 ] = start.split(':');
8 const startHour = Number.parseInt(startHourStr, 10); 3 const startHour = Number.parseInt(startHourStr, 10);
9 const startMinute = Number.parseInt(startMinuteStr, 10); 4 const startMinute = Number.parseInt(startMinuteStr, 10);
10 5
11 const [ 6 const [endHourStr, endMinuteStr] = end.split(':');
12 endHourStr,
13 endMinuteStr,
14 ] = end.split(':');
15 const endHour = Number.parseInt(endHourStr, 10); 7 const endHour = Number.parseInt(endHourStr, 10);
16 const endMinute = Number.parseInt(endMinuteStr, 10); 8 const endMinute = Number.parseInt(endMinuteStr, 10);
17 9
@@ -20,46 +12,31 @@ export function isInTimeframe(start: string, end: string) {
20 12
21 // Check if the end time is before the start time (scheduled overnight) 13 // Check if the end time is before the start time (scheduled overnight)
22 // as we need to change our checks based on this 14 // as we need to change our checks based on this
23 const endBeforeStart = (startHour > endHour || (startHour === endHour && startMinute > endMinute)); 15 const endBeforeStart =
16 startHour > endHour || (startHour === endHour && startMinute > endMinute);
24 17
25 if ( 18 if (
26 // End is after start (e.g. 09:00-17:00) 19 // End is after start (e.g. 09:00-17:00)
27 !endBeforeStart 20 !endBeforeStart &&
28 // Check if past start 21 // Check if past start
29 && ((currentHour > startHour 22 (currentHour > startHour ||
30 || ( 23 (currentHour === startHour && currentMinute >= startMinute)) &&
31 currentHour === startHour 24 // Check that not past end
32 && currentMinute >= startMinute 25 (currentHour < endHour ||
33 ) 26 (currentHour === endHour && currentMinute < endMinute))
34 )
35 // Check that not past end
36 && (currentHour < endHour
37 || (
38 currentHour === endHour
39 && currentMinute < endMinute
40 )
41 ))
42 ) { 27 ) {
43 // We are in scheduled timeframe 28 // We are in scheduled timeframe
44 return true; 29 return true;
45 } 30 }
46 if ( 31 if (
47 // End is before start (e.g. 17:00-09:00) 32 // End is before start (e.g. 17:00-09:00)
48 endBeforeStart 33 endBeforeStart &&
49 // Check if past start 34 // Check if past start
50 && ((currentHour > startHour 35 (currentHour > startHour ||
51 || ( 36 (currentHour === startHour && currentMinute >= startMinute) ||
52 currentHour === startHour
53 && currentMinute >= startMinute
54 )
55 )
56 // Check that we are not past end 37 // Check that we are not past end
57 || (currentHour < endHour 38 currentHour < endHour ||
58 || ( 39 (currentHour === endHour && currentMinute < endMinute))
59 currentHour === endHour
60 && currentMinute < endMinute
61 )
62 ))
63 ) { 40 ) {
64 // We are also in scheduled timeframe 41 // We are also in scheduled timeframe
65 return true; 42 return true;
diff --git a/src/helpers/url-helpers.ts b/src/helpers/url-helpers.ts
index 1e87ecabb..135f06cbf 100644
--- a/src/helpers/url-helpers.ts
+++ b/src/helpers/url-helpers.ts
@@ -29,7 +29,10 @@ export async function openPath(folderName: string) {
29} 29}
30 30
31// TODO: Need to verify and fix/remove the skipping logic. Ideally, we should never skip this check 31// TODO: Need to verify and fix/remove the skipping logic. Ideally, we should never skip this check
32export function openExternalUrl(url: string | URL, skipValidityCheck: boolean = false) { 32export function openExternalUrl(
33 url: string | URL,
34 skipValidityCheck: boolean = false,
35) {
33 debug('Open url:', url, 'with skipValidityCheck:', skipValidityCheck); 36 debug('Open url:', url, 'with skipValidityCheck:', skipValidityCheck);
34 if (skipValidityCheck || isValidExternalURL(url)) { 37 if (skipValidityCheck || isValidExternalURL(url)) {
35 shell.openExternal(url.toString()); 38 shell.openExternal(url.toString());
diff --git a/src/helpers/userAgent-helpers.ts b/src/helpers/userAgent-helpers.ts
index 091a76400..c1e8e02da 100644
--- a/src/helpers/userAgent-helpers.ts
+++ b/src/helpers/userAgent-helpers.ts
@@ -1,13 +1,18 @@
1import os from 'os'; 1import { cpus } from 'os';
2import macosVersion from 'macos-version'; 2import macosVersion from 'macos-version';
3import { chrome } from 'useragent-generator'; 3import { chrome } from 'useragent-generator';
4import { 4import {
5 chromeVersion, isMac, isWindows, is64Bit, osArch, osRelease, 5 chromeVersion,
6 isMac,
7 isWindows,
8 is64Bit,
9 osArch,
10 osRelease,
6} from '../environment'; 11} from '../environment';
7 12
8function macOS() { 13function macOS() {
9 const version = macosVersion() || ''; 14 const version = macosVersion() || '';
10 let cpuName = os.cpus()[0].model.split(' ')[0]; 15 let cpuName = cpus()[0].model.split(' ')[0];
11 if (cpuName && /\(/.test(cpuName)) { 16 if (cpuName && /\(/.test(cpuName)) {
12 cpuName = cpuName.split('(')[0]; 17 cpuName = cpuName.split('(')[0];
13 } 18 }
diff --git a/src/i18n/apply-branding.ts b/src/i18n/apply-branding.ts
index 801a4a525..7943c099d 100644
--- a/src/i18n/apply-branding.ts
+++ b/src/i18n/apply-branding.ts
@@ -1,8 +1,8 @@
1/** 1/**
2 * Apply Ferdi branding to i18n translations 2 * Apply Ferdi branding to i18n translations
3 */ 3 */
4import fs from 'fs-extra'; 4import { readdirSync, readJson, writeJson } from 'fs-extra';
5import path from 'path'; 5import { join } from 'path';
6 6
7console.log('Applying Ferdi branding to translations...'); 7console.log('Applying Ferdi branding to translations...');
8 8
@@ -31,13 +31,13 @@ const replace = {
31 '!!!': '', 31 '!!!': '',
32}; 32};
33 33
34const locales = path.join(__dirname, 'locales'); 34const locales = join(__dirname, 'locales');
35const files = fs.readdirSync(locales); 35const files = readdirSync(locales);
36 36
37const replaceFind = Object.keys(replace); 37const replaceFind = Object.keys(replace);
38const replaceReplaceWith = Object.values(replace); 38const replaceReplaceWith = Object.values(replace);
39 39
40const replaceStr = (str, find, replaceWith) => { 40const replaceStr = (str: string, find: any[], replaceWith: string[]) => {
41 for (const [i, element] of find.entries()) { 41 for (const [i, element] of find.entries()) {
42 str = str.replace(new RegExp(element, 'gi'), replaceWith[i]); 42 str = str.replace(new RegExp(element, 'gi'), replaceWith[i]);
43 } 43 }
@@ -49,8 +49,8 @@ files.forEach(async file => {
49 if (ignoreFiles.has(file)) return; 49 if (ignoreFiles.has(file)) return;
50 50
51 // Read locale data 51 // Read locale data
52 const filePath = path.join(locales, file); 52 const filePath = join(locales, file);
53 const locale = await fs.readJson(filePath); 53 const locale = await readJson(filePath);
54 54
55 // Replace branding 55 // Replace branding
56 for (const key in locale) { 56 for (const key in locale) {
@@ -59,7 +59,7 @@ files.forEach(async file => {
59 } 59 }
60 } 60 }
61 61
62 await fs.writeJson(filePath, locale, { 62 await writeJson(filePath, locale, {
63 spaces: 2, 63 spaces: 2,
64 EOL: '\n', 64 EOL: '\n',
65 }); 65 });
diff --git a/src/i18n/locales/af.json b/src/i18n/locales/af.json
index 041b02632..ff10978c4 100644
--- a/src/i18n/locales/af.json
+++ b/src/i18n/locales/af.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Enable Dark Mode", 211 "settings.app.form.darkMode": "Enable Dark Mode",
212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration", 212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Enable spell checking", 217 "settings.app.form.enableSpellchecking": "Enable spell checking",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Enable notifications", 312 "settings.service.form.enableNotification": "Enable notifications",
311 "settings.service.form.enableService": "Enable service", 313 "settings.service.form.enableService": "Enable service",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Unread message badges", 315 "settings.service.form.headlineBadges": "Unread message badges",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "General", 317 "settings.service.form.headlineGeneral": "General",
diff --git a/src/i18n/locales/ar.json b/src/i18n/locales/ar.json
index 2ab80e865..8183d193f 100644
--- a/src/i18n/locales/ar.json
+++ b/src/i18n/locales/ar.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "تمكين الوضع المظلم", 211 "settings.app.form.darkMode": "تمكين الوضع المظلم",
212 "settings.app.form.enableGPUAcceleration": "تÙعيل التسريع بوحدة معالجة الرسومات", 212 "settings.app.form.enableGPUAcceleration": "تÙعيل التسريع بوحدة معالجة الرسومات",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "تÙعيل القÙÙ„ بكلمة المرور", 214 "settings.app.form.enableLock": "تÙعيل القÙÙ„ بكلمة المرور",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "تÙعيل التصحيح الإملائي", 217 "settings.app.form.enableSpellchecking": "تÙعيل التصحيح الإملائي",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "تÙعيل الإشعارات", 312 "settings.service.form.enableNotification": "تÙعيل الإشعارات",
311 "settings.service.form.enableService": "تÙعيل الخدمة", 313 "settings.service.form.enableService": "تÙعيل الخدمة",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "شارات الرسائل غير المقروءة", 315 "settings.service.form.headlineBadges": "شارات الرسائل غير المقروءة",
313 "settings.service.form.headlineDarkReaderSettings": "إعدادات وضع القارئ المظلم", 316 "settings.service.form.headlineDarkReaderSettings": "إعدادات وضع القارئ المظلم",
314 "settings.service.form.headlineGeneral": "عام", 317 "settings.service.form.headlineGeneral": "عام",
diff --git a/src/i18n/locales/be.json b/src/i18n/locales/be.json
index 8caf73fe6..5e5b594fe 100644
--- a/src/i18n/locales/be.json
+++ b/src/i18n/locales/be.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Enable Dark Mode", 211 "settings.app.form.darkMode": "Enable Dark Mode",
212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration", 212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Enable spell checking", 217 "settings.app.form.enableSpellchecking": "Enable spell checking",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Enable notifications", 312 "settings.service.form.enableNotification": "Enable notifications",
311 "settings.service.form.enableService": "Enable service", 313 "settings.service.form.enableService": "Enable service",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Unread message badges", 315 "settings.service.form.headlineBadges": "Unread message badges",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "General", 317 "settings.service.form.headlineGeneral": "General",
diff --git a/src/i18n/locales/bs.json b/src/i18n/locales/bs.json
index 041b02632..ff10978c4 100644
--- a/src/i18n/locales/bs.json
+++ b/src/i18n/locales/bs.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Enable Dark Mode", 211 "settings.app.form.darkMode": "Enable Dark Mode",
212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration", 212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Enable spell checking", 217 "settings.app.form.enableSpellchecking": "Enable spell checking",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Enable notifications", 312 "settings.service.form.enableNotification": "Enable notifications",
311 "settings.service.form.enableService": "Enable service", 313 "settings.service.form.enableService": "Enable service",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Unread message badges", 315 "settings.service.form.headlineBadges": "Unread message badges",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "General", 317 "settings.service.form.headlineGeneral": "General",
diff --git a/src/i18n/locales/ca.json b/src/i18n/locales/ca.json
index 6e33be919..b04591bee 100644
--- a/src/i18n/locales/ca.json
+++ b/src/i18n/locales/ca.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Activar el Mode Fosc", 211 "settings.app.form.darkMode": "Activar el Mode Fosc",
212 "settings.app.form.enableGPUAcceleration": "Activar acceleració GPU", 212 "settings.app.form.enableGPUAcceleration": "Activar acceleració GPU",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Habilita la comprobació ortogràfica", 217 "settings.app.form.enableSpellchecking": "Habilita la comprobació ortogràfica",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Activa les notificacions", 312 "settings.service.form.enableNotification": "Activa les notificacions",
311 "settings.service.form.enableService": "Activa el servei", 313 "settings.service.form.enableService": "Activa el servei",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Insígnies de missatges no llegits", 315 "settings.service.form.headlineBadges": "Insígnies de missatges no llegits",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "General", 317 "settings.service.form.headlineGeneral": "General",
diff --git a/src/i18n/locales/cs.json b/src/i18n/locales/cs.json
index 08468e307..edb840722 100644
--- a/src/i18n/locales/cs.json
+++ b/src/i18n/locales/cs.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Povolit Tmavý vzhled", 211 "settings.app.form.darkMode": "Povolit Tmavý vzhled",
212 "settings.app.form.enableGPUAcceleration": "Aktivovat GPU zrychlení", 212 "settings.app.form.enableGPUAcceleration": "Aktivovat GPU zrychlení",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Zapnout kontrolu pravopisu", 217 "settings.app.form.enableSpellchecking": "Zapnout kontrolu pravopisu",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Povolit upozornění", 312 "settings.service.form.enableNotification": "Povolit upozornění",
311 "settings.service.form.enableService": "Povolit službu", 313 "settings.service.form.enableService": "Povolit službu",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Odznaky nepÅ™eÄtených zpráv", 315 "settings.service.form.headlineBadges": "Odznaky nepÅ™eÄtených zpráv",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "Obecné", 317 "settings.service.form.headlineGeneral": "Obecné",
diff --git a/src/i18n/locales/da.json b/src/i18n/locales/da.json
index 9a6d0cf7e..fbe907f10 100644
--- a/src/i18n/locales/da.json
+++ b/src/i18n/locales/da.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Aktiver mørk tilstand", 211 "settings.app.form.darkMode": "Aktiver mørk tilstand",
212 "settings.app.form.enableGPUAcceleration": "Aktiver GPU-acceleration", 212 "settings.app.form.enableGPUAcceleration": "Aktiver GPU-acceleration",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Aktiver adgangskodelås", 214 "settings.app.form.enableLock": "Aktiver adgangskodelås",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Aktiver stavekontrol", 217 "settings.app.form.enableSpellchecking": "Aktiver stavekontrol",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Aktiver notifikationer", 312 "settings.service.form.enableNotification": "Aktiver notifikationer",
311 "settings.service.form.enableService": "Aktiverede tjenester", 313 "settings.service.form.enableService": "Aktiverede tjenester",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Ulæste beskedbadges", 315 "settings.service.form.headlineBadges": "Ulæste beskedbadges",
313 "settings.service.form.headlineDarkReaderSettings": "Mørk læser indstillinger", 316 "settings.service.form.headlineDarkReaderSettings": "Mørk læser indstillinger",
314 "settings.service.form.headlineGeneral": "Generelt", 317 "settings.service.form.headlineGeneral": "Generelt",
diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json
index 0baa4cf5e..a5498fcc9 100644
--- a/src/i18n/locales/de.json
+++ b/src/i18n/locales/de.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Dark Mode aktivieren", 211 "settings.app.form.darkMode": "Dark Mode aktivieren",
212 "settings.app.form.enableGPUAcceleration": "Hardwarebeschleunigung aktivieren", 212 "settings.app.form.enableGPUAcceleration": "Hardwarebeschleunigung aktivieren",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Passwort-Sperre aktivieren", 214 "settings.app.form.enableLock": "Passwort-Sperre aktivieren",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Ferdi immer in der Menüleiste anzeigen", 216 "settings.app.form.enableMenuBar": "Ferdi immer in der Menüleiste anzeigen",
215 "settings.app.form.enableSpellchecking": "Rechtschreibprüfung aktivieren", 217 "settings.app.form.enableSpellchecking": "Rechtschreibprüfung aktivieren",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Ruhezustand aktivieren", 311 "settings.service.form.enableHibernation": "Ruhezustand aktivieren",
310 "settings.service.form.enableNotification": "Benachrichtigungen aktivieren", 312 "settings.service.form.enableNotification": "Benachrichtigungen aktivieren",
311 "settings.service.form.enableService": "Dienst aktivieren", 313 "settings.service.form.enableService": "Dienst aktivieren",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Nachrichten-Badge", 315 "settings.service.form.headlineBadges": "Nachrichten-Badge",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Einstellungen", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Einstellungen",
314 "settings.service.form.headlineGeneral": "Allgemeines", 317 "settings.service.form.headlineGeneral": "Allgemeines",
diff --git a/src/i18n/locales/el.json b/src/i18n/locales/el.json
index 4c658297e..22a3a0bc5 100644
--- a/src/i18n/locales/el.json
+++ b/src/i18n/locales/el.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Enable Dark Mode", 211 "settings.app.form.darkMode": "Enable Dark Mode",
212 "settings.app.form.enableGPUAcceleration": "ΕνεÏγοποιήση Ενίσχυσης GPU ", 212 "settings.app.form.enableGPUAcceleration": "ΕνεÏγοποιήση Ενίσχυσης GPU ",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "ΕνεÏγοποίηση οÏθογÏÎ±Ï†Î¹ÎºÎ¿Ï ÎµÎ»Î­Î³Ï‡Î¿Ï…", 217 "settings.app.form.enableSpellchecking": "ΕνεÏγοποίηση οÏθογÏÎ±Ï†Î¹ÎºÎ¿Ï ÎµÎ»Î­Î³Ï‡Î¿Ï…",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "ΕνεÏγοποίηση ειδοποιήσεων", 312 "settings.service.form.enableNotification": "ΕνεÏγοποίηση ειδοποιήσεων",
311 "settings.service.form.enableService": "ΕνεÏγοποίηση υπηÏεσίας", 313 "settings.service.form.enableService": "ΕνεÏγοποίηση υπηÏεσίας",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Εικονίδια μη αναγνωσμένου μηνÏματος", 315 "settings.service.form.headlineBadges": "Εικονίδια μη αναγνωσμένου μηνÏματος",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "Γενικά", 317 "settings.service.form.headlineGeneral": "Γενικά",
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index 812ec8c4c..d5cf072f6 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Enable Dark Mode", 211 "settings.app.form.darkMode": "Enable Dark Mode",
212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration", 212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Enable spell checking", 217 "settings.app.form.enableSpellchecking": "Enable spell checking",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Enable notifications", 312 "settings.service.form.enableNotification": "Enable notifications",
311 "settings.service.form.enableService": "Enable service", 313 "settings.service.form.enableService": "Enable service",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Unread message badges", 315 "settings.service.form.headlineBadges": "Unread message badges",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "General", 317 "settings.service.form.headlineGeneral": "General",
diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json
index 518e862ba..8f3f05c2c 100644
--- a/src/i18n/locales/es.json
+++ b/src/i18n/locales/es.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Servidor de TODO personalizado", 210 "settings.app.form.customTodoServer": "Servidor de TODO personalizado",
211 "settings.app.form.darkMode": "Habilitar modo oscuro", 211 "settings.app.form.darkMode": "Habilitar modo oscuro",
212 "settings.app.form.enableGPUAcceleration": "Habilitar aceleración de GPU", 212 "settings.app.form.enableGPUAcceleration": "Habilitar aceleración de GPU",
213 "settings.app.form.enableGlobalHideShortcut": "Habilitar acceso directo global para ocultar Ferdi",
213 "settings.app.form.enableLock": "Activar bloqueo por contraseña", 214 "settings.app.form.enableLock": "Activar bloqueo por contraseña",
215 "settings.app.form.enableLongPressServiceHint": "Habilitar sugerencia de acceso directo al servicio con pulsación larga",
214 "settings.app.form.enableMenuBar": "Mostrar a Ferdi en la barra de menús", 216 "settings.app.form.enableMenuBar": "Mostrar a Ferdi en la barra de menús",
215 "settings.app.form.enableSpellchecking": "Activar corrección ortográfica", 217 "settings.app.form.enableSpellchecking": "Activar corrección ortográfica",
216 "settings.app.form.enableSystemTray": "Mostrar siempre icono de Ferdi en la bandeja del sistema", 218 "settings.app.form.enableSystemTray": "Mostrar siempre icono de Ferdi en la bandeja del sistema",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Habilitar hibernación", 311 "settings.service.form.enableHibernation": "Habilitar hibernación",
310 "settings.service.form.enableNotification": "Activar notificaciones", 312 "settings.service.form.enableNotification": "Activar notificaciones",
311 "settings.service.form.enableService": "Activar servicio", 313 "settings.service.form.enableService": "Activar servicio",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Insignias de mensaje no leídos", 315 "settings.service.form.headlineBadges": "Insignias de mensaje no leídos",
313 "settings.service.form.headlineDarkReaderSettings": "Configuración del Lector Oscuro", 316 "settings.service.form.headlineDarkReaderSettings": "Configuración del Lector Oscuro",
314 "settings.service.form.headlineGeneral": "General", 317 "settings.service.form.headlineGeneral": "General",
diff --git a/src/i18n/locales/fi.json b/src/i18n/locales/fi.json
index e6c756935..dbac3aa59 100644
--- a/src/i18n/locales/fi.json
+++ b/src/i18n/locales/fi.json
@@ -186,7 +186,7 @@
186 "settings.account.tryReloadServices": "Yritä uudelleen", 186 "settings.account.tryReloadServices": "Yritä uudelleen",
187 "settings.account.tryReloadUserInfoRequest": "Yritä uudelleen", 187 "settings.account.tryReloadUserInfoRequest": "Yritä uudelleen",
188 "settings.account.userInfoRequestFailed": "Käyttäjätietoja ei voitu ladata", 188 "settings.account.userInfoRequestFailed": "Käyttäjätietoja ei voitu ladata",
189 "settings.account.yourLicense": "Your Ferdi License:", 189 "settings.account.yourLicense": "Ferdi-lisenssisi:",
190 "settings.app.accentColorInfo": "Write your accent color in a CSS-compatible format. (Default: {defaultAccentColor})", 190 "settings.app.accentColorInfo": "Write your accent color in a CSS-compatible format. (Default: {defaultAccentColor})",
191 "settings.app.buttonClearAllCache": "Tyhjennä välimuisti", 191 "settings.app.buttonClearAllCache": "Tyhjennä välimuisti",
192 "settings.app.buttonInstallUpdate": "Käynnistä uudelleen ja asenna päivitys", 192 "settings.app.buttonInstallUpdate": "Käynnistä uudelleen ja asenna päivitys",
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Ota tumma tila käyttöön", 211 "settings.app.form.darkMode": "Ota tumma tila käyttöön",
212 "settings.app.form.enableGPUAcceleration": "Ota Gpu-kiihdytys käyttöön", 212 "settings.app.form.enableGPUAcceleration": "Ota Gpu-kiihdytys käyttöön",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Ota Salasanan lukitus käyttöön", 214 "settings.app.form.enableLock": "Ota Salasanan lukitus käyttöön",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Ota oikoluku käyttöön", 217 "settings.app.form.enableSpellchecking": "Ota oikoluku käyttöön",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Ota ilmoitukset käyttöön", 312 "settings.service.form.enableNotification": "Ota ilmoitukset käyttöön",
311 "settings.service.form.enableService": "Ota palvelu käyttöön", 313 "settings.service.form.enableService": "Ota palvelu käyttöön",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Lukemattomat viestit", 315 "settings.service.form.headlineBadges": "Lukemattomat viestit",
313 "settings.service.form.headlineDarkReaderSettings": "Tumman tilan asetukset", 316 "settings.service.form.headlineDarkReaderSettings": "Tumman tilan asetukset",
314 "settings.service.form.headlineGeneral": "Yleinen", 317 "settings.service.form.headlineGeneral": "Yleinen",
diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json
index 80b32f495..7e2c591e2 100644
--- a/src/i18n/locales/fr.json
+++ b/src/i18n/locales/fr.json
@@ -53,7 +53,7 @@
53 "infobar.requiredRequestsFailed": "Impossible d'accéder aux services et informations de l'utilisateur", 53 "infobar.requiredRequestsFailed": "Impossible d'accéder aux services et informations de l'utilisateur",
54 "infobar.servicesUpdated": "Vos services ont été mis à jour.", 54 "infobar.servicesUpdated": "Vos services ont été mis à jour.",
55 "infobar.updateAvailable": "Une nouvelle mise à jour de Ferdi est disponible.", 55 "infobar.updateAvailable": "Une nouvelle mise à jour de Ferdi est disponible.",
56 "infobox.dismiss": "Dismiss", 56 "infobox.dismiss": "Abandonner",
57 "invite.email.label": "Adresse Email", 57 "invite.email.label": "Adresse Email",
58 "invite.headline.friends": "Invitez 3 amis ou collègues", 58 "invite.headline.friends": "Invitez 3 amis ou collègues",
59 "invite.name.label": "Nom", 59 "invite.name.label": "Nom",
@@ -74,7 +74,7 @@
74 "login.email.label": "Adresse Email", 74 "login.email.label": "Adresse Email",
75 "login.headline": "S'identifier", 75 "login.headline": "S'identifier",
76 "login.invalidCredentials": "Email ou mot de passe invalide", 76 "login.invalidCredentials": "Email ou mot de passe invalide",
77 "login.link.password": "Reset password", 77 "login.link.password": "Réinitialiser le mot de passe",
78 "login.link.signup": "Créer un compte gratuit", 78 "login.link.signup": "Créer un compte gratuit",
79 "login.password.label": "Mot de passe", 79 "login.password.label": "Mot de passe",
80 "login.serverLogout": "Votre session a expiré. Reconnectez-vous s'il vous plaît.", 80 "login.serverLogout": "Votre session a expiré. Reconnectez-vous s'il vous plaît.",
@@ -86,40 +86,40 @@
86 "menu.app.autohideMenuBar": "Cacher automatiquement la barre de menu", 86 "menu.app.autohideMenuBar": "Cacher automatiquement la barre de menu",
87 "menu.app.checkForUpdates": "Vérifier les mises à jour", 87 "menu.app.checkForUpdates": "Vérifier les mises à jour",
88 "menu.app.hide": "Masquer", 88 "menu.app.hide": "Masquer",
89 "menu.app.hideOthers": "Hide Others", 89 "menu.app.hideOthers": "Masquer les autres",
90 "menu.app.unhide": "Unhide", 90 "menu.app.unhide": "Afficher l’élément masqué",
91 "menu.edit": "Éditer", 91 "menu.edit": "Éditer",
92 "menu.edit.copy": "Copy", 92 "menu.edit.copy": "Copier",
93 "menu.edit.cut": "Cut", 93 "menu.edit.cut": "Couper",
94 "menu.edit.delete": "Supprimer", 94 "menu.edit.delete": "Supprimer",
95 "menu.edit.emojiSymbols": "Emoji & Symboles", 95 "menu.edit.emojiSymbols": "Emoji & Symboles",
96 "menu.edit.findInPage": "Rechercher dans la page", 96 "menu.edit.findInPage": "Rechercher dans la page",
97 "menu.edit.paste": "Paste", 97 "menu.edit.paste": "Coller",
98 "menu.edit.pasteAndMatchStyle": "Paste And Match Style", 98 "menu.edit.pasteAndMatchStyle": "Coller en conservant la mise en forme",
99 "menu.edit.redo": "Redo", 99 "menu.edit.redo": "Rétablir",
100 "menu.edit.selectAll": "Select All", 100 "menu.edit.selectAll": "Sélectionner tout",
101 "menu.edit.speech": "Synthèse vocale", 101 "menu.edit.speech": "Synthèse vocale",
102 "menu.edit.startDictation": "Démarrer la dictée", 102 "menu.edit.startDictation": "Démarrer la dictée",
103 "menu.edit.startSpeaking": "Démarrer la synthèse vocale", 103 "menu.edit.startSpeaking": "Démarrer la synthèse vocale",
104 "menu.edit.stopSpeaking": "Arrêter la synthèse vocale", 104 "menu.edit.stopSpeaking": "Arrêter la synthèse vocale",
105 "menu.edit.undo": "Undo", 105 "menu.edit.undo": "Annuler",
106 "menu.file": "Fichier", 106 "menu.file": "Fichier",
107 "menu.help": "Help", 107 "menu.help": "Aide",
108 "menu.help.changelog": "Liste des modifications", 108 "menu.help.changelog": "Liste des modifications",
109 "menu.help.debugInfo": "Copier les informations de Debug", 109 "menu.help.debugInfo": "Copier les informations de Debug",
110 "menu.help.debugInfoCopiedBody": "Les informations de Debug ont été copié dans votre presse-papier.", 110 "menu.help.debugInfoCopiedBody": "Les informations de Debug ont été copié dans votre presse-papier.",
111 "menu.help.debugInfoCopiedHeadline": "Information de Debug de Ferdi", 111 "menu.help.debugInfoCopiedHeadline": "Information de Debug de Ferdi",
112 "menu.help.importExportData": "Import/Export Configuration Data", 112 "menu.help.importExportData": "Importer/Exporter les données de configuration",
113 "menu.help.learnMore": "En savoir plus", 113 "menu.help.learnMore": "En savoir plus",
114 "menu.help.privacy": "Déclaration de confidentialité", 114 "menu.help.privacy": "Déclaration de confidentialité",
115 "menu.help.publishDebugInfo": "Publier les informations de débogage", 115 "menu.help.publishDebugInfo": "Publier les informations de débogage",
116 "menu.help.support": "Assistance", 116 "menu.help.support": "Assistance",
117 "menu.help.tos": "Conditions d'utilisation", 117 "menu.help.tos": "Conditions d'utilisation",
118 "menu.services": "Services", 118 "menu.services": "Services",
119 "menu.services.activatePreviousService": "Activate previous service", 119 "menu.services.activatePreviousService": "Activer le service précédent",
120 "menu.services.addNewService": "Add New Service...", 120 "menu.services.addNewService": "Ajouter un nouveau service...",
121 "menu.services.goHome": "Accueil", 121 "menu.services.goHome": "Accueil",
122 "menu.services.setNextServiceActive": "Activate next service", 122 "menu.services.setNextServiceActive": "Activer le service suivant",
123 "menu.todos": "Todos", 123 "menu.todos": "Todos",
124 "menu.todos.enableTodos": "Activer Todos", 124 "menu.todos.enableTodos": "Activer Todos",
125 "menu.view": "Aperçu", 125 "menu.view": "Aperçu",
@@ -127,7 +127,7 @@
127 "menu.view.forward": "Avancer", 127 "menu.view.forward": "Avancer",
128 "menu.view.lockFerdi": "Verrouiller Ferdi", 128 "menu.view.lockFerdi": "Verrouiller Ferdi",
129 "menu.view.openQuickSwitch": "Ouvrir le changement rapide", 129 "menu.view.openQuickSwitch": "Ouvrir le changement rapide",
130 "menu.view.reloadFerdi": "Reload Ferdi", 130 "menu.view.reloadFerdi": "Recharger Ferdi",
131 "menu.view.reloadService": "Redémarrer le service", 131 "menu.view.reloadService": "Redémarrer le service",
132 "menu.view.reloadTodos": "Recharger les Todos", 132 "menu.view.reloadTodos": "Recharger les Todos",
133 "menu.view.resetZoom": "Taille actuelle", 133 "menu.view.resetZoom": "Taille actuelle",
@@ -147,11 +147,11 @@
147 "menu.workspaces.defaultWorkspace": "Tous les services", 147 "menu.workspaces.defaultWorkspace": "Tous les services",
148 "menu.workspaces.openWorkspaceDrawer": "Ouvrir l'espace de travail", 148 "menu.workspaces.openWorkspaceDrawer": "Ouvrir l'espace de travail",
149 "password.email.label": "Adresse Email", 149 "password.email.label": "Adresse Email",
150 "password.headline": "Reset password", 150 "password.headline": "Réinitialiser le mot de passe",
151 "password.link.login": "Connectez-vous à votre compte", 151 "password.link.login": "Connectez-vous à votre compte",
152 "password.link.signup": "Créer un compte gratuit", 152 "password.link.signup": "Créer un compte gratuit",
153 "password.noUser": "Aucun utilisateur n'a été trouvé avec cette adresse email", 153 "password.noUser": "Aucun utilisateur n'a été trouvé avec cette adresse de courriel",
154 "password.successInfo": "Your new password was sent to your email address", 154 "password.successInfo": "Votre nouveau mot de passe a été envoyé à votre adresse de courriel",
155 "service.crashHandler.action": "Recharger {name}", 155 "service.crashHandler.action": "Recharger {name}",
156 "service.crashHandler.autoReload": "Tentative de restauration automatique de {name} dans {seconds} secondes", 156 "service.crashHandler.autoReload": "Tentative de restauration automatique de {name} dans {seconds} secondes",
157 "service.crashHandler.headline": "Oh non!", 157 "service.crashHandler.headline": "Oh non!",
@@ -166,7 +166,7 @@
166 "service.webviewLoader.loading": "Chargement de {service}", 166 "service.webviewLoader.loading": "Chargement de {service}",
167 "services.getStarted": "Commencer", 167 "services.getStarted": "Commencer",
168 "services.login": "Veuillez vous connecter pour utiliser Ferdi.", 168 "services.login": "Veuillez vous connecter pour utiliser Ferdi.",
169 "services.serverInfo": "Optionally, you can change your Ferdi server by clicking the cog in the bottom left corner. If you are switching over (from one of the hosted servers) to using Ferdi without an account, please be informed that you can export your data from that server and subsequently import it using the Help menu to resurrect all your workspaces and configured services!", 169 "services.serverInfo": "Optionnellement, vous pouvez changer de serveur Ferdi en cliquant sur le contrôle dans le coin inférieur gauche. Si vous basculez d'un serveur hébergé à une utilisation de Ferdi sans compte, vous pouvez exporter vos données depuis ce serveur et ensuite l'importer en utilisant le menu Aide pour ressusciter tous vos espaces de travail et services configurés !",
170 "services.serverless": "Utiliser Ferdi sans compte", 170 "services.serverless": "Utiliser Ferdi sans compte",
171 "services.welcome": "Bienvenue dans Ferdi", 171 "services.welcome": "Bienvenue dans Ferdi",
172 "settings.account.account.editButton": "Modifier le compte", 172 "settings.account.account.editButton": "Modifier le compte",
@@ -187,11 +187,11 @@
187 "settings.account.tryReloadUserInfoRequest": "Réessayer", 187 "settings.account.tryReloadUserInfoRequest": "Réessayer",
188 "settings.account.userInfoRequestFailed": "Impossible de charger les informations de l'utilisateur", 188 "settings.account.userInfoRequestFailed": "Impossible de charger les informations de l'utilisateur",
189 "settings.account.yourLicense": "Votre licence Ferdi :", 189 "settings.account.yourLicense": "Votre licence Ferdi :",
190 "settings.app.accentColorInfo": "Write your accent color in a CSS-compatible format. (Default: {defaultAccentColor})", 190 "settings.app.accentColorInfo": "Écrivez votre couleur d'accentuation dans un format compatible avec CSS. (Par défaut: {defaultAccentColor})",
191 "settings.app.buttonClearAllCache": "Vider le cache", 191 "settings.app.buttonClearAllCache": "Vider le cache",
192 "settings.app.buttonInstallUpdate": "Redémarrer et installer la mise à jour", 192 "settings.app.buttonInstallUpdate": "Redémarrer et installer la mise à jour",
193 "settings.app.buttonOpenFerdiProfileFolder": "Afficher le dossier du profil", 193 "settings.app.buttonOpenFerdiProfileFolder": "Afficher le dossier du profil",
194 "settings.app.buttonOpenFerdiServiceRecipesFolder": "Open Service Recipes folder", 194 "settings.app.buttonOpenFerdiServiceRecipesFolder": "Ouvrir le dossier des recettes de service (Recipes)",
195 "settings.app.buttonSearchForUpdate": "Vérifier les mises à jour", 195 "settings.app.buttonSearchForUpdate": "Vérifier les mises à jour",
196 "settings.app.cacheInfo": "Le cache de Ferdi occupe actuellement {size} en espace disque.", 196 "settings.app.cacheInfo": "Le cache de Ferdi occupe actuellement {size} en espace disque.",
197 "settings.app.cacheNotCleared": "Impossible de vider toute la cache", 197 "settings.app.cacheNotCleared": "Impossible de vider toute la cache",
@@ -204,16 +204,18 @@
204 "settings.app.form.autoLaunchOnStart": "Lancer Ferdi au démarrage", 204 "settings.app.form.autoLaunchOnStart": "Lancer Ferdi au démarrage",
205 "settings.app.form.automaticUpdates": "Activer les mises à jour", 205 "settings.app.form.automaticUpdates": "Activer les mises à jour",
206 "settings.app.form.beta": "Accepter les versions bêta", 206 "settings.app.form.beta": "Accepter les versions bêta",
207 "settings.app.form.clipboardNotifications": "Don't show notifications for clipboard events", 207 "settings.app.form.clipboardNotifications": "Ne pas afficher les notifications pour les événements du presse-papiers",
208 "settings.app.form.closeToSystemTray": "Afficher Ferdi dans la barre d'état système", 208 "settings.app.form.closeToSystemTray": "Lors de la fermeture, laisser Ferdi actif dans la barre d'état système",
209 "settings.app.form.confirmOnQuit": "Confirm when quitting Ferdi", 209 "settings.app.form.confirmOnQuit": "Confirmer lorsque vous quittez Ferdi",
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Serveur Todo personnalisé",
211 "settings.app.form.darkMode": "Activer le mode sombre", 211 "settings.app.form.darkMode": "Activer le mode sombre",
212 "settings.app.form.enableGPUAcceleration": "Activer l'accélération GPU", 212 "settings.app.form.enableGPUAcceleration": "Activer l'accélération GPU",
213 "settings.app.form.enableGlobalHideShortcut": "Activer le raccourci global pour masquer Ferdi",
213 "settings.app.form.enableLock": "Activer le verrouillage par mot de passe", 214 "settings.app.form.enableLock": "Activer le verrouillage par mot de passe",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 215 "settings.app.form.enableLongPressServiceHint": "Activer le service lors d'un appui long sur son raccourci",
216 "settings.app.form.enableMenuBar": "Toujours afficher Ferdi dans la barre de menu",
215 "settings.app.form.enableSpellchecking": "Activer la vérification orthographique", 217 "settings.app.form.enableSpellchecking": "Activer la vérification orthographique",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Toujours afficher l'icône Ferdi dans la zone de notification",
217 "settings.app.form.enableTodos": "Activer Ferdi Todos", 219 "settings.app.form.enableTodos": "Activer Ferdi Todos",
218 "settings.app.form.hibernateOnStartup": "Garder les services en veille prolongée au démarrage", 220 "settings.app.form.hibernateOnStartup": "Garder les services en veille prolongée au démarrage",
219 "settings.app.form.hibernationStrategy": "Stratégie d'hibernation", 221 "settings.app.form.hibernationStrategy": "Stratégie d'hibernation",
@@ -225,7 +227,7 @@
225 "settings.app.form.minimizeToSystemTray": "Minimiser Ferdi dans la zone de notification", 227 "settings.app.form.minimizeToSystemTray": "Minimiser Ferdi dans la zone de notification",
226 "settings.app.form.navigationBarBehaviour": "Comportement de la barre de navigation", 228 "settings.app.form.navigationBarBehaviour": "Comportement de la barre de navigation",
227 "settings.app.form.notifyTaskBarOnMessage": "Notification sur la barre des tâches/le dock lors d'un nouveau message", 229 "settings.app.form.notifyTaskBarOnMessage": "Notification sur la barre des tâches/le dock lors d'un nouveau message",
228 "settings.app.form.passwordToggle": "Password toggle", 230 "settings.app.form.passwordToggle": "Afficher le mot de passe",
229 "settings.app.form.predefinedTodoServer": "Serveur Todo", 231 "settings.app.form.predefinedTodoServer": "Serveur Todo",
230 "settings.app.form.privateNotifications": "Ne pas afficher le contenu des notifications", 232 "settings.app.form.privateNotifications": "Ne pas afficher le contenu des notifications",
231 "settings.app.form.reloadAfterResume": "Recharger Ferdi après la reprise du système", 233 "settings.app.form.reloadAfterResume": "Recharger Ferdi après la reprise du système",
@@ -233,38 +235,38 @@
233 "settings.app.form.scheduledDNDEnabled": "Activer Ne-pas-Déranger", 235 "settings.app.form.scheduledDNDEnabled": "Activer Ne-pas-Déranger",
234 "settings.app.form.scheduledDNDEnd": "À", 236 "settings.app.form.scheduledDNDEnd": "À",
235 "settings.app.form.scheduledDNDStart": "De", 237 "settings.app.form.scheduledDNDStart": "De",
236 "settings.app.form.searchEngine": "Search engine", 238 "settings.app.form.searchEngine": "Moteur de recherche",
237 "settings.app.form.sentry": "Envoyer des données de télémétrie", 239 "settings.app.form.sentry": "Envoyer des données de télémétrie",
238 "settings.app.form.serviceRibbonWidth": "Largeur du menu", 240 "settings.app.form.serviceRibbonWidth": "Largeur du menu",
239 "settings.app.form.showDisabledServices": "Afficher les onglets des services désactivés", 241 "settings.app.form.showDisabledServices": "Afficher les onglets des services désactivés",
240 "settings.app.form.showDragArea": "Afficher les zones de glisser-déposer dans la fenêtre", 242 "settings.app.form.showDragArea": "Afficher les zones de glisser-déposer dans la fenêtre",
241 "settings.app.form.showMessagesBadgesWhenMuted": "Afficher les badges de messages non lus quand les notifications sont désactivées", 243 "settings.app.form.showMessagesBadgesWhenMuted": "Afficher les badges de messages non lus quand les notifications sont désactivées",
242 "settings.app.form.splitMode": "Enable Split View Mode", 244 "settings.app.form.splitMode": "Activer le Mode Vue Divisée",
243 "settings.app.form.startMinimized": "Démarrage minimisé", 245 "settings.app.form.startMinimized": "Démarrage minimisé",
244 "settings.app.form.universalDarkMode": "Activer le mode sombre universel", 246 "settings.app.form.universalDarkMode": "Activer le mode sombre universel",
245 "settings.app.form.useTouchIdToUnlock": "Allow using TouchID to unlock Ferdi", 247 "settings.app.form.useTouchIdToUnlock": "Autoriser l'utilisation du système de lecture d'empreinte digitale pour déverrouiller Ferdi",
246 "settings.app.form.useVerticalStyle": "Use horizontal style", 248 "settings.app.form.useVerticalStyle": "Utiliser un style horizontal",
247 "settings.app.form.wakeUpStrategy": "Wake up strategy", 249 "settings.app.form.wakeUpStrategy": "Stratégie de réveil",
248 "settings.app.headlineAdvanced": "Paramètres avancés", 250 "settings.app.headlineAdvanced": "Paramètres avancés",
249 "settings.app.headlineAppearance": "Apparence", 251 "settings.app.headlineAppearance": "Apparence",
250 "settings.app.headlineGeneral": "Général", 252 "settings.app.headlineGeneral": "Général",
251 "settings.app.headlineLanguage": "Langue", 253 "settings.app.headlineLanguage": "Langue",
252 "settings.app.headlinePrivacy": "Privacy", 254 "settings.app.headlinePrivacy": "Confidentialité",
253 "settings.app.headlineUpdates": "Mises à jour", 255 "settings.app.headlineUpdates": "Mises à jour",
254 "settings.app.hibernateInfo": "Par défaut, Ferdi gardera tous vos services ouverts et chargés en arrière-plan afin qu'ils soient prêts lorsque vous voulez les utiliser. Le service d'hibernation déchargera vos services après un montant spécifié. Ceci est utile pour sauver de la RAM ou garder les services de ralentir votre ordinateur.", 256 "settings.app.hibernateInfo": "Par défaut, Ferdi gardera tous vos services ouverts et chargés en arrière-plan afin qu'ils soient prêts lorsque vous voulez les utiliser. Le service d'hibernation déchargera vos services après un montant spécifié. Ceci est utile pour sauver de la RAM ou garder les services de ralentir votre ordinateur.",
255 "settings.app.inactivityLockInfo": "Minutes d'inactivité, après lesquelles Ferdi devrait se verrouiller automatiquement. Utilisez 0 pour désactiver", 257 "settings.app.inactivityLockInfo": "Minutes d'inactivité, après lesquelles Ferdi devrait se verrouiller automatiquement. Utilisez 0 pour désactiver",
256 "settings.app.languageDisclaimer": "Les traductions officielles sont l'anglais et l'allemand. Toutes les autres langues sont des traductions faites par la communauté.", 258 "settings.app.languageDisclaimer": "Les traductions officielles sont l'anglais et l'allemand. Toutes les autres langues sont des traductions faites par la communauté.",
257 "settings.app.lockInfo": "Password Lock allows you to keep your messages protected.\nUsing Password Lock, you will be prompted to enter your password everytime you start Ferdi or lock Ferdi yourself using the lock symbol in the bottom left corner or the shortcut {lockShortcut}.", 259 "settings.app.lockInfo": "Le verrouillage par mot de passe vous permet de garder vos messages protégés.\nPour l'utilisation du verrouillage par mot de passe, vous serez invité à entrer votre mot de passe chaque fois que vous démarrez Ferdi ou que vous verrouillerez Ferdi vous-même en utilisant le symbole de verrouillage en bas à gauche ou le raccourci {lockShortcut}.",
258 "settings.app.lockedPassword": "Mot de passe", 260 "settings.app.lockedPassword": "Mot de passe",
259 "settings.app.lockedPasswordInfo": "Please make sure to set a password you'll remember.\nIf you loose this password, you will have to reinstall Ferdi.", 261 "settings.app.lockedPasswordInfo": "Veuillez vous assurer de définir un mot de passe dont vous vous souviendrez.\nSi vous perdez ce mot de passe, vous devrez réinstaller Ferdi.",
260 "settings.app.restartRequired": "Les modifications nécessitent un redémarrage", 262 "settings.app.restartRequired": "Les modifications nécessitent un redémarrage",
261 "settings.app.scheduledDNDInfo": "Planifier le Ne-pas-Déranger vous permet de définir une période de temps dans lequel vous ne voulez pas de notifications de Ferdi.", 263 "settings.app.scheduledDNDInfo": "Planifier le Ne-pas-Déranger vous permet de définir une période de temps dans lequel vous ne voulez pas de notifications de Ferdi.",
262 "settings.app.scheduledDNDTimeInfo": "Le temps est en format 24 heures. La fin du temps peut être avant le début du temps (ex: début 17:00, fin 09:00) pour activer le Ne-pas-Déranger durant la nuit.", 264 "settings.app.scheduledDNDTimeInfo": "Le temps est en format 24 heures. La fin du temps peut être avant le début du temps (ex: début 17:00, fin 09:00) pour activer le Ne-pas-Déranger durant la nuit.",
263 "settings.app.sentryInfo": "Sending telemetry data allows us to find errors in Ferdi - we will not send any personal information like your message data!", 265 "settings.app.sentryInfo": "L'envoi de données de télémétrie nous permet de trouver des erreurs dans Ferdi - nous n'enverrons aucune information personnelle comme vos données de message!",
264 "settings.app.spellCheckerLanguageInfo": "Ferdi utilise le correcteur orthographique intégré de votre Mac pour vérifier les fautes de frappe. Si vous voulez changer les langues pour lesquelles le correcteur vérifie l'orthographe, vous pouvez le faire dans les préférences système de votre Mac.", 266 "settings.app.spellCheckerLanguageInfo": "Ferdi utilise le correcteur orthographique intégré de votre Mac pour vérifier les fautes de frappe. Si vous voulez changer les langues pour lesquelles le correcteur vérifie l'orthographe, vous pouvez le faire dans les préférences système de votre Mac.",
265 "settings.app.subheadlineCache": "Cache", 267 "settings.app.subheadlineCache": "Cache",
266 "settings.app.subheadlineFerdiProfile": "Ferdi Profile", 268 "settings.app.subheadlineFerdiProfile": "Profil Ferdi",
267 "settings.app.todoServerInfo": "This server will be used for the \"Ferdi Todo\" feature.", 269 "settings.app.todoServerInfo": "Ce serveur sera utilisé pour la fonctionnalité \"Ferdi Todo\".",
268 "settings.app.translationHelp": "Aidez-nous à traduire Ferdi dans votre langue.", 270 "settings.app.translationHelp": "Aidez-nous à traduire Ferdi dans votre langue.",
269 "settings.app.universalDarkModeInfo": "Le mode sombre universel tente de générer dynamiquement des styles de mode sombre pour les services qui ne sont pas encore supportés.", 271 "settings.app.universalDarkModeInfo": "Le mode sombre universel tente de générer dynamiquement des styles de mode sombre pour les services qui ne sont pas encore supportés.",
270 "settings.app.updateStatusAvailable": "Mise à jour disponible, téléchargement en cours...", 272 "settings.app.updateStatusAvailable": "Mise à jour disponible, téléchargement en cours...",
@@ -283,12 +285,12 @@
283 "settings.recipes.customService.headline.communityRecipes": "Recettes tiers communautaire", 285 "settings.recipes.customService.headline.communityRecipes": "Recettes tiers communautaire",
284 "settings.recipes.customService.headline.customRecipes": "Recettes tiers modifiées", 286 "settings.recipes.customService.headline.customRecipes": "Recettes tiers modifiées",
285 "settings.recipes.customService.headline.devRecipes": "Vos recettes de service de développement", 287 "settings.recipes.customService.headline.devRecipes": "Vos recettes de service de développement",
286 "settings.recipes.customService.intro": "To add a custom service, copy the service recipe to:", 288 "settings.recipes.customService.intro": "Pour ajouter un service personnalisé, copiez la recette du service à:",
287 "settings.recipes.customService.openDevDocs": "Documentation de Développeur", 289 "settings.recipes.customService.openDevDocs": "Documentation de Développeur",
288 "settings.recipes.customService.openFolder": "Open folder", 290 "settings.recipes.customService.openFolder": "Ouvrir le dossier",
289 "settings.recipes.headline": "Services disponibles", 291 "settings.recipes.headline": "Services disponibles",
290 "settings.recipes.missingService": "Un service est manquant?", 292 "settings.recipes.missingService": "Un service est manquant?",
291 "settings.recipes.nothingFound": "Sorry, but no service matched your search term - but you can still probably add it using the \"Custom Website\" option. Please note that the website might show more services that have been added to Ferdi since the version that you are currently on. To get those new services, please consider upgrading to a newer version of Ferdi.", 293 "settings.recipes.nothingFound": "Désolé, mais aucun service ne correspond à votre recherche - mais vous pouvez toujours l'ajouter en utilisant l'option \"Custom Website\". Veuillez noter que le site Web pourrait afficher plus de services qui ont été ajoutés à Ferdi depuis la version sur laquelle vous êtes actuellement. Pour obtenir ces nouveaux services, veuillez envisager la mise à niveau vers une version plus récente de Ferdi.",
292 "settings.recipes.servicesSuccessfulAddedInfo": "Le service a été ajouté avec succès", 294 "settings.recipes.servicesSuccessfulAddedInfo": "Le service a été ajouté avec succès",
293 "settings.searchService": "Chercher un service", 295 "settings.searchService": "Chercher un service",
294 "settings.service.error.goBack": "Retour aux services", 296 "settings.service.error.goBack": "Retour aux services",
@@ -301,14 +303,15 @@
301 "settings.service.form.darkReaderBrightness": "Luminosité de Dark Reader", 303 "settings.service.form.darkReaderBrightness": "Luminosité de Dark Reader",
302 "settings.service.form.darkReaderContrast": "Contraste de Dark Reader", 304 "settings.service.form.darkReaderContrast": "Contraste de Dark Reader",
303 "settings.service.form.darkReaderSepia": "Sepia de Dark Reader", 305 "settings.service.form.darkReaderSepia": "Sepia de Dark Reader",
304 "settings.service.form.deleteButton": "Delete service", 306 "settings.service.form.deleteButton": "Supprimer le service",
305 "settings.service.form.editServiceHeadline": "Modifier {name}", 307 "settings.service.form.editServiceHeadline": "Modifier {name}",
306 "settings.service.form.enableAudio": "Activer l'audio", 308 "settings.service.form.enableAudio": "Activer l'audio",
307 "settings.service.form.enableBadge": "Afficher le badge des messages non lus", 309 "settings.service.form.enableBadge": "Afficher le badge des messages non lus",
308 "settings.service.form.enableDarkMode": "Activer le mode sombre", 310 "settings.service.form.enableDarkMode": "Activer le mode sombre",
309 "settings.service.form.enableHibernation": "Activer l'hibernation", 311 "settings.service.form.enableHibernation": "Activer la veille prolongée",
310 "settings.service.form.enableNotification": "Activer les notifications", 312 "settings.service.form.enableNotification": "Activer les notifications",
311 "settings.service.form.enableService": "Activer le service", 313 "settings.service.form.enableService": "Activer le service",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Badge des messages non lus", 315 "settings.service.form.headlineBadges": "Badge des messages non lus",
313 "settings.service.form.headlineDarkReaderSettings": "Paramètre de Dark Reader", 316 "settings.service.form.headlineDarkReaderSettings": "Paramètre de Dark Reader",
314 "settings.service.form.headlineGeneral": "Général", 317 "settings.service.form.headlineGeneral": "Général",
@@ -321,18 +324,18 @@
321 "settings.service.form.isHibernatedEnabledInfo": "Quand activé, les services seront éteints après un moment pour économiser les ressources systèmes.", 324 "settings.service.form.isHibernatedEnabledInfo": "Quand activé, les services seront éteints après un moment pour économiser les ressources systèmes.",
322 "settings.service.form.isMutedInfo": "Lorsque désactivé, tous les sons de notifications ainsi que l'audio sont coupés", 325 "settings.service.form.isMutedInfo": "Lorsque désactivé, tous les sons de notifications ainsi que l'audio sont coupés",
323 "settings.service.form.name": "Nom", 326 "settings.service.form.name": "Nom",
324 "settings.service.form.onlyShowFavoritesInUnreadCount": "Only show Favorites in unread count", 327 "settings.service.form.onlyShowFavoritesInUnreadCount": "Afficher uniquement les Favoris dans le nombre de non-lus",
325 "settings.service.form.openDarkmodeCss": "Ouvrir darkmode.css", 328 "settings.service.form.openDarkmodeCss": "Ouvrir darkmode.css",
326 "settings.service.form.openUserCss": "Ouvrir user.css", 329 "settings.service.form.openUserCss": "Ouvrir user.css",
327 "settings.service.form.openUserJs": "Ouvrir user.js", 330 "settings.service.form.openUserJs": "Ouvrir user.js",
328 "settings.service.form.proxy.headline": "Paramètres proxy HTTP/HTTPS", 331 "settings.service.form.proxy.headline": "Paramètres proxy HTTP/HTTPS",
329 "settings.service.form.proxy.host": "Hôte/IP du proxy", 332 "settings.service.form.proxy.host": "Hôte/IP du proxy",
330 "settings.service.form.proxy.info": "Proxy settings will not be synchronized with the Ferdi servers.", 333 "settings.service.form.proxy.info": "Les paramètres du proxy ne seront pas synchronisés avec les serveurs Ferdi.",
331 "settings.service.form.proxy.isEnabled": "Utiliser un proxy", 334 "settings.service.form.proxy.isEnabled": "Utiliser un proxy",
332 "settings.service.form.proxy.password": "Password (optional)", 335 "settings.service.form.proxy.password": "Mot de passe (facultatif)",
333 "settings.service.form.proxy.port": "Port", 336 "settings.service.form.proxy.port": "Port",
334 "settings.service.form.proxy.restartInfo": "Veuillez redémarrer Ferdi après avoir modifié les paramètres proxy.", 337 "settings.service.form.proxy.restartInfo": "Veuillez redémarrer Ferdi après avoir modifié les paramètres proxy.",
335 "settings.service.form.proxy.user": "User (optional)", 338 "settings.service.form.proxy.user": "Utilisateur (facultatif)",
336 "settings.service.form.recipeFileInfo": "Vos fichiers utilisateur seront insérés dans la page Web afin que vous puissiez personnaliser les services comme vous le souhaitez. Les fichiers utilisateurs sont stockés localement et ne sont pas transférés à d'autres ordinateurs en utilisant le même compte.", 339 "settings.service.form.recipeFileInfo": "Vos fichiers utilisateur seront insérés dans la page Web afin que vous puissiez personnaliser les services comme vous le souhaitez. Les fichiers utilisateurs sont stockés localement et ne sont pas transférés à d'autres ordinateurs en utilisant le même compte.",
337 "settings.service.form.saveButton": "Enregistrer le service", 340 "settings.service.form.saveButton": "Enregistrer le service",
338 "settings.service.form.tabHosted": "Hébergé", 341 "settings.service.form.tabHosted": "Hébergé",
@@ -344,7 +347,7 @@
344 "settings.services.discoverServices": "Découvrir les services", 347 "settings.services.discoverServices": "Découvrir les services",
345 "settings.services.headline": "Vos services", 348 "settings.services.headline": "Vos services",
346 "settings.services.noServicesAdded": "Commencez par ajouter un service.", 349 "settings.services.noServicesAdded": "Commencez par ajouter un service.",
347 "settings.services.nothingFound": "Sorry, but no service matched your search term - but you can still probably add it using the \"Custom Website\" option. Please note that the website might show more services that have been added to Ferdi since the version that you are currently on. To get those new services, please consider upgrading to a newer version of Ferdi.", 350 "settings.services.nothingFound": "Désolé, mais aucun service ne correspond à votre recherche - mais vous pouvez toujours l'ajouter en utilisant l'option \"Custom Website\". Veuillez noter que le site Web pourrait afficher plus de services qui ont été ajoutés à Ferdi depuis la version sur laquelle vous êtes actuellement. Pour obtenir ces nouveaux services, veuillez envisager la mise à niveau vers une version plus récente de Ferdi.",
348 "settings.services.servicesRequestFailed": "Impossible de charger vos services", 351 "settings.services.servicesRequestFailed": "Impossible de charger vos services",
349 "settings.services.tooltip.isDisabled": "Ce service est désactivé", 352 "settings.services.tooltip.isDisabled": "Ce service est désactivé",
350 "settings.services.tooltip.isMuted": "Tous les sons sont coupés", 353 "settings.services.tooltip.isMuted": "Tous les sons sont coupés",
@@ -353,22 +356,22 @@
353 "settings.supportFerdi.aboutIntro": "<p>Ferdi est une application open-source et dirigée par la communauté.</p><p>Merci à toutes les personnes qui rendent cela possible :</p>", 356 "settings.supportFerdi.aboutIntro": "<p>Ferdi est une application open-source et dirigée par la communauté.</p><p>Merci à toutes les personnes qui rendent cela possible :</p>",
354 "settings.supportFerdi.bannerText": "Voulez-vous nous aider à améliorer Ferdi ?", 357 "settings.supportFerdi.bannerText": "Voulez-vous nous aider à améliorer Ferdi ?",
355 "settings.supportFerdi.headline": "À propos de Ferdi", 358 "settings.supportFerdi.headline": "À propos de Ferdi",
356 "settings.supportFerdi.openSurvey": "Open survey", 359 "settings.supportFerdi.openSurvey": "Ouvrir l'enquête",
357 "settings.supportFerdi.textDonation": "Si vous avez envie de soutenir le développement de Ferdi par un don, vous pouvez le faire via", 360 "settings.supportFerdi.textDonation": "Si vous avez envie de soutenir le développement de Ferdi par un don, vous pouvez le faire via",
358 "settings.supportFerdi.textDonationAnd": "et", 361 "settings.supportFerdi.textDonationAnd": "et",
359 "settings.supportFerdi.textExpenses": "Bien que les bénévoles fassent la majeure partie du travail, nous avons besoin de payer pour les serveurs et les certificats. En tant que communauté, nous sommes totalement transparents sur les fonds que nous collectons et dépensons - voir nos", 362 "settings.supportFerdi.textExpenses": "Bien que les bénévoles fassent la majeure partie du travail, nous avons besoin de payer pour les serveurs et les certificats. En tant que communauté, nous sommes totalement transparents sur les fonds que nous collectons et dépensons - voir nos",
360 "settings.supportFerdi.textGitHubSponsors": "Sponsors sur GitHub", 363 "settings.supportFerdi.textGitHubSponsors": "Sponsors sur GitHub",
361 "settings.supportFerdi.textListContributors": "Full list of contributors", 364 "settings.supportFerdi.textListContributors": "Liste complète des contributeurs",
362 "settings.supportFerdi.textListContributorsHere": "ici", 365 "settings.supportFerdi.textListContributorsHere": "ici",
363 "settings.supportFerdi.textOpenCollective": "Open Collective", 366 "settings.supportFerdi.textOpenCollective": "Open Collective",
364 "settings.supportFerdi.textSupportWelcome": "Toute aide est toujours la bienvenue. Vous pouvez trouver une liste de l'aide dont nous avons besoin", 367 "settings.supportFerdi.textSupportWelcome": "Toute aide est toujours la bienvenue. Vous pouvez trouver une liste de l'aide dont nous avons besoin",
365 "settings.supportFerdi.textSupportWelcomeHere": "ici", 368 "settings.supportFerdi.textSupportWelcomeHere": "ici",
366 "settings.supportFerdi.textVolunteers": "Le développement de Ferdi est fait par des bénévoles. Des gens qui utilisent Ferdi comme vous maintiennent, réparent et améliorent Ferdi pendant leur temps libre.", 369 "settings.supportFerdi.textVolunteers": "Le développement de Ferdi est fait par des bénévoles. Des gens qui utilisent Ferdi comme vous. Ils maintiennent, réparent et améliorent Ferdi pendant leur temps libre.",
367 "settings.supportFerdi.title": "Vous aimez Ferdi ?", 370 "settings.supportFerdi.title": "Vous aimez Ferdi ?",
368 "settings.team.contentHeadline": "Gestion d'équipe Franz", 371 "settings.team.contentHeadline": "Gestion d'équipe Franz",
369 "settings.team.copy": "La gestion d'équipe de Franz vous permet de gérer les abonnements Franz pour plusieurs utilisateurs. N’oubliez pas que le fait d’avoir un abonnement Franz Premium ne vous donnera aucun avantage dans l'utilisation de Ferdi : la seule raison pour laquelle vous avez encore accès à la gestion d’équipe est que vous pouvez gérer vos équipes Franz héritées et que vous ne perdez donc aucune fonctionnalité dans la gestion de votre compte.", 372 "settings.team.copy": "La gestion d'équipe de Franz vous permet de gérer les abonnements Franz pour plusieurs utilisateurs. N’oubliez pas que le fait d’avoir un abonnement Franz Premium ne vous donnera aucun avantage dans l'utilisation de Ferdi : la seule raison pour laquelle vous avez encore accès à la gestion d’équipe est que vous pouvez gérer vos équipes Franz héritées et que vous ne perdez donc aucune fonctionnalité dans la gestion de votre compte.",
370 "settings.team.headline": "Équipe", 373 "settings.team.headline": "Équipe",
371 "settings.team.intro": "You are currently using Franz Servers, which is why you have access to Team Management.", 374 "settings.team.intro": "Vous utilisez actuellement les serveurs Franz, c'est pourquoi vous avez accès à la gestion d'équipe.",
372 "settings.team.manageAction": "Gérez votre équipe sur meetfranz.com", 375 "settings.team.manageAction": "Gérez votre équipe sur meetfranz.com",
373 "settings.team.teamsUnavailable": "Les équipes sont indisponibles", 376 "settings.team.teamsUnavailable": "Les équipes sont indisponibles",
374 "settings.team.teamsUnavailableInfo": "Les équipes sont actuellement disponibles uniquement lorsque vous utilisez le serveur de Franz et après avoir payé pour Franz Professionnel. Veuillez changer votre serveur à https://api.franzinfra.com pour utiliser des équipes.", 377 "settings.team.teamsUnavailableInfo": "Les équipes sont actuellement disponibles uniquement lorsque vous utilisez le serveur de Franz et après avoir payé pour Franz Professionnel. Veuillez changer votre serveur à https://api.franzinfra.com pour utiliser des équipes.",
@@ -378,8 +381,8 @@
378 "settings.user.form.accountType.non-profit": "Non-lucratif", 381 "settings.user.form.accountType.non-profit": "Non-lucratif",
379 "settings.user.form.currentPassword": "Mot de passe actuel", 382 "settings.user.form.currentPassword": "Mot de passe actuel",
380 "settings.user.form.email": "Email", 383 "settings.user.form.email": "Email",
381 "settings.user.form.firstname": "First Name", 384 "settings.user.form.firstname": "Prénom",
382 "settings.user.form.lastname": "Last Name", 385 "settings.user.form.lastname": "Nom",
383 "settings.user.form.newPassword": "Nouveau mot de passe", 386 "settings.user.form.newPassword": "Nouveau mot de passe",
384 "settings.workspace.add.form.name": "Nom", 387 "settings.workspace.add.form.name": "Nom",
385 "settings.workspace.add.form.submitButton": "Créer l'espace de travail", 388 "settings.workspace.add.form.submitButton": "Créer l'espace de travail",
@@ -396,7 +399,7 @@
396 "settings.workspaces.tryReloadWorkspaces": "Réessayer", 399 "settings.workspaces.tryReloadWorkspaces": "Réessayer",
397 "settings.workspaces.updatedInfo": "Vos modifications ont été enregistrées", 400 "settings.workspaces.updatedInfo": "Vos modifications ont été enregistrées",
398 "settings.workspaces.workspaceFeatureHeadline": "Présentation des Espaces de travail de Ferdi", 401 "settings.workspaces.workspaceFeatureHeadline": "Présentation des Espaces de travail de Ferdi",
399 "settings.workspaces.workspaceFeatureInfo": "Ferdi Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time. You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.", 402 "settings.workspaces.workspaceFeatureInfo": "Les Espaces de travail de Ferdi vous permettant de rester concentré sur ce qui est important. Créez différents groupes de services et naviguez facilement entre eux à n'importe quel moment. Vous décidez de quels services vous avez besoin, où et quand, ainsi nous pouvons vous aider à rester concentré sur votre travail - ou à le quitter dès que vous le souhaitez.",
400 "settings.workspaces.workspacesRequestFailed": "Impossible de charger vos espaces de travail", 403 "settings.workspaces.workspacesRequestFailed": "Impossible de charger vos espaces de travail",
401 "setupAssistant.headline": "Commençons", 404 "setupAssistant.headline": "Commençons",
402 "setupAssistant.subheadline": "Choisissez parmi nos services les plus utilisés et revenez maintenant sur votre messagerie.", 405 "setupAssistant.subheadline": "Choisissez parmi nos services les plus utilisés et revenez maintenant sur votre messagerie.",
@@ -411,33 +414,33 @@
411 "sidebar.unmuteApp": "Activer les notifications et les sons", 414 "sidebar.unmuteApp": "Activer les notifications et les sons",
412 "signup.email.label": "Adresse Email", 415 "signup.email.label": "Adresse Email",
413 "signup.emailDuplicate": "Cette adresse email est déjà utilisée", 416 "signup.emailDuplicate": "Cette adresse email est déjà utilisée",
414 "signup.firstname.label": "First Name", 417 "signup.firstname.label": "Prénom",
415 "signup.headline": "S'inscrire", 418 "signup.headline": "S'inscrire",
416 "signup.lastname.label": "Last Name", 419 "signup.lastname.label": "Nom",
417 "signup.legal.info": "En créant un compte Ferdi, vous acceptez la", 420 "signup.legal.info": "En créant un compte Ferdi, vous acceptez la",
418 "signup.legal.privacy": "Déclaration de confidentialité", 421 "signup.legal.privacy": "Déclaration de confidentialité",
419 "signup.legal.terms": "Conditions d'utilisation", 422 "signup.legal.terms": "Conditions d'utilisation",
420 "signup.link.login": "Vous avez déjà un compte? Connectez-vous", 423 "signup.link.login": "Vous avez déjà un compte? Connectez-vous",
421 "signup.password.label": "Mot de passe", 424 "signup.password.label": "Mot de passe",
422 "signup.submit.label": "Créer un compte", 425 "signup.submit.label": "Créer un compte",
423 "tabs.item.confirmDeleteService": "Do you really want to delete the {serviceName} service?", 426 "tabs.item.confirmDeleteService": "Voulez-vous vraiment supprimer le service {serviceName}?",
424 "tabs.item.deleteService": "Delete service", 427 "tabs.item.deleteService": "Supprimer le service",
425 "tabs.item.disableAudio": "Désactiver l'audio", 428 "tabs.item.disableAudio": "Désactiver l'audio",
426 "tabs.item.disableDarkMode": "Disable Dark mode", 429 "tabs.item.disableDarkMode": "Désactiver le mode sombre",
427 "tabs.item.disableNotifications": "Désactiver les notifications", 430 "tabs.item.disableNotifications": "Désactiver les notifications",
428 "tabs.item.disableService": "Disable service", 431 "tabs.item.disableService": "Désactiver le service",
429 "tabs.item.enableAudio": "Activer l'audio", 432 "tabs.item.enableAudio": "Activer l'audio",
430 "tabs.item.enableDarkMode": "Enable Dark mode", 433 "tabs.item.enableDarkMode": "Activer le mode sombre",
431 "tabs.item.enableNotification": "Activer les notifications", 434 "tabs.item.enableNotification": "Activer les notifications",
432 "tabs.item.enableService": "Activer le service", 435 "tabs.item.enableService": "Activer le service",
433 "tabs.item.hibernateService": "Hibernate service", 436 "tabs.item.hibernateService": "Service de mise en veille prolongée",
434 "tabs.item.reload": "Actualiser", 437 "tabs.item.reload": "Actualiser",
435 "tabs.item.wakeUpService": "Wake up service", 438 "tabs.item.wakeUpService": "Service de réveil",
436 "validation.email": "{field} is not valid", 439 "validation.email": "{field} n'est pas valide",
437 "validation.minLength": "{field} should be at least {length} characters long", 440 "validation.minLength": "{field} doit comporter au moins {length} caractère(s)",
438 "validation.oneRequired": "Au moins un de ces champs est requis", 441 "validation.oneRequired": "Au moins un de ces champs est requis",
439 "validation.required": "{field} is required", 442 "validation.required": "{field} est requis",
440 "validation.url": "{field} is not a valid URL", 443 "validation.url": "{field} n'est pas une URL valide",
441 "webControls.back": "Revenir", 444 "webControls.back": "Revenir",
442 "webControls.forward": "Avancer", 445 "webControls.forward": "Avancer",
443 "webControls.goHome": "Accueil", 446 "webControls.goHome": "Accueil",
@@ -445,12 +448,12 @@
445 "webControls.reload": "Actualiser", 448 "webControls.reload": "Actualiser",
446 "welcome.loginButton": "Se connecter sur son compte", 449 "welcome.loginButton": "Se connecter sur son compte",
447 "welcome.signupButton": "Créer un compte gratuit", 450 "welcome.signupButton": "Créer un compte gratuit",
448 "workspaceDrawer.addNewWorkspaceLabel": "Add new workspace", 451 "workspaceDrawer.addNewWorkspaceLabel": "Ajouter un nouvel espace de travail",
449 "workspaceDrawer.allServices": "Tous les services", 452 "workspaceDrawer.allServices": "Tous les services",
450 "workspaceDrawer.headline": "Espace de travail", 453 "workspaceDrawer.headline": "Espace de travail",
451 "workspaceDrawer.item.contextMenuEdit": "Modifier", 454 "workspaceDrawer.item.contextMenuEdit": "Modifier",
452 "workspaceDrawer.item.noServicesAddedYet": "Aucun services ajoutés pour l'instant", 455 "workspaceDrawer.item.noServicesAddedYet": "Aucun services ajoutés pour l'instant",
453 "workspaceDrawer.workspaceFeatureInfo": "<p>Ferdi Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time.</p><p>You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.</p>", 456 "workspaceDrawer.workspaceFeatureInfo": "<p>Les Espaces de travail de Ferdi vous permettant de rester concentré sur ce qui est important. Créez différents groupes de services et naviguez facilement entre eux à n'importe quel moment.</p><p>Vous décidez de quels services vous avez besoin, où et quand, ainsi nous pouvons vous aider à rester concentré sur votre travail - ou à le quitter dès que vous le souhaitez.</p>",
454 "workspaceDrawer.workspacesSettingsTooltip": "Edit workspaces settings", 457 "workspaceDrawer.workspacesSettingsTooltip": "Éditer les paramètres de l'espace de travail",
455 "workspaces.switchingIndicator.switchingTo": "Changement vers" 458 "workspaces.switchingIndicator.switchingTo": "Changement vers"
456} 459}
diff --git a/src/i18n/locales/ga.json b/src/i18n/locales/ga.json
index 46dc1acc4..ed893c943 100644
--- a/src/i18n/locales/ga.json
+++ b/src/i18n/locales/ga.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Enable Dark Mode", 211 "settings.app.form.darkMode": "Enable Dark Mode",
212 "settings.app.form.enableGPUAcceleration": "Cumasaigh luasghéarú APG", 212 "settings.app.form.enableGPUAcceleration": "Cumasaigh luasghéarú APG",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Cumasaigh seiceáil litrithe", 217 "settings.app.form.enableSpellchecking": "Cumasaigh seiceáil litrithe",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Cumasaigh fógraí", 312 "settings.service.form.enableNotification": "Cumasaigh fógraí",
311 "settings.service.form.enableService": "Cumasaigh seirbhís", 313 "settings.service.form.enableService": "Cumasaigh seirbhís",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Comhartha do theachtaireachtaí neamhléite", 315 "settings.service.form.headlineBadges": "Comhartha do theachtaireachtaí neamhléite",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "Ginearálta", 317 "settings.service.form.headlineGeneral": "Ginearálta",
diff --git a/src/i18n/locales/he.json b/src/i18n/locales/he.json
index 5bec0bf13..1ba7c5ada 100644
--- a/src/i18n/locales/he.json
+++ b/src/i18n/locales/he.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "×פשר מצב לילה", 211 "settings.app.form.darkMode": "×פשר מצב לילה",
212 "settings.app.form.enableGPUAcceleration": "×פשר ×”×צת חומרה (GPU)", 212 "settings.app.form.enableGPUAcceleration": "×פשר ×”×צת חומרה (GPU)",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "×פשר נעילה ב×מצעות סיסמ×", 214 "settings.app.form.enableLock": "×פשר נעילה ב×מצעות סיסמ×",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "הפעל בדיקת ×יות", 217 "settings.app.form.enableSpellchecking": "הפעל בדיקת ×יות",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "×פשר התר×ות", 312 "settings.service.form.enableNotification": "×פשר התר×ות",
311 "settings.service.form.enableService": "×פשר שירות", 313 "settings.service.form.enableService": "×פשר שירות",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "חיווי על הודעות ×©×œ× × ×§×¨×ו", 315 "settings.service.form.headlineBadges": "חיווי על הודעות ×©×œ× × ×§×¨×ו",
313 "settings.service.form.headlineDarkReaderSettings": "הגדרות מצב לילה", 316 "settings.service.form.headlineDarkReaderSettings": "הגדרות מצב לילה",
314 "settings.service.form.headlineGeneral": "כללי", 317 "settings.service.form.headlineGeneral": "כללי",
diff --git a/src/i18n/locales/hi.json b/src/i18n/locales/hi.json
index 812ec8c4c..d5cf072f6 100644
--- a/src/i18n/locales/hi.json
+++ b/src/i18n/locales/hi.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Enable Dark Mode", 211 "settings.app.form.darkMode": "Enable Dark Mode",
212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration", 212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Enable spell checking", 217 "settings.app.form.enableSpellchecking": "Enable spell checking",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Enable notifications", 312 "settings.service.form.enableNotification": "Enable notifications",
311 "settings.service.form.enableService": "Enable service", 313 "settings.service.form.enableService": "Enable service",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Unread message badges", 315 "settings.service.form.headlineBadges": "Unread message badges",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "General", 317 "settings.service.form.headlineGeneral": "General",
diff --git a/src/i18n/locales/hr.json b/src/i18n/locales/hr.json
index 6dcd65032..6f0677e16 100644
--- a/src/i18n/locales/hr.json
+++ b/src/i18n/locales/hr.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Enable Dark Mode", 211 "settings.app.form.darkMode": "Enable Dark Mode",
212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration", 212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Omogući provjeru pravopisa", 217 "settings.app.form.enableSpellchecking": "Omogući provjeru pravopisa",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Omogućite obavijesti", 312 "settings.service.form.enableNotification": "Omogućite obavijesti",
311 "settings.service.form.enableService": "Omogućite usluge", 313 "settings.service.form.enableService": "Omogućite usluge",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Unread message badges", 315 "settings.service.form.headlineBadges": "Unread message badges",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "Općenito", 317 "settings.service.form.headlineGeneral": "Općenito",
diff --git a/src/i18n/locales/hu.json b/src/i18n/locales/hu.json
index 8d83fea42..8a9c1a441 100644
--- a/src/i18n/locales/hu.json
+++ b/src/i18n/locales/hu.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Sötét mód engedélyezése", 211 "settings.app.form.darkMode": "Sötét mód engedélyezése",
212 "settings.app.form.enableGPUAcceleration": "Hardveres gyorsítás engedélyezése", 212 "settings.app.form.enableGPUAcceleration": "Hardveres gyorsítás engedélyezése",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Jelszavas zár engedélyezése", 214 "settings.app.form.enableLock": "Jelszavas zár engedélyezése",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Helyesírás-ellenőrzés engedélyezése", 217 "settings.app.form.enableSpellchecking": "Helyesírás-ellenőrzés engedélyezése",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Értesítések engedélyezése", 312 "settings.service.form.enableNotification": "Értesítések engedélyezése",
311 "settings.service.form.enableService": "Szolgáltatás engedélyezése", 313 "settings.service.form.enableService": "Szolgáltatás engedélyezése",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Olvasatlan üzenet jelzések", 315 "settings.service.form.headlineBadges": "Olvasatlan üzenet jelzések",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "Ãltalános", 317 "settings.service.form.headlineGeneral": "Ãltalános",
diff --git a/src/i18n/locales/id.json b/src/i18n/locales/id.json
index 5a2aa1ea7..a57b60247 100644
--- a/src/i18n/locales/id.json
+++ b/src/i18n/locales/id.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Aktifkan Mode Gelap", 211 "settings.app.form.darkMode": "Aktifkan Mode Gelap",
212 "settings.app.form.enableGPUAcceleration": "Aktifkan Akselerasi GPU", 212 "settings.app.form.enableGPUAcceleration": "Aktifkan Akselerasi GPU",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Aktifkan pemeriksaan ejaan", 217 "settings.app.form.enableSpellchecking": "Aktifkan pemeriksaan ejaan",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Aktifkan pemberitahuan", 312 "settings.service.form.enableNotification": "Aktifkan pemberitahuan",
311 "settings.service.form.enableService": "Aktifkan layanan", 313 "settings.service.form.enableService": "Aktifkan layanan",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Lencana pesan belum dibaca", 315 "settings.service.form.headlineBadges": "Lencana pesan belum dibaca",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "Umum", 317 "settings.service.form.headlineGeneral": "Umum",
diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json
index 004bd2d26..09cfaaf25 100644
--- a/src/i18n/locales/it.json
+++ b/src/i18n/locales/it.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Server Todo personalizzato", 210 "settings.app.form.customTodoServer": "Server Todo personalizzato",
211 "settings.app.form.darkMode": "Attiva la modalità scura.", 211 "settings.app.form.darkMode": "Attiva la modalità scura.",
212 "settings.app.form.enableGPUAcceleration": "Attiva Accelerazione GPU", 212 "settings.app.form.enableGPUAcceleration": "Attiva Accelerazione GPU",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Abilita blocco con password", 214 "settings.app.form.enableLock": "Abilita blocco con password",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Mostra sempre Ferdi nella barra dei menù", 216 "settings.app.form.enableMenuBar": "Mostra sempre Ferdi nella barra dei menù",
215 "settings.app.form.enableSpellchecking": "Attiva controllo ortografico", 217 "settings.app.form.enableSpellchecking": "Attiva controllo ortografico",
216 "settings.app.form.enableSystemTray": "Mostra sempre icona Ferdi in area di notifica", 218 "settings.app.form.enableSystemTray": "Mostra sempre icona Ferdi in area di notifica",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Abilita ibernazione", 311 "settings.service.form.enableHibernation": "Abilita ibernazione",
310 "settings.service.form.enableNotification": "Attiva le notifiche", 312 "settings.service.form.enableNotification": "Attiva le notifiche",
311 "settings.service.form.enableService": "Attiva il servizio", 313 "settings.service.form.enableService": "Attiva il servizio",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Etichetta dei messaggi non letti", 315 "settings.service.form.headlineBadges": "Etichetta dei messaggi non letti",
313 "settings.service.form.headlineDarkReaderSettings": "Impostazioni della Modalità Scura", 316 "settings.service.form.headlineDarkReaderSettings": "Impostazioni della Modalità Scura",
314 "settings.service.form.headlineGeneral": "Generale", 317 "settings.service.form.headlineGeneral": "Generale",
diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json
index d07ed53af..54257c599 100644
--- a/src/i18n/locales/ja.json
+++ b/src/i18n/locales/ja.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "カスタムToDoサーãƒãƒ¼", 210 "settings.app.form.customTodoServer": "カスタムToDoサーãƒãƒ¼",
211 "settings.app.form.darkMode": "ダークモードを有効ã«ã™ã‚‹", 211 "settings.app.form.darkMode": "ダークモードを有効ã«ã™ã‚‹",
212 "settings.app.form.enableGPUAcceleration": "GPUアクセラレーションを有効ã«ã™ã‚‹", 212 "settings.app.form.enableGPUAcceleration": "GPUアクセラレーションを有効ã«ã™ã‚‹",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "パスワードロックを有効ã«ã™ã‚‹", 214 "settings.app.form.enableLock": "パスワードロックを有効ã«ã™ã‚‹",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Ferdi内ã«ãƒ¡ãƒ‹ãƒ¥ãƒ¼ãƒãƒ¼ã‚’常時表示ã™ã‚‹", 216 "settings.app.form.enableMenuBar": "Ferdi内ã«ãƒ¡ãƒ‹ãƒ¥ãƒ¼ãƒãƒ¼ã‚’常時表示ã™ã‚‹",
215 "settings.app.form.enableSpellchecking": "スペルãƒã‚§ãƒƒã‚¯ã‚’有効ã«ã™ã‚‹", 217 "settings.app.form.enableSpellchecking": "スペルãƒã‚§ãƒƒã‚¯ã‚’有効ã«ã™ã‚‹",
216 "settings.app.form.enableSystemTray": "Ferdiを常時システムトレイã«è¡¨ç¤ºã™ã‚‹", 218 "settings.app.form.enableSystemTray": "Ferdiを常時システムトレイã«è¡¨ç¤ºã™ã‚‹",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "休止を有効ã«ã™ã‚‹", 311 "settings.service.form.enableHibernation": "休止を有効ã«ã™ã‚‹",
310 "settings.service.form.enableNotification": "通知を有効ã«ã™ã‚‹", 312 "settings.service.form.enableNotification": "通知を有効ã«ã™ã‚‹",
311 "settings.service.form.enableService": "サービスを有効ã«ã™ã‚‹", 313 "settings.service.form.enableService": "サービスを有効ã«ã™ã‚‹",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "未読件数ã®é€šçŸ¥ãƒãƒƒã‚¸", 315 "settings.service.form.headlineBadges": "未読件数ã®é€šçŸ¥ãƒãƒƒã‚¸",
313 "settings.service.form.headlineDarkReaderSettings": "ダークリーダー設定", 316 "settings.service.form.headlineDarkReaderSettings": "ダークリーダー設定",
314 "settings.service.form.headlineGeneral": "一般", 317 "settings.service.form.headlineGeneral": "一般",
diff --git a/src/i18n/locales/ka.json b/src/i18n/locales/ka.json
index f327c1e51..fee1f6668 100644
--- a/src/i18n/locales/ka.json
+++ b/src/i18n/locales/ka.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Enable Dark Mode", 211 "settings.app.form.darkMode": "Enable Dark Mode",
212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration", 212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Enable spell checking", 217 "settings.app.form.enableSpellchecking": "Enable spell checking",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "შეტყáƒáƒ‘ინებების ჩáƒáƒ áƒ—ვáƒ", 312 "settings.service.form.enableNotification": "შეტყáƒáƒ‘ინებების ჩáƒáƒ áƒ—ვáƒ",
311 "settings.service.form.enableService": "სერვისის ჩáƒáƒ áƒ—ვáƒ", 313 "settings.service.form.enableService": "სერვისის ჩáƒáƒ áƒ—ვáƒ",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Unread message badges", 315 "settings.service.form.headlineBadges": "Unread message badges",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "მთáƒáƒ•áƒáƒ áƒ˜", 317 "settings.service.form.headlineGeneral": "მთáƒáƒ•áƒáƒ áƒ˜",
diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json
index 3be2b27ff..c66313751 100644
--- a/src/i18n/locales/ko.json
+++ b/src/i18n/locales/ko.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "ë‹¤í¬ ëª¨ë“œ 활성화", 211 "settings.app.form.darkMode": "ë‹¤í¬ ëª¨ë“œ 활성화",
212 "settings.app.form.enableGPUAcceleration": "GPU ê°€ì† ì‚¬ìš©", 212 "settings.app.form.enableGPUAcceleration": "GPU ê°€ì† ì‚¬ìš©",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "암호 잠금 활성화", 214 "settings.app.form.enableLock": "암호 잠금 활성화",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "맞춤법 검사 활성화", 217 "settings.app.form.enableSpellchecking": "맞춤법 검사 활성화",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "절전모드 활성화", 311 "settings.service.form.enableHibernation": "절전모드 활성화",
310 "settings.service.form.enableNotification": "알림 허용", 312 "settings.service.form.enableNotification": "알림 허용",
311 "settings.service.form.enableService": "서비스 활성화", 313 "settings.service.form.enableService": "서비스 활성화",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "ì½ì§€ ì•Šì€ ë©”ì„¸ì§€ 배지 표시", 315 "settings.service.form.headlineBadges": "ì½ì§€ ì•Šì€ ë©”ì„¸ì§€ 배지 표시",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "ì¼ë°˜", 317 "settings.service.form.headlineGeneral": "ì¼ë°˜",
diff --git a/src/i18n/locales/nl-BE.json b/src/i18n/locales/nl-BE.json
index 5104d1bc2..1cd2cdc42 100644
--- a/src/i18n/locales/nl-BE.json
+++ b/src/i18n/locales/nl-BE.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Dark Mode aanzetten", 211 "settings.app.form.darkMode": "Dark Mode aanzetten",
212 "settings.app.form.enableGPUAcceleration": "GPU Acceleratie Activeren", 212 "settings.app.form.enableGPUAcceleration": "GPU Acceleratie Activeren",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Spellingcontrole inschakelen", 217 "settings.app.form.enableSpellchecking": "Spellingcontrole inschakelen",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Notificaties aanzetten", 312 "settings.service.form.enableNotification": "Notificaties aanzetten",
311 "settings.service.form.enableService": "Service aanzetten", 313 "settings.service.form.enableService": "Service aanzetten",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Ongelezen berichten badges", 315 "settings.service.form.headlineBadges": "Ongelezen berichten badges",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "Algemeen", 317 "settings.service.form.headlineGeneral": "Algemeen",
diff --git a/src/i18n/locales/nl.json b/src/i18n/locales/nl.json
index 6899e02ee..4d58e5611 100644
--- a/src/i18n/locales/nl.json
+++ b/src/i18n/locales/nl.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Aangepaste Todo Server", 210 "settings.app.form.customTodoServer": "Aangepaste Todo Server",
211 "settings.app.form.darkMode": "Dark mode aanzetten", 211 "settings.app.form.darkMode": "Dark mode aanzetten",
212 "settings.app.form.enableGPUAcceleration": "Schakel videokaart-acceleratie in ", 212 "settings.app.form.enableGPUAcceleration": "Schakel videokaart-acceleratie in ",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Wachtwoordvergrendeling inschakelen", 214 "settings.app.form.enableLock": "Wachtwoordvergrendeling inschakelen",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Altijd Ferdi weergeven in de menubalk", 216 "settings.app.form.enableMenuBar": "Altijd Ferdi weergeven in de menubalk",
215 "settings.app.form.enableSpellchecking": "Zet spellingcontrole aan", 217 "settings.app.form.enableSpellchecking": "Zet spellingcontrole aan",
216 "settings.app.form.enableSystemTray": "Laat Ferdi altijd in systeembalk zien", 218 "settings.app.form.enableSystemTray": "Laat Ferdi altijd in systeembalk zien",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Meldingen inschakelen", 312 "settings.service.form.enableNotification": "Meldingen inschakelen",
311 "settings.service.form.enableService": "Service inschakelen", 313 "settings.service.form.enableService": "Service inschakelen",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Ongelezen berichten badges", 315 "settings.service.form.headlineBadges": "Ongelezen berichten badges",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "Algemeen", 317 "settings.service.form.headlineGeneral": "Algemeen",
diff --git a/src/i18n/locales/no.json b/src/i18n/locales/no.json
index cfa523a35..0d927a6cd 100644
--- a/src/i18n/locales/no.json
+++ b/src/i18n/locales/no.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Aktiver mørkt tema", 211 "settings.app.form.darkMode": "Aktiver mørkt tema",
212 "settings.app.form.enableGPUAcceleration": "Aktiver GPU-akselerasjon", 212 "settings.app.form.enableGPUAcceleration": "Aktiver GPU-akselerasjon",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Aktiver passordlås", 214 "settings.app.form.enableLock": "Aktiver passordlås",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Aktiver stavekontroll", 217 "settings.app.form.enableSpellchecking": "Aktiver stavekontroll",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Aktiver varsler", 312 "settings.service.form.enableNotification": "Aktiver varsler",
311 "settings.service.form.enableService": "Aktivere tjenesten", 313 "settings.service.form.enableService": "Aktivere tjenesten",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Uleste meldingsetiketter", 315 "settings.service.form.headlineBadges": "Uleste meldingsetiketter",
313 "settings.service.form.headlineDarkReaderSettings": "Innstillinger for lesing i mørket", 316 "settings.service.form.headlineDarkReaderSettings": "Innstillinger for lesing i mørket",
314 "settings.service.form.headlineGeneral": "Generelt", 317 "settings.service.form.headlineGeneral": "Generelt",
diff --git a/src/i18n/locales/pl.json b/src/i18n/locales/pl.json
index a19c7c444..b6c3bc98c 100644
--- a/src/i18n/locales/pl.json
+++ b/src/i18n/locales/pl.json
@@ -1,6 +1,6 @@
1{ 1{
2 "app.errorHandler.action": "Odśwież", 2 "app.errorHandler.action": "Odśwież",
3 "app.errorHandler.headline": "Coś poszło nie tak.", 3 "app.errorHandler.headline": "Something went wrong.",
4 "changeserver.customServerLabel": "Spersonalizowany serwer", 4 "changeserver.customServerLabel": "Spersonalizowany serwer",
5 "changeserver.headline": "Zmień serwer", 5 "changeserver.headline": "Zmień serwer",
6 "changeserver.label": "Serwer", 6 "changeserver.label": "Serwer",
@@ -22,15 +22,15 @@
22 "feature.publishDebugInfo.title": "Opublikuj dane diagnostyczne", 22 "feature.publishDebugInfo.title": "Opublikuj dane diagnostyczne",
23 "feature.quickSwitch.info": "Wybierz usługę naciskając TAB, ↑ oraz ↓. Otwórz usługę naciskając ENTER.", 23 "feature.quickSwitch.info": "Wybierz usługę naciskając TAB, ↑ oraz ↓. Otwórz usługę naciskając ENTER.",
24 "feature.quickSwitch.search": "Szukaj...", 24 "feature.quickSwitch.search": "Szukaj...",
25 "feature.quickSwitch.title": "Szybkie przełączenie", 25 "feature.quickSwitch.title": "QuickSwitch",
26 "global.api.unhealthy": "Nie można połączyć z usługami Ferdi", 26 "global.api.unhealthy": "Can't connect to Ferdi online services",
27 "global.cancel": "Anuluj", 27 "global.cancel": "Anuluj",
28 "global.edit": "Edytuj", 28 "global.edit": "Edytuj",
29 "global.no": "Nie", 29 "global.no": "Nie",
30 "global.notConnectedToTheInternet": "Nie masz połączenia z Internetem.", 30 "global.notConnectedToTheInternet": "Nie masz połączenia z Internetem.",
31 "global.ok": "Ok", 31 "global.ok": "Ok",
32 "global.quit": "Wyjdź", 32 "global.quit": "Wyjdź",
33 "global.quitConfirmation": "Czy chcesz wyjść z Ferdi?", 33 "global.quitConfirmation": "Czy napewno chcesz wyjść z Ferdi?",
34 "global.save": "Zapisz", 34 "global.save": "Zapisz",
35 "global.settings": "Ustawienia", 35 "global.settings": "Ustawienia",
36 "global.spellchecker.useDefault": "Użyj domyślnego dla systemu ({default})", 36 "global.spellchecker.useDefault": "Użyj domyślnego dla systemu ({default})",
@@ -44,9 +44,9 @@
44 "import.headline": "Importuj usługi Ferdi 4", 44 "import.headline": "Importuj usługi Ferdi 4",
45 "import.notSupportedHeadline": "Usługi, które nie są jeszcze obsługiwane w Ferdi 5", 45 "import.notSupportedHeadline": "Usługi, które nie są jeszcze obsługiwane w Ferdi 5",
46 "import.skip.label": "Chcę dodać usługi samodzielnie", 46 "import.skip.label": "Chcę dodać usługi samodzielnie",
47 "import.submit.label": "Importuj {count} usług", 47 "import.submit.label": "Import {count} services",
48 "infobar.authRequestFailed": "Pojawiły się błędy podczas próby uwierzytelniania. Proszę spróbuj się wylogować i zalogować ponownie, jeśli ten błąd będzie się powtarzał.", 48 "infobar.authRequestFailed": "Pojawiły się błędy podczas próby uwierzytelniania. Proszę spróbuj się wylogować i zalogować ponownie, jeśli ten błąd będzie się powtarzał.",
49 "infobar.buttonChangelog": "Co nowego?", 49 "infobar.buttonChangelog": "What is new?",
50 "infobar.buttonInstallUpdate": "Uruchom ponownie i zainstaluj aktualizacjÄ™", 50 "infobar.buttonInstallUpdate": "Uruchom ponownie i zainstaluj aktualizacjÄ™",
51 "infobar.buttonReloadServices": "Odśwież usługi", 51 "infobar.buttonReloadServices": "Odśwież usługi",
52 "infobar.hide": "Ukryj", 52 "infobar.hide": "Ukryj",
@@ -69,13 +69,13 @@
69 "locked.touchIdPrompt": "odblokuj przez Touch ID", 69 "locked.touchIdPrompt": "odblokuj przez Touch ID",
70 "locked.unlockWithPassword": "Odblokuj hasłem", 70 "locked.unlockWithPassword": "Odblokuj hasłem",
71 "login.changeServer": "Zmień serwer", 71 "login.changeServer": "Zmień serwer",
72 "login.customServerQuestion": "Używasz niestandardowego serwera Ferdi?", 72 "login.customServerQuestion": "Using a custom Ferdi server?",
73 "login.customServerSuggestion": "Spróbuj zaimportować konto Franz", 73 "login.customServerSuggestion": "Try importing your Franz account",
74 "login.email.label": "Adres email", 74 "login.email.label": "Adres email",
75 "login.headline": "Zaloguj siÄ™", 75 "login.headline": "Zaloguj siÄ™",
76 "login.invalidCredentials": "Adres email lub hasło są błędne", 76 "login.invalidCredentials": "Adres email lub hasło są błędne",
77 "login.link.password": "Resetuj hasło", 77 "login.link.password": "Reset password",
78 "login.link.signup": "Załóż konto", 78 "login.link.signup": "Załóż darmowe konto",
79 "login.password.label": "Hasło", 79 "login.password.label": "Hasło",
80 "login.serverLogout": "Twoja sesja wygasła, zaloguj się ponownie.", 80 "login.serverLogout": "Twoja sesja wygasła, zaloguj się ponownie.",
81 "login.submit.label": "Zaloguj siÄ™", 81 "login.submit.label": "Zaloguj siÄ™",
@@ -116,17 +116,17 @@
116 "menu.help.support": "Wsparcie", 116 "menu.help.support": "Wsparcie",
117 "menu.help.tos": "Warunki użytkowania", 117 "menu.help.tos": "Warunki użytkowania",
118 "menu.services": "Usługi", 118 "menu.services": "Usługi",
119 "menu.services.activatePreviousService": "Aktywuj poprzednią usługę", 119 "menu.services.activatePreviousService": "Activate previous service",
120 "menu.services.addNewService": "Dodaj nową usługę...", 120 "menu.services.addNewService": "Add New Service...",
121 "menu.services.goHome": "Strona główna", 121 "menu.services.goHome": "Strona główna",
122 "menu.services.setNextServiceActive": "Aktywuj następną usługę", 122 "menu.services.setNextServiceActive": "Activate next service",
123 "menu.todos": "Lista zadań", 123 "menu.todos": "Lista zadań",
124 "menu.todos.enableTodos": "Włącz listę zadań", 124 "menu.todos.enableTodos": "Włącz listę zadań",
125 "menu.view": "Widok", 125 "menu.view": "Widok",
126 "menu.view.back": "Wstecz", 126 "menu.view.back": "Wstecz",
127 "menu.view.forward": "Prześlij dalej", 127 "menu.view.forward": "Prześlij dalej",
128 "menu.view.lockFerdi": "Zablokuj Ferdi", 128 "menu.view.lockFerdi": "Zablokuj Ferdi",
129 "menu.view.openQuickSwitch": "Otwórz szybki wybór", 129 "menu.view.openQuickSwitch": "Otwórz Quick Switch",
130 "menu.view.reloadFerdi": "Przeładuj Ferdi", 130 "menu.view.reloadFerdi": "Przeładuj Ferdi",
131 "menu.view.reloadService": "Przeładuj usługę", 131 "menu.view.reloadService": "Przeładuj usługę",
132 "menu.view.reloadTodos": "Odśwież Zadania", 132 "menu.view.reloadTodos": "Odśwież Zadania",
@@ -147,11 +147,11 @@
147 "menu.workspaces.defaultWorkspace": "Wszystkie usługi", 147 "menu.workspaces.defaultWorkspace": "Wszystkie usługi",
148 "menu.workspaces.openWorkspaceDrawer": "Otwórz edytor obszaru roboczego", 148 "menu.workspaces.openWorkspaceDrawer": "Otwórz edytor obszaru roboczego",
149 "password.email.label": "Adres email", 149 "password.email.label": "Adres email",
150 "password.headline": "Resetuj hasło", 150 "password.headline": "Reset password",
151 "password.link.login": "Zaloguj siÄ™ na swoje konto", 151 "password.link.login": "Zaloguj siÄ™ na swoje konto",
152 "password.link.signup": "Załóż darmowe konto", 152 "password.link.signup": "Załóż darmowe konto",
153 "password.noUser": "Użytkownik z podanym adresem email nie istnieje", 153 "password.noUser": "No user with that email address was found",
154 "password.successInfo": "Twoje nowe hasło zostało wysłane na Twoją skrzynkę mailową", 154 "password.successInfo": "Your new password was sent to your email address",
155 "service.crashHandler.action": "Przeładuj {name}", 155 "service.crashHandler.action": "Przeładuj {name}",
156 "service.crashHandler.autoReload": "Próba automatycznego odnowienia {name} za {seconds} sekund/y", 156 "service.crashHandler.autoReload": "Próba automatycznego odnowienia {name} za {seconds} sekund/y",
157 "service.crashHandler.headline": "O nie!", 157 "service.crashHandler.headline": "O nie!",
@@ -166,10 +166,10 @@
166 "service.webviewLoader.loading": "Wczytywanie {service}", 166 "service.webviewLoader.loading": "Wczytywanie {service}",
167 "services.getStarted": "Zacznij", 167 "services.getStarted": "Zacznij",
168 "services.login": "Zaloguj się, aby używać Ferdi.", 168 "services.login": "Zaloguj się, aby używać Ferdi.",
169 "services.serverInfo": "Opcjonalnie możesz zmienić swój serwer Ferdi klikając zębatkę w lewym dolnym rogu. Jeżeli przełączasz się (z jednego z hostowanych serwerów) do serwerów Ferdi bez konta, pamiętaj, że możesz wyeksportować swoje dane z tego serwera a następnie zaimportować je używając opcji Pomoc w menu aby przywrócić swoje obszary robocze i skonfigurowane usługi!", 169 "services.serverInfo": "Optionally, you can change your Ferdi server by clicking the cog in the bottom left corner. If you are switching over (from one of the hosted servers) to using Ferdi without an account, please be informed that you can export your data from that server and subsequently import it using the Help menu to resurrect all your workspaces and configured services!",
170 "services.serverless": "Używaj Ferdi bez konta", 170 "services.serverless": "Używaj Ferdi bez konta",
171 "services.welcome": "Witaj w programie Ferdi", 171 "services.welcome": "Witaj w programie Ferdi",
172 "settings.account.account.editButton": "Edytuj konto", 172 "settings.account.account.editButton": "Edit account",
173 "settings.account.accountUnavailable": "Konto jest niedostępne", 173 "settings.account.accountUnavailable": "Konto jest niedostępne",
174 "settings.account.accountUnavailableInfo": "Używasz Ferdi bez konta. Jeśli chcesz używać Ferdi z kontem i synchronizować swoje usługi pomiędzy klientami, wybierz serwer w zakładce Ustawienia i zaloguj się.", 174 "settings.account.accountUnavailableInfo": "Używasz Ferdi bez konta. Jeśli chcesz używać Ferdi z kontem i synchronizować swoje usługi pomiędzy klientami, wybierz serwer w zakładce Ustawienia i zaloguj się.",
175 "settings.account.buttonSave": "Uaktualnij profil", 175 "settings.account.buttonSave": "Uaktualnij profil",
@@ -177,21 +177,21 @@
177 "settings.account.deleteEmailSent": "Wysłaliśmy email z linkiem do potwierdzenia usunięcia konta. Konto oraz dane są usuwane trwale i nie można tego cofnąć!", 177 "settings.account.deleteEmailSent": "Wysłaliśmy email z linkiem do potwierdzenia usunięcia konta. Konto oraz dane są usuwane trwale i nie można tego cofnąć!",
178 "settings.account.deleteInfo": "Jeżeli nie potrzebujesz już konta Ferdi, możesz je usunąć oraz wszystkie dane na nim zapisane.", 178 "settings.account.deleteInfo": "Jeżeli nie potrzebujesz już konta Ferdi, możesz je usunąć oraz wszystkie dane na nim zapisane.",
179 "settings.account.headline": "Konto", 179 "settings.account.headline": "Konto",
180 "settings.account.headlineAccount": "Informacje o koncie", 180 "settings.account.headlineAccount": "Account information",
181 "settings.account.headlineDangerZone": "Strefa niebezpieczna", 181 "settings.account.headlineDangerZone": "Danger Zone",
182 "settings.account.headlineInvoices": "Faktury", 182 "settings.account.headlineInvoices": "Invoices",
183 "settings.account.headlinePassword": "Zmień hasło", 183 "settings.account.headlinePassword": "Change password",
184 "settings.account.headlineProfile": "Uaktualnij profil", 184 "settings.account.headlineProfile": "Uaktualnij profil",
185 "settings.account.successInfo": "Twoje zmiany zostały zapisane", 185 "settings.account.successInfo": "Twoje zmiany zostały zapisane",
186 "settings.account.tryReloadServices": "Spróbuj ponownie", 186 "settings.account.tryReloadServices": "Spróbuj ponownie",
187 "settings.account.tryReloadUserInfoRequest": "Spróbuj ponownie", 187 "settings.account.tryReloadUserInfoRequest": "Spróbuj ponownie",
188 "settings.account.userInfoRequestFailed": "Nie można wczytać informacji o użytkowniku", 188 "settings.account.userInfoRequestFailed": "Nie można wczytać informacji o użytkowniku",
189 "settings.account.yourLicense": " Twoja licencja Ferdi:", 189 "settings.account.yourLicense": "Your Ferdi License:",
190 "settings.app.accentColorInfo": "Zdefiniuj kolor akcentu w formacie zgodnym z CSS. (Domyślnie: {defaultAccentColor})", 190 "settings.app.accentColorInfo": "Zdefiniuj kolor akcentu w formacie zgodnym z CSS. (Domyślnie: {defaultAccentColor})",
191 "settings.app.buttonClearAllCache": "Wyczyść pamięć podręczną (cache)", 191 "settings.app.buttonClearAllCache": "Wyczyść pamięć podręczną (cache)",
192 "settings.app.buttonInstallUpdate": "Uruchom ponownie i zainstaluj aktualizacjÄ™", 192 "settings.app.buttonInstallUpdate": "Uruchom ponownie i zainstaluj aktualizacjÄ™",
193 "settings.app.buttonOpenFerdiProfileFolder": "Pokaż folder z profilami", 193 "settings.app.buttonOpenFerdiProfileFolder": "Pokaż folder z profilami",
194 "settings.app.buttonOpenFerdiServiceRecipesFolder": "Otwórz folder usług", 194 "settings.app.buttonOpenFerdiServiceRecipesFolder": "Open Service Recipes folder",
195 "settings.app.buttonSearchForUpdate": "Sprawdź aktualizacje", 195 "settings.app.buttonSearchForUpdate": "Sprawdź aktualizacje",
196 "settings.app.cacheInfo": "Pamięć podręczna zajmuje obecnie {size} przestrzeni dyskowej", 196 "settings.app.cacheInfo": "Pamięć podręczna zajmuje obecnie {size} przestrzeni dyskowej",
197 "settings.app.cacheNotCleared": "Błąd czyszczenia pamięci podręcznej", 197 "settings.app.cacheNotCleared": "Błąd czyszczenia pamięci podręcznej",
@@ -207,24 +207,26 @@
207 "settings.app.form.clipboardNotifications": "Nie wyświetlaj powiadomień dla schowka", 207 "settings.app.form.clipboardNotifications": "Nie wyświetlaj powiadomień dla schowka",
208 "settings.app.form.closeToSystemTray": "Zminimalizuj Ferdi do paska zadań", 208 "settings.app.form.closeToSystemTray": "Zminimalizuj Ferdi do paska zadań",
209 "settings.app.form.confirmOnQuit": "Potwierdź podczas opuszczania Ferdi", 209 "settings.app.form.confirmOnQuit": "Potwierdź podczas opuszczania Ferdi",
210 "settings.app.form.customTodoServer": "Niestandardowy serwer Todo", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "WÅ‚Ä…cz Ciemny motyw", 211 "settings.app.form.darkMode": "WÅ‚Ä…cz Ciemny motyw",
212 "settings.app.form.enableGPUAcceleration": "WÅ‚Ä…cz akceleracjÄ™ GPU", 212 "settings.app.form.enableGPUAcceleration": "WÅ‚Ä…cz akceleracjÄ™ GPU",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Włącz blokadę hasłem", 214 "settings.app.form.enableLock": "Włącz blokadę hasłem",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Zawsze wyświetlaj Ferdi na pasku zadań", 216 "settings.app.form.enableMenuBar": "Zawsze wyświetlaj Ferdi na pasku zadań",
215 "settings.app.form.enableSpellchecking": "WÅ‚Ä…cz sprawdzanie pisowni", 217 "settings.app.form.enableSpellchecking": "WÅ‚Ä…cz sprawdzanie pisowni",
216 "settings.app.form.enableSystemTray": "Zawsze pokazuj Ferdi na pasku zadań", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
217 "settings.app.form.enableTodos": "WÅ‚Ä…cz Zadania Ferdi", 219 "settings.app.form.enableTodos": "WÅ‚Ä…cz Zadania Ferdi",
218 "settings.app.form.hibernateOnStartup": "Hibernacja usług po starcie systemu", 220 "settings.app.form.hibernateOnStartup": "Hibernacja usług po starcie systemu",
219 "settings.app.form.hibernationStrategy": "Strategia hibernacji", 221 "settings.app.form.hibernationStrategy": "Strategia hibernacji",
220 "settings.app.form.iconSize": "Rozmiar ikony usługi", 222 "settings.app.form.iconSize": "Rozmiar ikony usługi",
221 "settings.app.form.inactivityLock": "Zablokuj przy braku aktywności", 223 "settings.app.form.inactivityLock": "Zablokuj przy braku aktywności",
222 "settings.app.form.keepAllWorkspacesLoaded": "Trzymaj wszystkie pola robocze załadowane", 224 "settings.app.form.keepAllWorkspacesLoaded": "Keep all workspaces loaded",
223 "settings.app.form.language": "Język", 225 "settings.app.form.language": "Język",
224 "settings.app.form.lockPassword": "Hasło", 226 "settings.app.form.lockPassword": "Hasło",
225 "settings.app.form.minimizeToSystemTray": "Zminimalizuj aplikacjÄ™ Ferdi", 227 "settings.app.form.minimizeToSystemTray": "Zminimalizuj aplikacjÄ™ Ferdi",
226 "settings.app.form.navigationBarBehaviour": "Zachowanie paska nawigacji", 228 "settings.app.form.navigationBarBehaviour": "Zachowanie paska nawigacji",
227 "settings.app.form.notifyTaskBarOnMessage": "Powiadom na pasku zadań o nowej wiadomości", 229 "settings.app.form.notifyTaskBarOnMessage": "Notify TaskBar/Dock on new message",
228 "settings.app.form.passwordToggle": "Przełącznik hasła", 230 "settings.app.form.passwordToggle": "Przełącznik hasła",
229 "settings.app.form.predefinedTodoServer": "Serwer Todo", 231 "settings.app.form.predefinedTodoServer": "Serwer Todo",
230 "settings.app.form.privateNotifications": "Nie pokazuj treści wiadomości w powiadomieniach", 232 "settings.app.form.privateNotifications": "Nie pokazuj treści wiadomości w powiadomieniach",
@@ -237,12 +239,12 @@
237 "settings.app.form.sentry": "Wysyłaj dane telemetrii", 239 "settings.app.form.sentry": "Wysyłaj dane telemetrii",
238 "settings.app.form.serviceRibbonWidth": "Szerokość paska bocznego", 240 "settings.app.form.serviceRibbonWidth": "Szerokość paska bocznego",
239 "settings.app.form.showDisabledServices": "Wyświetlaj karty wyłączonych usług", 241 "settings.app.form.showDisabledServices": "Wyświetlaj karty wyłączonych usług",
240 "settings.app.form.showDragArea": "Pokaż obszar do przeciągania w oknie", 242 "settings.app.form.showDragArea": "Show draggable area on window",
241 "settings.app.form.showMessagesBadgesWhenMuted": "Pokaż licznik nieprzeczytanych wiadomości gdy powiadomienia są wyłączone", 243 "settings.app.form.showMessagesBadgesWhenMuted": "Pokaż licznik nieprzeczytanych wiadomości gdy powiadomienia są wyłączone",
242 "settings.app.form.splitMode": "Aktywuj tryb podzielonego widoku", 244 "settings.app.form.splitMode": "Enable Split View Mode",
243 "settings.app.form.startMinimized": "Uruchom zminimalizowany", 245 "settings.app.form.startMinimized": "Uruchom zminimalizowany",
244 "settings.app.form.universalDarkMode": "WÅ‚Ä…cz uniwersalny tryb ciemny", 246 "settings.app.form.universalDarkMode": "WÅ‚Ä…cz uniwersalny tryb ciemny",
245 "settings.app.form.useTouchIdToUnlock": "Zawsze używaj TouchID do odblokowania Ferdi", 247 "settings.app.form.useTouchIdToUnlock": "Allow using TouchID to unlock Ferdi",
246 "settings.app.form.useVerticalStyle": "Użyj stylu poziomego", 248 "settings.app.form.useVerticalStyle": "Użyj stylu poziomego",
247 "settings.app.form.wakeUpStrategy": "Strategia wybudzania", 249 "settings.app.form.wakeUpStrategy": "Strategia wybudzania",
248 "settings.app.headlineAdvanced": "Zaawansowane", 250 "settings.app.headlineAdvanced": "Zaawansowane",
@@ -251,8 +253,8 @@
251 "settings.app.headlineLanguage": "Język", 253 "settings.app.headlineLanguage": "Język",
252 "settings.app.headlinePrivacy": "Prywatność", 254 "settings.app.headlinePrivacy": "Prywatność",
253 "settings.app.headlineUpdates": "Aktualizacje", 255 "settings.app.headlineUpdates": "Aktualizacje",
254 "settings.app.hibernateInfo": "Domyślnie, Ferdi trzyma wszystkie Twoje usługi włączone w tle, aby były gotowe w momencie kiedy będziesz ich potrzebował. Hibernacja usług odłączy Twoje usługi po określonym czasie. Dzięki temu zachowasz pamięć RAM lub zachowasz usługi od spowolnienia komputera.", 256 "settings.app.hibernateInfo": "By default, Ferdi will keep all your services open and loaded in the background so they are ready when you want to use them. Service Hibernation will unload your services after a specified amount. This is useful to save RAM or keeping services from slowing down your computer.",
255 "settings.app.inactivityLockInfo": "Minut do dezaktywacji, po których Ferdi automatycznie zablokuje. Wybierz 0 aby dezaktywować", 257 "settings.app.inactivityLockInfo": "Minutes of inactivity, after which Ferdi should automatically lock. Use 0 to disable",
256 "settings.app.languageDisclaimer": "Oficjalnymi językami są Angielski i Niemiecki. Inne języki są tłumaczone przez społeczność Ferdi.", 258 "settings.app.languageDisclaimer": "Oficjalnymi językami są Angielski i Niemiecki. Inne języki są tłumaczone przez społeczność Ferdi.",
257 "settings.app.lockInfo": "Password Lock allows you to keep your messages protected.\nUsing Password Lock, you will be prompted to enter your password everytime you start Ferdi or lock Ferdi yourself using the lock symbol in the bottom left corner or the shortcut {lockShortcut}.", 259 "settings.app.lockInfo": "Password Lock allows you to keep your messages protected.\nUsing Password Lock, you will be prompted to enter your password everytime you start Ferdi or lock Ferdi yourself using the lock symbol in the bottom left corner or the shortcut {lockShortcut}.",
258 "settings.app.lockedPassword": "Hasło", 260 "settings.app.lockedPassword": "Hasło",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "WÅ‚Ä…cz hibernacjÄ™", 311 "settings.service.form.enableHibernation": "WÅ‚Ä…cz hibernacjÄ™",
310 "settings.service.form.enableNotification": "Aktywuj powiadomienia", 312 "settings.service.form.enableNotification": "Aktywuj powiadomienia",
311 "settings.service.form.enableService": "Aktywuj usługę", 313 "settings.service.form.enableService": "Aktywuj usługę",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Znaczniki nieprzeczytanych wiadomości", 315 "settings.service.form.headlineBadges": "Znaczniki nieprzeczytanych wiadomości",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "Ogólne", 317 "settings.service.form.headlineGeneral": "Ogólne",
diff --git a/src/i18n/locales/pt-BR.json b/src/i18n/locales/pt-BR.json
index 1d068e87e..08a631c9a 100644
--- a/src/i18n/locales/pt-BR.json
+++ b/src/i18n/locales/pt-BR.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Servidor de Tarefas Personalizado", 210 "settings.app.form.customTodoServer": "Servidor de Tarefas Personalizado",
211 "settings.app.form.darkMode": "Ativar o Tema Escuro", 211 "settings.app.form.darkMode": "Ativar o Tema Escuro",
212 "settings.app.form.enableGPUAcceleration": "Ativar Aceleração de GPU", 212 "settings.app.form.enableGPUAcceleration": "Ativar Aceleração de GPU",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Habilitar bloqueio por senha", 214 "settings.app.form.enableLock": "Habilitar bloqueio por senha",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Sempre exibir Ferdi na Barra de Menu", 216 "settings.app.form.enableMenuBar": "Sempre exibir Ferdi na Barra de Menu",
215 "settings.app.form.enableSpellchecking": "Ativar verificação ortográfica", 217 "settings.app.form.enableSpellchecking": "Ativar verificação ortográfica",
216 "settings.app.form.enableSystemTray": "Sempre mostrar o ícone na Bandeja de Sistema", 218 "settings.app.form.enableSystemTray": "Sempre mostrar o ícone na Bandeja de Sistema",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Ativer hibernação", 311 "settings.service.form.enableHibernation": "Ativer hibernação",
310 "settings.service.form.enableNotification": "Ativar notificações", 312 "settings.service.form.enableNotification": "Ativar notificações",
311 "settings.service.form.enableService": "Ativar serviço", 313 "settings.service.form.enableService": "Ativar serviço",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Emblema de mensagem não lida", 315 "settings.service.form.headlineBadges": "Emblema de mensagem não lida",
313 "settings.service.form.headlineDarkReaderSettings": "Configurações do Dark Reader", 316 "settings.service.form.headlineDarkReaderSettings": "Configurações do Dark Reader",
314 "settings.service.form.headlineGeneral": "Geral", 317 "settings.service.form.headlineGeneral": "Geral",
diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json
index 9675d5d77..b0411578b 100644
--- a/src/i18n/locales/pt.json
+++ b/src/i18n/locales/pt.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Ativar modo noturno", 211 "settings.app.form.darkMode": "Ativar modo noturno",
212 "settings.app.form.enableGPUAcceleration": "Ativar Aceleração de GPU", 212 "settings.app.form.enableGPUAcceleration": "Ativar Aceleração de GPU",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Ativar palavra-passe", 214 "settings.app.form.enableLock": "Ativar palavra-passe",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Ativar correção ortográfica", 217 "settings.app.form.enableSpellchecking": "Ativar correção ortográfica",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Ativar notificações", 312 "settings.service.form.enableNotification": "Ativar notificações",
311 "settings.service.form.enableService": "Ativar serviço", 313 "settings.service.form.enableService": "Ativar serviço",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Notificações de Mensagens não lidas", 315 "settings.service.form.headlineBadges": "Notificações de Mensagens não lidas",
313 "settings.service.form.headlineDarkReaderSettings": "Configurações do Leitor Noturno", 316 "settings.service.form.headlineDarkReaderSettings": "Configurações do Leitor Noturno",
314 "settings.service.form.headlineGeneral": "Geral", 317 "settings.service.form.headlineGeneral": "Geral",
diff --git a/src/i18n/locales/ro.json b/src/i18n/locales/ro.json
index fad6dae82..e6c8f6262 100644
--- a/src/i18n/locales/ro.json
+++ b/src/i18n/locales/ro.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Enable Dark Mode", 211 "settings.app.form.darkMode": "Enable Dark Mode",
212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration", 212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Enable spell checking", 217 "settings.app.form.enableSpellchecking": "Enable spell checking",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Enable notifications", 312 "settings.service.form.enableNotification": "Enable notifications",
311 "settings.service.form.enableService": "Enable service", 313 "settings.service.form.enableService": "Enable service",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Unread message badges", 315 "settings.service.form.headlineBadges": "Unread message badges",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "General", 317 "settings.service.form.headlineGeneral": "General",
diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json
index f7d25e08b..3d9bd13b1 100644
--- a/src/i18n/locales/ru.json
+++ b/src/i18n/locales/ru.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Включить Тёмный режим", 211 "settings.app.form.darkMode": "Включить Тёмный режим",
212 "settings.app.form.enableGPUAcceleration": "Включить уÑкорение GPU", 212 "settings.app.form.enableGPUAcceleration": "Включить уÑкорение GPU",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Включить проверку правопиÑаниÑ", 217 "settings.app.form.enableSpellchecking": "Включить проверку правопиÑаниÑ",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Включить уведомлениÑ", 312 "settings.service.form.enableNotification": "Включить уведомлениÑ",
311 "settings.service.form.enableService": "Включить ÑервиÑ", 313 "settings.service.form.enableService": "Включить ÑервиÑ",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Значки непрочитанных Ñообщений", 315 "settings.service.form.headlineBadges": "Значки непрочитанных Ñообщений",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "Общие", 317 "settings.service.form.headlineGeneral": "Общие",
diff --git a/src/i18n/locales/sk.json b/src/i18n/locales/sk.json
index 02d325afd..08fb905cf 100644
--- a/src/i18n/locales/sk.json
+++ b/src/i18n/locales/sk.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Zapnúť Dark Mode", 211 "settings.app.form.darkMode": "Zapnúť Dark Mode",
212 "settings.app.form.enableGPUAcceleration": "Zapnúť GPU zrýchlenie", 212 "settings.app.form.enableGPUAcceleration": "Zapnúť GPU zrýchlenie",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Zapnúť kontrolu pravopisu", 217 "settings.app.form.enableSpellchecking": "Zapnúť kontrolu pravopisu",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Povoliť oznámenia", 312 "settings.service.form.enableNotification": "Povoliť oznámenia",
311 "settings.service.form.enableService": "Povoliť službu", 313 "settings.service.form.enableService": "Povoliť službu",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Symboly nepreÄítaných správ", 315 "settings.service.form.headlineBadges": "Symboly nepreÄítaných správ",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "Všeobecné", 317 "settings.service.form.headlineGeneral": "Všeobecné",
diff --git a/src/i18n/locales/sl.json b/src/i18n/locales/sl.json
index 041b02632..ff10978c4 100644
--- a/src/i18n/locales/sl.json
+++ b/src/i18n/locales/sl.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Enable Dark Mode", 211 "settings.app.form.darkMode": "Enable Dark Mode",
212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration", 212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Enable spell checking", 217 "settings.app.form.enableSpellchecking": "Enable spell checking",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Enable notifications", 312 "settings.service.form.enableNotification": "Enable notifications",
311 "settings.service.form.enableService": "Enable service", 313 "settings.service.form.enableService": "Enable service",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Unread message badges", 315 "settings.service.form.headlineBadges": "Unread message badges",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "General", 317 "settings.service.form.headlineGeneral": "General",
diff --git a/src/i18n/locales/sr.json b/src/i18n/locales/sr.json
index 85068ee5c..a058a81ed 100644
--- a/src/i18n/locales/sr.json
+++ b/src/i18n/locales/sr.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Enable Dark Mode", 211 "settings.app.form.darkMode": "Enable Dark Mode",
212 "settings.app.form.enableGPUAcceleration": "Омогући убрзање графичке јединице", 212 "settings.app.form.enableGPUAcceleration": "Омогући убрзање графичке јединице",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Omogući provjeru pravopisa", 217 "settings.app.form.enableSpellchecking": "Omogući provjeru pravopisa",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Omogućite obavijesti", 312 "settings.service.form.enableNotification": "Omogućite obavijesti",
311 "settings.service.form.enableService": "Omogućite usluge", 313 "settings.service.form.enableService": "Omogućite usluge",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Беџеви за непрочитане поруке", 315 "settings.service.form.headlineBadges": "Беџеви за непрочитане поруке",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "Općenito", 317 "settings.service.form.headlineGeneral": "Općenito",
diff --git a/src/i18n/locales/sv.json b/src/i18n/locales/sv.json
index ef254fc5d..c3491f720 100644
--- a/src/i18n/locales/sv.json
+++ b/src/i18n/locales/sv.json
@@ -1,6 +1,6 @@
1{ 1{
2 "app.errorHandler.action": "Ladda om", 2 "app.errorHandler.action": "Ladda om",
3 "app.errorHandler.headline": "Something went wrong.", 3 "app.errorHandler.headline": "NÃ¥gonting gick fel.",
4 "changeserver.customServerLabel": "Anpassad server", 4 "changeserver.customServerLabel": "Anpassad server",
5 "changeserver.headline": "Byt server", 5 "changeserver.headline": "Byt server",
6 "changeserver.label": "Server", 6 "changeserver.label": "Server",
@@ -9,7 +9,7 @@
9 "connectionLostBanner.cta": "Ladda om tjänst", 9 "connectionLostBanner.cta": "Ladda om tjänst",
10 "connectionLostBanner.informationLink": "Vad hände?", 10 "connectionLostBanner.informationLink": "Vad hände?",
11 "connectionLostBanner.message": "Åh nej! Ferdi förlorade anslutningen till {name}.", 11 "connectionLostBanner.message": "Åh nej! Ferdi förlorade anslutningen till {name}.",
12 "feature.basicAuth.signIn": "Sign In", 12 "feature.basicAuth.signIn": "Logga in",
13 "feature.nightlyBuilds.activate": "Aktivera", 13 "feature.nightlyBuilds.activate": "Aktivera",
14 "feature.nightlyBuilds.info": "Nattliga kompileringar är väldigt experimentella versioner av Ferdi som kan innehålla ofärdiga och ännu ej testade funktioner. Nattliga kompilering är i första hand tänkt för utvecklare för att kunna testa hur nya funktioner fungerar och presterar. Är du osäker på vad detta innebär rekommenderas du att inte aktivera nattliga kompileringar.", 14 "feature.nightlyBuilds.info": "Nattliga kompileringar är väldigt experimentella versioner av Ferdi som kan innehålla ofärdiga och ännu ej testade funktioner. Nattliga kompilering är i första hand tänkt för utvecklare för att kunna testa hur nya funktioner fungerar och presterar. Är du osäker på vad detta innebär rekommenderas du att inte aktivera nattliga kompileringar.",
15 "feature.nightlyBuilds.title": "Nattlig kompilering", 15 "feature.nightlyBuilds.title": "Nattlig kompilering",
@@ -23,37 +23,37 @@
23 "feature.quickSwitch.info": "Välj en tjänst med TAB, ↑ and ↓. Öppna en tjänst med ENTER.", 23 "feature.quickSwitch.info": "Välj en tjänst med TAB, ↑ and ↓. Öppna en tjänst med ENTER.",
24 "feature.quickSwitch.search": "Sök...", 24 "feature.quickSwitch.search": "Sök...",
25 "feature.quickSwitch.title": "Snabbväxling", 25 "feature.quickSwitch.title": "Snabbväxling",
26 "global.api.unhealthy": "Can't connect to Ferdi online services", 26 "global.api.unhealthy": "Kan inte ansluta till Ferdis onlinetjänster",
27 "global.cancel": "Cancel", 27 "global.cancel": "Avbryt",
28 "global.edit": "Redigera", 28 "global.edit": "Redigera",
29 "global.no": "No", 29 "global.no": "Nej",
30 "global.notConnectedToTheInternet": "Du är inte ansluten till Internet.", 30 "global.notConnectedToTheInternet": "Du är inte ansluten till Internet.",
31 "global.ok": "Ok", 31 "global.ok": "Ok",
32 "global.quit": "Quit", 32 "global.quit": "Avsluta",
33 "global.quitConfirmation": "Do you really want to quit Ferdi?", 33 "global.quitConfirmation": "Vill du verkligen avsluta?",
34 "global.save": "Save", 34 "global.save": "Spara",
35 "global.settings": "Settings", 35 "global.settings": "Inställningar",
36 "global.spellchecker.useDefault": "Använd systemstandard ({default})", 36 "global.spellchecker.useDefault": "Använd systemstandard ({default})",
37 "global.spellchecking.autodetect": "Identifiera språk automatiskt", 37 "global.spellchecking.autodetect": "Identifiera språk automatiskt",
38 "global.spellchecking.autodetect.short": "Automatisk", 38 "global.spellchecking.autodetect.short": "Automatisk",
39 "global.spellchecking.language": "Rättstavningsspråk", 39 "global.spellchecking.language": "Rättstavningsspråk",
40 "global.submit": "Submit", 40 "global.submit": "Skicka",
41 "global.userAgentHelp": "Use 'https://whatmyuseragent.com/' (to discover) or 'https://developers.whatismybrowser.com/useragents/explore/' (to choose) your desired user agent and copy-paste it here.", 41 "global.userAgentHelp": "Använd 'https://whatmyuseragent.com/' (för att upptäcka) eller 'https://developers.whatismybrowser.com/useragents/explore/' (för att välja) din önskade användaragent och kopiera-klistra in den här.",
42 "global.userAgentPref": "User Agent", 42 "global.userAgentPref": "Användaragent",
43 "global.yes": "Yes", 43 "global.yes": "Ja",
44 "import.headline": "Importera dina Ferdi 4-tjänster", 44 "import.headline": "Importera dina Ferdi 4-tjänster",
45 "import.notSupportedHeadline": "Tjänster som ännu inte stöds i Ferdi 5", 45 "import.notSupportedHeadline": "Tjänster som ännu inte stöds i Ferdi 5",
46 "import.skip.label": "Jag vill lägga till tjänster manuellt", 46 "import.skip.label": "Jag vill lägga till tjänster manuellt",
47 "import.submit.label": "Import {count} services", 47 "import.submit.label": "Importera {count} tjänster",
48 "infobar.authRequestFailed": "Det uppstod fel vid försök då en autentiserad begäran utfördes. Prova att logga ut och in igen om felet kvarstår.", 48 "infobar.authRequestFailed": "Det uppstod fel vid försök då en autentiserad begäran utfördes. Prova att logga ut och in igen om felet kvarstår.",
49 "infobar.buttonChangelog": "What is new?", 49 "infobar.buttonChangelog": "Vad är nytt?",
50 "infobar.buttonInstallUpdate": "Starta om & installera uppdatering", 50 "infobar.buttonInstallUpdate": "Starta om & installera uppdatering",
51 "infobar.buttonReloadServices": "Ladda om tjänster", 51 "infobar.buttonReloadServices": "Ladda om tjänster",
52 "infobar.hide": "Dölj", 52 "infobar.hide": "Dölj",
53 "infobar.requiredRequestsFailed": "Kunde inte ladda tjänster och användarinformation", 53 "infobar.requiredRequestsFailed": "Kunde inte ladda tjänster och användarinformation",
54 "infobar.servicesUpdated": "Dina tjänster har uppdaterats.", 54 "infobar.servicesUpdated": "Dina tjänster har uppdaterats.",
55 "infobar.updateAvailable": "En ny uppdatering för Ferdi finns tillgänglig.", 55 "infobar.updateAvailable": "En ny uppdatering för Ferdi finns tillgänglig.",
56 "infobox.dismiss": "Dismiss", 56 "infobox.dismiss": "Avfärda",
57 "invite.email.label": "E-postadress", 57 "invite.email.label": "E-postadress",
58 "invite.headline.friends": "Bjud in 3 av dina vänner eller kollegor", 58 "invite.headline.friends": "Bjud in 3 av dina vänner eller kollegor",
59 "invite.name.label": "Namn", 59 "invite.name.label": "Namn",
@@ -74,7 +74,7 @@
74 "login.email.label": "E-postadress", 74 "login.email.label": "E-postadress",
75 "login.headline": "Logga in", 75 "login.headline": "Logga in",
76 "login.invalidCredentials": "E-post eller lösenord är felaktigt", 76 "login.invalidCredentials": "E-post eller lösenord är felaktigt",
77 "login.link.password": "Reset password", 77 "login.link.password": "Återställ lösenord",
78 "login.link.signup": "Skapa ett gratis konto", 78 "login.link.signup": "Skapa ett gratis konto",
79 "login.password.label": "Lösenord", 79 "login.password.label": "Lösenord",
80 "login.serverLogout": "Din session har gått ut. Vänligen logga in på nytt.", 80 "login.serverLogout": "Din session har gått ut. Vänligen logga in på nytt.",
@@ -86,40 +86,40 @@
86 "menu.app.autohideMenuBar": "Dölj menyraden automatiskt", 86 "menu.app.autohideMenuBar": "Dölj menyraden automatiskt",
87 "menu.app.checkForUpdates": "Sök efter uppdateringar", 87 "menu.app.checkForUpdates": "Sök efter uppdateringar",
88 "menu.app.hide": "Dölj", 88 "menu.app.hide": "Dölj",
89 "menu.app.hideOthers": "Hide Others", 89 "menu.app.hideOthers": "Dölj övriga",
90 "menu.app.unhide": "Unhide", 90 "menu.app.unhide": "Visa",
91 "menu.edit": "Redigera", 91 "menu.edit": "Redigera",
92 "menu.edit.copy": "Copy", 92 "menu.edit.copy": "Kopiera",
93 "menu.edit.cut": "Cut", 93 "menu.edit.cut": "Klipp ut",
94 "menu.edit.delete": "Radera", 94 "menu.edit.delete": "Radera",
95 "menu.edit.emojiSymbols": "Emojis & symboler", 95 "menu.edit.emojiSymbols": "Emojis & symboler",
96 "menu.edit.findInPage": "Hitta på sidan", 96 "menu.edit.findInPage": "Hitta på sidan",
97 "menu.edit.paste": "Paste", 97 "menu.edit.paste": "Klistra in",
98 "menu.edit.pasteAndMatchStyle": "Paste And Match Style", 98 "menu.edit.pasteAndMatchStyle": "Klistra in och matcha stil",
99 "menu.edit.redo": "Redo", 99 "menu.edit.redo": "Gör om",
100 "menu.edit.selectAll": "Select All", 100 "menu.edit.selectAll": "Markera allt",
101 "menu.edit.speech": "Tal", 101 "menu.edit.speech": "Tal",
102 "menu.edit.startDictation": "Börja diktera", 102 "menu.edit.startDictation": "Börja diktera",
103 "menu.edit.startSpeaking": "Börja tala", 103 "menu.edit.startSpeaking": "Börja tala",
104 "menu.edit.stopSpeaking": "Sluta prata", 104 "menu.edit.stopSpeaking": "Sluta prata",
105 "menu.edit.undo": "Undo", 105 "menu.edit.undo": "Ã…ngra",
106 "menu.file": "Fil", 106 "menu.file": "Fil",
107 "menu.help": "Help", 107 "menu.help": "Hjälp",
108 "menu.help.changelog": "Ändringslogg", 108 "menu.help.changelog": "Ändringslogg",
109 "menu.help.debugInfo": "Kopiera felsökningsinformation", 109 "menu.help.debugInfo": "Kopiera felsökningsinformation",
110 "menu.help.debugInfoCopiedBody": "Din felsökningsinformation har kopierats till ditt urklipp.", 110 "menu.help.debugInfoCopiedBody": "Din felsökningsinformation har kopierats till ditt urklipp.",
111 "menu.help.debugInfoCopiedHeadline": "Ferdi felsökningsinformation", 111 "menu.help.debugInfoCopiedHeadline": "Ferdi felsökningsinformation",
112 "menu.help.importExportData": "Import/Export Configuration Data", 112 "menu.help.importExportData": "Importera/Exportera konfigurationsdata",
113 "menu.help.learnMore": "Läs mer", 113 "menu.help.learnMore": "Läs mer",
114 "menu.help.privacy": "Integritetspolicy", 114 "menu.help.privacy": "Integritetspolicy",
115 "menu.help.publishDebugInfo": "Skicka felsökningsinformation", 115 "menu.help.publishDebugInfo": "Skicka felsökningsinformation",
116 "menu.help.support": "Support", 116 "menu.help.support": "Support",
117 "menu.help.tos": "Användarvillkor", 117 "menu.help.tos": "Användarvillkor",
118 "menu.services": "Tjänster", 118 "menu.services": "Tjänster",
119 "menu.services.activatePreviousService": "Activate previous service", 119 "menu.services.activatePreviousService": "Aktivera föregående tjänst",
120 "menu.services.addNewService": "Add New Service...", 120 "menu.services.addNewService": "Lägg till ny tjänst...",
121 "menu.services.goHome": "Hem", 121 "menu.services.goHome": "Hem",
122 "menu.services.setNextServiceActive": "Activate next service", 122 "menu.services.setNextServiceActive": "Aktivera nästa tjänst",
123 "menu.todos": "Todos", 123 "menu.todos": "Todos",
124 "menu.todos.enableTodos": "Aktivera Todos", 124 "menu.todos.enableTodos": "Aktivera Todos",
125 "menu.view": "Visa", 125 "menu.view": "Visa",
@@ -127,31 +127,31 @@
127 "menu.view.forward": "Framåt", 127 "menu.view.forward": "Framåt",
128 "menu.view.lockFerdi": "LÃ¥s Ferdi", 128 "menu.view.lockFerdi": "LÃ¥s Ferdi",
129 "menu.view.openQuickSwitch": "Öppna snabbväxling", 129 "menu.view.openQuickSwitch": "Öppna snabbväxling",
130 "menu.view.reloadFerdi": "Reload Ferdi", 130 "menu.view.reloadFerdi": "Ladda om Ferdi",
131 "menu.view.reloadService": "Ladda om tjänst", 131 "menu.view.reloadService": "Ladda om tjänst",
132 "menu.view.reloadTodos": "Ladda om: AttGöra", 132 "menu.view.reloadTodos": "Ladda om: AttGöra",
133 "menu.view.resetZoom": "Actual Size", 133 "menu.view.resetZoom": "Originalstorlek",
134 "menu.view.toggleDarkMode": "Växla mörkt läge", 134 "menu.view.toggleDarkMode": "Växla mörkt läge",
135 "menu.view.toggleDevTools": "Växla utvecklarverktyg", 135 "menu.view.toggleDevTools": "Växla utvecklarverktyg",
136 "menu.view.toggleFullScreen": "Toggle Full Screen", 136 "menu.view.toggleFullScreen": "Växla till/från helskärm",
137 "menu.view.toggleServiceDevTools": "Växla tjänsteverktyg för utvecklare", 137 "menu.view.toggleServiceDevTools": "Växla tjänsteverktyg för utvecklare",
138 "menu.view.toggleTodosDevTools": "Växla Todos utvecklarverktyg", 138 "menu.view.toggleTodosDevTools": "Växla Todos utvecklarverktyg",
139 "menu.view.zoomIn": "Zoom In", 139 "menu.view.zoomIn": "Zooma in",
140 "menu.view.zoomOut": "Zoom Out", 140 "menu.view.zoomOut": "Zooma ut",
141 "menu.window": "Window", 141 "menu.window": "Fönster",
142 "menu.window.close": "Close", 142 "menu.window.close": "Stäng",
143 "menu.window.minimize": "Minimize", 143 "menu.window.minimize": "Minimera",
144 "menu.workspaces": "Arbetsytor", 144 "menu.workspaces": "Arbetsytor",
145 "menu.workspaces.addNewWorkspace": "Skapa ny arbetsyta...", 145 "menu.workspaces.addNewWorkspace": "Skapa ny arbetsyta...",
146 "menu.workspaces.closeWorkspaceDrawer": "Stäng arbetsytan", 146 "menu.workspaces.closeWorkspaceDrawer": "Stäng arbetsytan",
147 "menu.workspaces.defaultWorkspace": "Alla tjänster", 147 "menu.workspaces.defaultWorkspace": "Alla tjänster",
148 "menu.workspaces.openWorkspaceDrawer": "Öppna arbetsytan", 148 "menu.workspaces.openWorkspaceDrawer": "Öppna arbetsytan",
149 "password.email.label": "E-postadress", 149 "password.email.label": "E-postadress",
150 "password.headline": "Reset password", 150 "password.headline": "Återställ lösenord",
151 "password.link.login": "Logga in på ditt konto", 151 "password.link.login": "Logga in på ditt konto",
152 "password.link.signup": "Skapa ett gratis konto", 152 "password.link.signup": "Skapa ett gratis konto",
153 "password.noUser": "No user with that email address was found", 153 "password.noUser": "Ingen användare med den e-postadressen hittades",
154 "password.successInfo": "Your new password was sent to your email address", 154 "password.successInfo": "Ett nytt lösenord har skickats till din e-postadress",
155 "service.crashHandler.action": "Ladda om {name}", 155 "service.crashHandler.action": "Ladda om {name}",
156 "service.crashHandler.autoReload": "Försöker automatiskt återställa {name} om {seconds} sekunder", 156 "service.crashHandler.autoReload": "Försöker automatiskt återställa {name} om {seconds} sekunder",
157 "service.crashHandler.headline": "Ã…h nej!", 157 "service.crashHandler.headline": "Ã…h nej!",
@@ -166,10 +166,10 @@
166 "service.webviewLoader.loading": "Laddar {service}", 166 "service.webviewLoader.loading": "Laddar {service}",
167 "services.getStarted": "Kom igång", 167 "services.getStarted": "Kom igång",
168 "services.login": "Logga in för att använda Ferdi.", 168 "services.login": "Logga in för att använda Ferdi.",
169 "services.serverInfo": "Optionally, you can change your Ferdi server by clicking the cog in the bottom left corner. If you are switching over (from one of the hosted servers) to using Ferdi without an account, please be informed that you can export your data from that server and subsequently import it using the Help menu to resurrect all your workspaces and configured services!", 169 "services.serverInfo": "Alternativt kan du ändra din Ferdi server genom att klicka på kuggen i det nedre vänstra hörnet. Om du byter över (från en av de hostade servrarna) till att använda Ferdi utan ett konto, informeras om att du kan exportera dina data från den servern och därefter importera den med hjälp av Hjälpmenyn för att återuppliva alla dina arbetsytor och konfigurerade tjänster!",
170 "services.serverless": "Använd Ferdi utan ett konto", 170 "services.serverless": "Använd Ferdi utan ett konto",
171 "services.welcome": "Välkommen till Ferdi", 171 "services.welcome": "Välkommen till Ferdi",
172 "settings.account.account.editButton": "Edit account", 172 "settings.account.account.editButton": "Redigera konto",
173 "settings.account.accountUnavailable": "Kontot är inte tillgängligt", 173 "settings.account.accountUnavailable": "Kontot är inte tillgängligt",
174 "settings.account.accountUnavailableInfo": "Du använder Ferdi utan ett konto. Om du vill använda Ferdi med ett konto och hålla dina tjänster synkroniserade mellan installationer, välj en server i fliken Inställningar och logga in.", 174 "settings.account.accountUnavailableInfo": "Du använder Ferdi utan ett konto. Om du vill använda Ferdi med ett konto och hålla dina tjänster synkroniserade mellan installationer, välj en server i fliken Inställningar och logga in.",
175 "settings.account.buttonSave": "Uppdatera profil", 175 "settings.account.buttonSave": "Uppdatera profil",
@@ -177,25 +177,25 @@
177 "settings.account.deleteEmailSent": "Du har fått ett e-postmeddelande med en länk för att bekräfta raderingen av ditt konto. Ditt konto och data kan inte återställas!", 177 "settings.account.deleteEmailSent": "Du har fått ett e-postmeddelande med en länk för att bekräfta raderingen av ditt konto. Ditt konto och data kan inte återställas!",
178 "settings.account.deleteInfo": "Om du inte behöver ditt Ferdi-konto längre, kan du ta bort ditt konto och all anknuten information här.", 178 "settings.account.deleteInfo": "Om du inte behöver ditt Ferdi-konto längre, kan du ta bort ditt konto och all anknuten information här.",
179 "settings.account.headline": "Konto", 179 "settings.account.headline": "Konto",
180 "settings.account.headlineAccount": "Account information", 180 "settings.account.headlineAccount": "Kontoinformation",
181 "settings.account.headlineDangerZone": "Danger Zone", 181 "settings.account.headlineDangerZone": "Högrisksområde",
182 "settings.account.headlineInvoices": "Invoices", 182 "settings.account.headlineInvoices": "Fakturor",
183 "settings.account.headlinePassword": "Change password", 183 "settings.account.headlinePassword": "Ändra lösenord",
184 "settings.account.headlineProfile": "Uppdatera profil", 184 "settings.account.headlineProfile": "Uppdatera profil",
185 "settings.account.successInfo": "Dina ändringar har sparats", 185 "settings.account.successInfo": "Dina ändringar har sparats",
186 "settings.account.tryReloadServices": "Försök igen", 186 "settings.account.tryReloadServices": "Försök igen",
187 "settings.account.tryReloadUserInfoRequest": "Försök igen", 187 "settings.account.tryReloadUserInfoRequest": "Försök igen",
188 "settings.account.userInfoRequestFailed": "Kunde inte ladda användarinformation", 188 "settings.account.userInfoRequestFailed": "Kunde inte ladda användarinformation",
189 "settings.account.yourLicense": "Your Ferdi License:", 189 "settings.account.yourLicense": "Din Ferdi-licens:",
190 "settings.app.accentColorInfo": "Write your accent color in a CSS-compatible format. (Default: {defaultAccentColor})", 190 "settings.app.accentColorInfo": "Skriv din accentfärg i ett CSS-kompatibelt format. (Standard: {defaultAccentColor})",
191 "settings.app.buttonClearAllCache": "Rensa cache", 191 "settings.app.buttonClearAllCache": "Rensa cache",
192 "settings.app.buttonInstallUpdate": "Starta om & installera uppdatering", 192 "settings.app.buttonInstallUpdate": "Starta om & installera uppdatering",
193 "settings.app.buttonOpenFerdiProfileFolder": "Open Profile folder", 193 "settings.app.buttonOpenFerdiProfileFolder": "Öppna profilmapp",
194 "settings.app.buttonOpenFerdiServiceRecipesFolder": "Open Service Recipes folder", 194 "settings.app.buttonOpenFerdiServiceRecipesFolder": "Öppna Service Recept mapp",
195 "settings.app.buttonSearchForUpdate": "Sök efter uppdateringar", 195 "settings.app.buttonSearchForUpdate": "Sök efter uppdateringar",
196 "settings.app.cacheInfo": "Ferdis cache använder för närvarande {size} diskutrymme.", 196 "settings.app.cacheInfo": "Ferdis cache använder för närvarande {size} diskutrymme.",
197 "settings.app.cacheNotCleared": "Kunde inte rensa alla temporära filer", 197 "settings.app.cacheNotCleared": "Kunde inte rensa alla temporära filer",
198 "settings.app.closeSettings": "Close settings", 198 "settings.app.closeSettings": "Stäng inställningarna",
199 "settings.app.currentVersion": "Nuvarande version:", 199 "settings.app.currentVersion": "Nuvarande version:",
200 "settings.app.form.accentColor": "Accentfärg", 200 "settings.app.form.accentColor": "Accentfärg",
201 "settings.app.form.adaptableDarkMode": "Synkronisera det mörka läget med operativsystemets inställning för mörkt läge", 201 "settings.app.form.adaptableDarkMode": "Synkronisera det mörka läget med operativsystemets inställning för mörkt läge",
@@ -204,18 +204,20 @@
204 "settings.app.form.autoLaunchOnStart": "Starta Ferdi vid uppstart", 204 "settings.app.form.autoLaunchOnStart": "Starta Ferdi vid uppstart",
205 "settings.app.form.automaticUpdates": "Aktivera uppdateringar", 205 "settings.app.form.automaticUpdates": "Aktivera uppdateringar",
206 "settings.app.form.beta": "Inkludera betaversioner", 206 "settings.app.form.beta": "Inkludera betaversioner",
207 "settings.app.form.clipboardNotifications": "Don't show notifications for clipboard events", 207 "settings.app.form.clipboardNotifications": "Visa inte aviseringar för urklipp händelser",
208 "settings.app.form.closeToSystemTray": "Close Ferdi to system tray", 208 "settings.app.form.closeToSystemTray": "Stäng Ferdi till systemfältet",
209 "settings.app.form.confirmOnQuit": "Confirm when quitting Ferdi", 209 "settings.app.form.confirmOnQuit": "Bekräfta när du avslutar Ferdi",
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Anpassad Todo-server",
211 "settings.app.form.darkMode": "Aktivera mörkt läge", 211 "settings.app.form.darkMode": "Aktivera mörkt läge",
212 "settings.app.form.enableGPUAcceleration": "Aktivera GPU-hårdvaruacceleration", 212 "settings.app.form.enableGPUAcceleration": "Aktivera GPU-hårdvaruacceleration",
213 "settings.app.form.enableGlobalHideShortcut": "Aktivera Global genväg för att dölja Ferdi",
213 "settings.app.form.enableLock": "Aktivera lösenordslås", 214 "settings.app.form.enableLock": "Aktivera lösenordslås",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 215 "settings.app.form.enableLongPressServiceHint": "Aktivera servicegenvägledningstips vid långt tryck",
216 "settings.app.form.enableMenuBar": "Visa alltid Ferdi i menyraden",
215 "settings.app.form.enableSpellchecking": "Aktivera stavningskontroll", 217 "settings.app.form.enableSpellchecking": "Aktivera stavningskontroll",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Visa alltid ikon i systemfältet",
217 "settings.app.form.enableTodos": "Aktivera Ferdi Todos", 219 "settings.app.form.enableTodos": "Aktivera Ferdi Todos",
218 "settings.app.form.hibernateOnStartup": "Keep services in hibernation on startup", 220 "settings.app.form.hibernateOnStartup": "Håll tjänster i viloläge vid start",
219 "settings.app.form.hibernationStrategy": "Strategi för vila", 221 "settings.app.form.hibernationStrategy": "Strategi för vila",
220 "settings.app.form.iconSize": "Ikonstorlek för tjänster", 222 "settings.app.form.iconSize": "Ikonstorlek för tjänster",
221 "settings.app.form.inactivityLock": "LÃ¥s efter inaktivitet", 223 "settings.app.form.inactivityLock": "LÃ¥s efter inaktivitet",
@@ -224,8 +226,8 @@
224 "settings.app.form.lockPassword": "Lösenord", 226 "settings.app.form.lockPassword": "Lösenord",
225 "settings.app.form.minimizeToSystemTray": "Minimera Ferdi till systemfältet", 227 "settings.app.form.minimizeToSystemTray": "Minimera Ferdi till systemfältet",
226 "settings.app.form.navigationBarBehaviour": "Navigeringsfältets beteende", 228 "settings.app.form.navigationBarBehaviour": "Navigeringsfältets beteende",
227 "settings.app.form.notifyTaskBarOnMessage": "Notify TaskBar/Dock on new message", 229 "settings.app.form.notifyTaskBarOnMessage": "Meddela Aktivitetsfält/Docka på nytt meddelande",
228 "settings.app.form.passwordToggle": "Password toggle", 230 "settings.app.form.passwordToggle": "Växla lösenord",
229 "settings.app.form.predefinedTodoServer": "Todo-server", 231 "settings.app.form.predefinedTodoServer": "Todo-server",
230 "settings.app.form.privateNotifications": "Visa inte meddelandeinnehåll i aviseringar", 232 "settings.app.form.privateNotifications": "Visa inte meddelandeinnehåll i aviseringar",
231 "settings.app.form.reloadAfterResume": "Ladda om Ferdi efter datorn väckts från vila", 233 "settings.app.form.reloadAfterResume": "Ladda om Ferdi efter datorn väckts från vila",
@@ -233,38 +235,38 @@
233 "settings.app.form.scheduledDNDEnabled": "Aktivera schemalagt Stör ej-läge", 235 "settings.app.form.scheduledDNDEnabled": "Aktivera schemalagt Stör ej-läge",
234 "settings.app.form.scheduledDNDEnd": "Till", 236 "settings.app.form.scheduledDNDEnd": "Till",
235 "settings.app.form.scheduledDNDStart": "Från", 237 "settings.app.form.scheduledDNDStart": "Från",
236 "settings.app.form.searchEngine": "Search engine", 238 "settings.app.form.searchEngine": "Sökmotor",
237 "settings.app.form.sentry": "Skicka telemetridata", 239 "settings.app.form.sentry": "Skicka telemetridata",
238 "settings.app.form.serviceRibbonWidth": "Sidofältets bredd", 240 "settings.app.form.serviceRibbonWidth": "Sidofältets bredd",
239 "settings.app.form.showDisabledServices": "Visa flikar för inaktiverade tjänster", 241 "settings.app.form.showDisabledServices": "Visa flikar för inaktiverade tjänster",
240 "settings.app.form.showDragArea": "Visa dragbart område i fönstret", 242 "settings.app.form.showDragArea": "Visa dragbart område i fönstret",
241 "settings.app.form.showMessagesBadgesWhenMuted": "Visa antal olästa meddelanden när aviseringar är inaktiverade", 243 "settings.app.form.showMessagesBadgesWhenMuted": "Visa antal olästa meddelanden när aviseringar är inaktiverade",
242 "settings.app.form.splitMode": "Enable Split View Mode", 244 "settings.app.form.splitMode": "Aktivera delat visningsläge",
243 "settings.app.form.startMinimized": "Starta i minimerat läge", 245 "settings.app.form.startMinimized": "Starta i minimerat läge",
244 "settings.app.form.universalDarkMode": "Aktivera globalt mörkt läge", 246 "settings.app.form.universalDarkMode": "Aktivera globalt mörkt läge",
245 "settings.app.form.useTouchIdToUnlock": "Allow using TouchID to unlock Ferdi", 247 "settings.app.form.useTouchIdToUnlock": "Tillåt att TouchID används för att låsa upp Ferdi",
246 "settings.app.form.useVerticalStyle": "Use horizontal style", 248 "settings.app.form.useVerticalStyle": "Använd horisontell stil",
247 "settings.app.form.wakeUpStrategy": "Wake up strategy", 249 "settings.app.form.wakeUpStrategy": "Vakna strategi",
248 "settings.app.headlineAdvanced": "Avancerat", 250 "settings.app.headlineAdvanced": "Avancerat",
249 "settings.app.headlineAppearance": "Utseende", 251 "settings.app.headlineAppearance": "Utseende",
250 "settings.app.headlineGeneral": "Allmänt", 252 "settings.app.headlineGeneral": "Allmänt",
251 "settings.app.headlineLanguage": "Språk", 253 "settings.app.headlineLanguage": "Språk",
252 "settings.app.headlinePrivacy": "Privacy", 254 "settings.app.headlinePrivacy": "Sekretess",
253 "settings.app.headlineUpdates": "Uppdateringar", 255 "settings.app.headlineUpdates": "Uppdateringar",
254 "settings.app.hibernateInfo": "Som standard kommer Ferdi att hålla alla dina tjänster öppna och laddade i bakgrunden så att de är redo när du vill använda dem. Viloläget kommer att stänga dina tjänster efter ett angivet belopp. Detta är användbart för att spara på arbetsminne eller se till att tjänster inte saktar ner datorn.", 256 "settings.app.hibernateInfo": "Som standard kommer Ferdi att hålla alla dina tjänster öppna och laddade i bakgrunden så att de är redo när du vill använda dem. Viloläget kommer att stänga dina tjänster efter ett angivet belopp. Detta är användbart för att spara på arbetsminne eller se till att tjänster inte saktar ner datorn.",
255 "settings.app.inactivityLockInfo": "Antal minuter av inaktivitet, varefter Ferdi låses automatiskt. Ange 0 för att inaktivera", 257 "settings.app.inactivityLockInfo": "Antal minuter av inaktivitet, varefter Ferdi låses automatiskt. Ange 0 för att inaktivera",
256 "settings.app.languageDisclaimer": "Engelska och tyska är officella översättningar. Övriga språk har översatts av gemenskapen.", 258 "settings.app.languageDisclaimer": "Engelska och tyska är officella översättningar. Övriga språk har översatts av gemenskapen.",
257 "settings.app.lockInfo": "Password Lock allows you to keep your messages protected.\nUsing Password Lock, you will be prompted to enter your password everytime you start Ferdi or lock Ferdi yourself using the lock symbol in the bottom left corner or the shortcut {lockShortcut}.", 259 "settings.app.lockInfo": "Lösenordslås låter dig skydda dina meddelanden.\nAnvända lösenordsblock, du kommer att bli ombedd att ange ditt lösenord varje gång du startar Ferdi eller låsa Ferdi själv med hjälp av låssymbolen i det nedre vänstra hörnet eller genvägen {lockShortcut}.",
258 "settings.app.lockedPassword": "Lösenord", 260 "settings.app.lockedPassword": "Lösenord",
259 "settings.app.lockedPasswordInfo": "Please make sure to set a password you'll remember.\nIf you loose this password, you will have to reinstall Ferdi.", 261 "settings.app.lockedPasswordInfo": "Se till att du anger ett lösenord du kommer att komma ihåg.\nOm du tappar bort detta lösenord måste du installera om Ferdi.",
260 "settings.app.restartRequired": "Ändringar kräver omstart", 262 "settings.app.restartRequired": "Ändringar kräver omstart",
261 "settings.app.scheduledDNDInfo": "Schemalagd \"Stör ej\" låter dig definiera en tidsperiod inom vilken du inte vill få meddelanden från Ferdi.", 263 "settings.app.scheduledDNDInfo": "Schemalagd \"Stör ej\" låter dig definiera en tidsperiod inom vilken du inte vill få meddelanden från Ferdi.",
262 "settings.app.scheduledDNDTimeInfo": "Tid i 24-timmarsformat. Sluttid kan vara före starttid (t.ex. start 17:00, slut 09:00) för att aktivera \"Stör ej\" över natten.", 264 "settings.app.scheduledDNDTimeInfo": "Tid i 24-timmarsformat. Sluttid kan vara före starttid (t.ex. start 17:00, slut 09:00) för att aktivera \"Stör ej\" över natten.",
263 "settings.app.sentryInfo": "Sending telemetry data allows us to find errors in Ferdi - we will not send any personal information like your message data!", 265 "settings.app.sentryInfo": "Genom att skicka telemetri data kan vi hitta fel i Ferdi - vi kommer inte att skicka någon personlig information som dina meddelandedata!",
264 "settings.app.spellCheckerLanguageInfo": "Ferdi uses your Mac's build-in spellchecker to check for typos. If you want to change the languages the spellchecker checks for, you can do so in your Mac's System Preferences.", 266 "settings.app.spellCheckerLanguageInfo": "Ferdi använder Mac's inbyggda stavningskontroll för att kontrollera skrivfel. Om du vill ändra de språk som stavningskontrollen kontrollerar efter kan du göra det i din Macs Systeminställningar.",
265 "settings.app.subheadlineCache": "Cache", 267 "settings.app.subheadlineCache": "Cache",
266 "settings.app.subheadlineFerdiProfile": "Ferdi Profile", 268 "settings.app.subheadlineFerdiProfile": "Ferdi Profile",
267 "settings.app.todoServerInfo": "This server will be used for the \"Ferdi Todo\" feature.", 269 "settings.app.todoServerInfo": "Denna server kommer att användas för \"Ferdi Todo\"-funktionen.",
268 "settings.app.translationHelp": "Hjälp oss att översätta Ferdi till ditt språk.", 270 "settings.app.translationHelp": "Hjälp oss att översätta Ferdi till ditt språk.",
269 "settings.app.universalDarkModeInfo": "Globalt mörkt läge försöker att dynamiskt generera en mörk stil för tjänster som ännu inte stöds.", 271 "settings.app.universalDarkModeInfo": "Globalt mörkt läge försöker att dynamiskt generera en mörk stil för tjänster som ännu inte stöds.",
270 "settings.app.updateStatusAvailable": "Uppdatering tillgänglig, laddar ner...", 272 "settings.app.updateStatusAvailable": "Uppdatering tillgänglig, laddar ner...",
@@ -283,12 +285,12 @@
283 "settings.recipes.customService.headline.communityRecipes": "Tredjepartsrecept från gemenskapen", 285 "settings.recipes.customService.headline.communityRecipes": "Tredjepartsrecept från gemenskapen",
284 "settings.recipes.customService.headline.customRecipes": "Egna tredjepartsrecept", 286 "settings.recipes.customService.headline.customRecipes": "Egna tredjepartsrecept",
285 "settings.recipes.customService.headline.devRecipes": "Dina recept för utvecklingsservice", 287 "settings.recipes.customService.headline.devRecipes": "Dina recept för utvecklingsservice",
286 "settings.recipes.customService.intro": "To add a custom service, copy the service recipe to:", 288 "settings.recipes.customService.intro": "För att lägga till en anpassad tjänst, kopiera receptet för tjänsten till:",
287 "settings.recipes.customService.openDevDocs": "Dokumentation för utvecklare", 289 "settings.recipes.customService.openDevDocs": "Dokumentation för utvecklare",
288 "settings.recipes.customService.openFolder": "Open folder", 290 "settings.recipes.customService.openFolder": "Öppna mapp",
289 "settings.recipes.headline": "Tillgängliga tjänster", 291 "settings.recipes.headline": "Tillgängliga tjänster",
290 "settings.recipes.missingService": "Saknar du en tjänst?", 292 "settings.recipes.missingService": "Saknar du en tjänst?",
291 "settings.recipes.nothingFound": "Sorry, but no service matched your search term - but you can still probably add it using the \"Custom Website\" option. Please note that the website might show more services that have been added to Ferdi since the version that you are currently on. To get those new services, please consider upgrading to a newer version of Ferdi.", 293 "settings.recipes.nothingFound": "Tyvärr, men ingen tjänst matchade ditt sökord - men du kan fortfarande förmodligen lägga till det med \"Anpassad webbplats\" alternativet. Observera att webbplatsen kan visa fler tjänster som har lagts till Ferdi sedan den version som du för närvarande är på. För att få dessa nya tjänster, överväg att uppgradera till en nyare version av Ferdi.",
292 "settings.recipes.servicesSuccessfulAddedInfo": "Tjänsten har lagts till", 294 "settings.recipes.servicesSuccessfulAddedInfo": "Tjänsten har lagts till",
293 "settings.searchService": "Sök efter tjänst", 295 "settings.searchService": "Sök efter tjänst",
294 "settings.service.error.goBack": "Tillbaka till tjänster", 296 "settings.service.error.goBack": "Tillbaka till tjänster",
@@ -298,19 +300,20 @@
298 "settings.service.form.availableServices": "Tillgängliga tjänster", 300 "settings.service.form.availableServices": "Tillgängliga tjänster",
299 "settings.service.form.customUrl": "Anpassad server", 301 "settings.service.form.customUrl": "Anpassad server",
300 "settings.service.form.customUrlValidationError": "Kunde inte validera anpassad {name} -server.", 302 "settings.service.form.customUrlValidationError": "Kunde inte validera anpassad {name} -server.",
301 "settings.service.form.darkReaderBrightness": "Dark Reader Brightness", 303 "settings.service.form.darkReaderBrightness": "Mörk läsare ljusstyrka",
302 "settings.service.form.darkReaderContrast": "Dark Reader Contrast", 304 "settings.service.form.darkReaderContrast": "Mörk Läsare kontrast",
303 "settings.service.form.darkReaderSepia": "Dark Reader Sepia", 305 "settings.service.form.darkReaderSepia": "Mörk Läsare Sepia",
304 "settings.service.form.deleteButton": "Delete service", 306 "settings.service.form.deleteButton": "Ta bort tjänst",
305 "settings.service.form.editServiceHeadline": "Redigera {name}", 307 "settings.service.form.editServiceHeadline": "Redigera {name}",
306 "settings.service.form.enableAudio": "Aktivera ljud", 308 "settings.service.form.enableAudio": "Aktivera ljud",
307 "settings.service.form.enableBadge": "Visa olästa meddelandemärken", 309 "settings.service.form.enableBadge": "Visa olästa meddelandemärken",
308 "settings.service.form.enableDarkMode": "Aktivera mörkt läge", 310 "settings.service.form.enableDarkMode": "Aktivera mörkt läge",
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Aktivera hibernation",
310 "settings.service.form.enableNotification": "Aktivera aviseringar", 312 "settings.service.form.enableNotification": "Aktivera aviseringar",
311 "settings.service.form.enableService": "Aktivera tjänst", 313 "settings.service.form.enableService": "Aktivera tjänst",
314 "settings.service.form.enableWakeUp": "Aktivera vakna",
312 "settings.service.form.headlineBadges": "Olästa meddelandemärken", 315 "settings.service.form.headlineBadges": "Olästa meddelandemärken",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Mörk läsare inställningar",
314 "settings.service.form.headlineGeneral": "Allmänt", 317 "settings.service.form.headlineGeneral": "Allmänt",
315 "settings.service.form.headlineNotifications": "Aviseringar", 318 "settings.service.form.headlineNotifications": "Aviseringar",
316 "settings.service.form.icon": "Anpassad ikon", 319 "settings.service.form.icon": "Anpassad ikon",
@@ -318,21 +321,21 @@
318 "settings.service.form.iconUpload": "Släpp din bild, eller klicka här", 321 "settings.service.form.iconUpload": "Släpp din bild, eller klicka här",
319 "settings.service.form.indirectMessageInfo": "Du kommer att aviseras om alla nya meddelanden i en kanal, inte bara @username, @channel, @here, ...", 322 "settings.service.form.indirectMessageInfo": "Du kommer att aviseras om alla nya meddelanden i en kanal, inte bara @username, @channel, @here, ...",
320 "settings.service.form.indirectMessages": "Visa meddelandemärke för alla nya meddelanden", 323 "settings.service.form.indirectMessages": "Visa meddelandemärke för alla nya meddelanden",
321 "settings.service.form.isHibernatedEnabledInfo": "When enabled, a service will be shut down after a period of time to save system resources.", 324 "settings.service.form.isHibernatedEnabledInfo": "När den är aktiverad kommer en tjänst att stängas av efter en tidsperiod för att spara systemresurser.",
322 "settings.service.form.isMutedInfo": "Om inaktiverad kommer alla aviseringsljud och ljuduppspelning att tystas", 325 "settings.service.form.isMutedInfo": "Om inaktiverad kommer alla aviseringsljud och ljuduppspelning att tystas",
323 "settings.service.form.name": "Namn", 326 "settings.service.form.name": "Namn",
324 "settings.service.form.onlyShowFavoritesInUnreadCount": "Only show Favorites in unread count", 327 "settings.service.form.onlyShowFavoritesInUnreadCount": "Visa endast favoriter i olästa antal",
325 "settings.service.form.openDarkmodeCss": "Öppna darkmode.css", 328 "settings.service.form.openDarkmodeCss": "Öppna darkmode.css",
326 "settings.service.form.openUserCss": "Öppna user.css", 329 "settings.service.form.openUserCss": "Öppna user.css",
327 "settings.service.form.openUserJs": "Öppna user.js", 330 "settings.service.form.openUserJs": "Öppna user.js",
328 "settings.service.form.proxy.headline": "Inställningar för HTTP/HTTPS-proxy", 331 "settings.service.form.proxy.headline": "Inställningar för HTTP/HTTPS-proxy",
329 "settings.service.form.proxy.host": "Proxy-värd/IP", 332 "settings.service.form.proxy.host": "Proxy-värd/IP",
330 "settings.service.form.proxy.info": "Proxy settings will not be synchronized with the Ferdi servers.", 333 "settings.service.form.proxy.info": "Proxyinställningarna kommer inte att synkroniseras med Ferdi servrarna.",
331 "settings.service.form.proxy.isEnabled": "Använd proxy", 334 "settings.service.form.proxy.isEnabled": "Använd proxy",
332 "settings.service.form.proxy.password": "Password (optional)", 335 "settings.service.form.proxy.password": "Lösenord (frivilligt)",
333 "settings.service.form.proxy.port": "Port", 336 "settings.service.form.proxy.port": "Port",
334 "settings.service.form.proxy.restartInfo": "Vänligen starta om Ferdi efter att du ändrat proxyinställningar.", 337 "settings.service.form.proxy.restartInfo": "Vänligen starta om Ferdi efter att du ändrat proxyinställningar.",
335 "settings.service.form.proxy.user": "User (optional)", 338 "settings.service.form.proxy.user": "Användare (valfritt)",
336 "settings.service.form.recipeFileInfo": "Dina användarfiler kommer att infogas i webbsidan så att du kan anpassa tjänsterna hur du vill. Användarfiler lagras endast lokalt och överförs inte till andra datorer som använer samma konto.", 339 "settings.service.form.recipeFileInfo": "Dina användarfiler kommer att infogas i webbsidan så att du kan anpassa tjänsterna hur du vill. Användarfiler lagras endast lokalt och överförs inte till andra datorer som använer samma konto.",
337 "settings.service.form.saveButton": "Spara tjänst", 340 "settings.service.form.saveButton": "Spara tjänst",
338 "settings.service.form.tabHosted": "Värd", 341 "settings.service.form.tabHosted": "Värd",
@@ -343,32 +346,32 @@
343 "settings.services.deletedInfo": "Tjänsten har tagits bort", 346 "settings.services.deletedInfo": "Tjänsten har tagits bort",
344 "settings.services.discoverServices": "Upptäck tjänster", 347 "settings.services.discoverServices": "Upptäck tjänster",
345 "settings.services.headline": "Dina tjänster", 348 "settings.services.headline": "Dina tjänster",
346 "settings.services.noServicesAdded": "Start by adding a service.", 349 "settings.services.noServicesAdded": "Börja med att lägga till en tjänst.",
347 "settings.services.nothingFound": "Sorry, but no service matched your search term - but you can still probably add it using the \"Custom Website\" option. Please note that the website might show more services that have been added to Ferdi since the version that you are currently on. To get those new services, please consider upgrading to a newer version of Ferdi.", 350 "settings.services.nothingFound": "Tyvärr, men ingen tjänst matchade ditt sökord - men du kan fortfarande förmodligen lägga till det med \"Anpassad webbplats\" alternativet. Observera att webbplatsen kan visa fler tjänster som har lagts till Ferdi sedan den version som du för närvarande är på. För att få dessa nya tjänster, överväg att uppgradera till en nyare version av Ferdi.",
348 "settings.services.servicesRequestFailed": "Kunde inte ladda dina tjänster", 351 "settings.services.servicesRequestFailed": "Kunde inte ladda dina tjänster",
349 "settings.services.tooltip.isDisabled": "Tjänsten är inaktiverad", 352 "settings.services.tooltip.isDisabled": "Tjänsten är inaktiverad",
350 "settings.services.tooltip.isMuted": "Alla ljud är avstängda", 353 "settings.services.tooltip.isMuted": "Alla ljud är avstängda",
351 "settings.services.tooltip.notificationsDisabled": "Aviseringar är inaktiverade", 354 "settings.services.tooltip.notificationsDisabled": "Aviseringar är inaktiverade",
352 "settings.services.updatedInfo": "Dina ändringar har sparats", 355 "settings.services.updatedInfo": "Dina ändringar har sparats",
353 "settings.supportFerdi.aboutIntro": "<p>Ferdi is an open-source and a community-lead application.</p><p>Thanks to the people who make this possbile:</p>", 356 "settings.supportFerdi.aboutIntro": "<p>Ferdi är en öppen källkod och en community-ledd applikation.</p><p>Tack till de personer som gör detta besitter:</p>",
354 "settings.supportFerdi.bannerText": "Do you want to help us improve Ferdi?", 357 "settings.supportFerdi.bannerText": "Vill du hjälpa oss att förbättra Ferdi?",
355 "settings.supportFerdi.headline": "Om Ferdi", 358 "settings.supportFerdi.headline": "Om Ferdi",
356 "settings.supportFerdi.openSurvey": "Open survey", 359 "settings.supportFerdi.openSurvey": "Öppna enkäten",
357 "settings.supportFerdi.textDonation": "If you feel like supporting Ferdi development with a donation, you can do so on both,", 360 "settings.supportFerdi.textDonation": "Om du känner för att stödja Ferdi utveckling med en donation, kan du göra det på båda,",
358 "settings.supportFerdi.textDonationAnd": "and", 361 "settings.supportFerdi.textDonationAnd": "och",
359 "settings.supportFerdi.textExpenses": "While volunteers do most of the work, we still need to pay for servers and certificates. As a community, we are fully transparent on funds we collect and spend - see our", 362 "settings.supportFerdi.textExpenses": "Medan volontärer gör det mesta av arbetet, måste vi fortfarande betala för servrar och certifikat. Som ett samhälle är vi helt öppna för medel som vi samlar in och spenderar - se vår",
360 "settings.supportFerdi.textGitHubSponsors": "GitHub Sponsors", 363 "settings.supportFerdi.textGitHubSponsors": "GitHub Sponsors",
361 "settings.supportFerdi.textListContributors": "Full list of contributors", 364 "settings.supportFerdi.textListContributors": "Fullständig lista över bidragsgivare",
362 "settings.supportFerdi.textListContributorsHere": "here", 365 "settings.supportFerdi.textListContributorsHere": "här",
363 "settings.supportFerdi.textOpenCollective": "Open Collective", 366 "settings.supportFerdi.textOpenCollective": "Open Collective",
364 "settings.supportFerdi.textSupportWelcome": "Support is always welcome. You can find a list of the help we need", 367 "settings.supportFerdi.textSupportWelcome": "Stöd är alltid välkommet. Du hittar en lista över den hjälp vi behöver",
365 "settings.supportFerdi.textSupportWelcomeHere": "here", 368 "settings.supportFerdi.textSupportWelcomeHere": "här",
366 "settings.supportFerdi.textVolunteers": "The development of Ferdi is done by volunteers. People who use Ferdi like you. They maintain, fix, and improve Ferdi in their spare time.", 369 "settings.supportFerdi.textVolunteers": "Utvecklingen av Ferdi görs av volontärer. Människor som använder Ferdi som du. De underhåller, fixar och förbättrar Ferdi på sin fritid.",
367 "settings.supportFerdi.title": "Do you like Ferdi?", 370 "settings.supportFerdi.title": "Tycker du om Ferdi?",
368 "settings.team.contentHeadline": "Hantera Franz-grupp", 371 "settings.team.contentHeadline": "Hantera Franz-grupp",
369 "settings.team.copy": "Grupphanteringen i Franz låter dig hantera Franz-abonnemang för flera användare. Observera att ett Franz Premium-abonnemang inte ger dig några extra funktioner i Ferdi: Den enda anledningen till att du fortfarande har tillgång till grupphanteringen är att du kan hantera dina äldre Franz-grupper så att du inte förlorar någon funktionalitet i hanteringen av ditt konto.", 372 "settings.team.copy": "Grupphanteringen i Franz låter dig hantera Franz-abonnemang för flera användare. Observera att ett Franz Premium-abonnemang inte ger dig några extra funktioner i Ferdi: Den enda anledningen till att du fortfarande har tillgång till grupphanteringen är att du kan hantera dina äldre Franz-grupper så att du inte förlorar någon funktionalitet i hanteringen av ditt konto.",
370 "settings.team.headline": "Grupp", 373 "settings.team.headline": "Grupp",
371 "settings.team.intro": "You are currently using Franz Servers, which is why you have access to Team Management.", 374 "settings.team.intro": "Du använder för närvarande Franz servrar, vilket är anledningen till att du har tillgång till Team Management.",
372 "settings.team.manageAction": "Hantera din grupp på meetfranz.com", 375 "settings.team.manageAction": "Hantera din grupp på meetfranz.com",
373 "settings.team.teamsUnavailable": "Grupper är inte tillgängliga", 376 "settings.team.teamsUnavailable": "Grupper är inte tillgängliga",
374 "settings.team.teamsUnavailableInfo": "Grupper är för närvarande endast tillgängliga när du använder Franz Server och har betalat för Franz Professional. Vänligen ändra din server till https://api.franzinfra.com för att använda gruppfunktionalitet.", 377 "settings.team.teamsUnavailableInfo": "Grupper är för närvarande endast tillgängliga när du använder Franz Server och har betalat för Franz Professional. Vänligen ändra din server till https://api.franzinfra.com för att använda gruppfunktionalitet.",
@@ -378,8 +381,8 @@
378 "settings.user.form.accountType.non-profit": "Ideell organisation", 381 "settings.user.form.accountType.non-profit": "Ideell organisation",
379 "settings.user.form.currentPassword": "Nuvarande lösenord", 382 "settings.user.form.currentPassword": "Nuvarande lösenord",
380 "settings.user.form.email": "E-post", 383 "settings.user.form.email": "E-post",
381 "settings.user.form.firstname": "First Name", 384 "settings.user.form.firstname": "Förnamn",
382 "settings.user.form.lastname": "Last Name", 385 "settings.user.form.lastname": "Efternamn",
383 "settings.user.form.newPassword": "Nytt lösenord", 386 "settings.user.form.newPassword": "Nytt lösenord",
384 "settings.workspace.add.form.name": "Namn", 387 "settings.workspace.add.form.name": "Namn",
385 "settings.workspace.add.form.submitButton": "Skapa arbetsyta", 388 "settings.workspace.add.form.submitButton": "Skapa arbetsyta",
@@ -392,15 +395,15 @@
392 "settings.workspace.form.yourWorkspaces": "Dina arbetsytor", 395 "settings.workspace.form.yourWorkspaces": "Dina arbetsytor",
393 "settings.workspaces.deletedInfo": "Arbetsytan har tagits bort", 396 "settings.workspaces.deletedInfo": "Arbetsytan har tagits bort",
394 "settings.workspaces.headline": "Dina arbetsytor", 397 "settings.workspaces.headline": "Dina arbetsytor",
395 "settings.workspaces.noWorkspacesAdded": "You haven't created any workspaces yet.", 398 "settings.workspaces.noWorkspacesAdded": "Du har inte lagt till några arbetsytor än.",
396 "settings.workspaces.tryReloadWorkspaces": "Försök igen", 399 "settings.workspaces.tryReloadWorkspaces": "Försök igen",
397 "settings.workspaces.updatedInfo": "Dina ändringar har sparats", 400 "settings.workspaces.updatedInfo": "Dina ändringar har sparats",
398 "settings.workspaces.workspaceFeatureHeadline": "\"Less is more\": Vi presenterar Ferdi-arbetsytor", 401 "settings.workspaces.workspaceFeatureHeadline": "\"Less is more\": Vi presenterar Ferdi-arbetsytor",
399 "settings.workspaces.workspaceFeatureInfo": "Ferdi Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time. You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.", 402 "settings.workspaces.workspaceFeatureInfo": "Ferdi Workspaces låter dig fokusera på vad som är viktigt just nu. Konfigurera olika uppsättningar av tjänster och växla enkelt mellan dem när som helst. Du bestämmer vilka tjänster du behöver när och var, så att vi kan hjälpa dig att hålla koll på ditt spel - eller enkelt stänga av från jobbet när du vill.",
400 "settings.workspaces.workspacesRequestFailed": "Kunde inte ladda dina arbetsytor", 403 "settings.workspaces.workspacesRequestFailed": "Kunde inte ladda dina arbetsytor",
401 "setupAssistant.headline": "Let's get started", 404 "setupAssistant.headline": "Kom igång",
402 "setupAssistant.subheadline": "Choose from our most used services and get back on top of your messaging now.", 405 "setupAssistant.subheadline": "Välj bland våra mest använda tjänster och kom tillbaka på toppen av dina meddelanden nu.",
403 "setupAssistant.submit.label": "Let's go", 406 "setupAssistant.submit.label": "Sätt igång",
404 "sidebar.addNewService": "Lägg till ny tjänst", 407 "sidebar.addNewService": "Lägg till ny tjänst",
405 "sidebar.closeTodosDrawer": "Stäng Ferdi Todos", 408 "sidebar.closeTodosDrawer": "Stäng Ferdi Todos",
406 "sidebar.closeWorkspaceDrawer": "Stäng arbetsytan", 409 "sidebar.closeWorkspaceDrawer": "Stäng arbetsytan",
@@ -411,33 +414,33 @@
411 "sidebar.unmuteApp": "Aktivera aviseringar och ljud", 414 "sidebar.unmuteApp": "Aktivera aviseringar och ljud",
412 "signup.email.label": "E-postadress", 415 "signup.email.label": "E-postadress",
413 "signup.emailDuplicate": "En användare med den e-postadressen finns redan", 416 "signup.emailDuplicate": "En användare med den e-postadressen finns redan",
414 "signup.firstname.label": "First Name", 417 "signup.firstname.label": "Förnamn",
415 "signup.headline": "Registrera dig", 418 "signup.headline": "Registrera dig",
416 "signup.lastname.label": "Last Name", 419 "signup.lastname.label": "Efternamn",
417 "signup.legal.info": "Genom att skapa ett Ferdi konto accepterar du", 420 "signup.legal.info": "Genom att skapa ett Ferdi konto accepterar du",
418 "signup.legal.privacy": "Integritetspolicy", 421 "signup.legal.privacy": "Integritetspolicy",
419 "signup.legal.terms": "Användarvillkor", 422 "signup.legal.terms": "Användarvillkor",
420 "signup.link.login": "Har du redan ett konto? Logga in", 423 "signup.link.login": "Har du redan ett konto? Logga in",
421 "signup.password.label": "Lösenord", 424 "signup.password.label": "Lösenord",
422 "signup.submit.label": "Skapa konto", 425 "signup.submit.label": "Skapa konto",
423 "tabs.item.confirmDeleteService": "Do you really want to delete the {serviceName} service?", 426 "tabs.item.confirmDeleteService": "Vill du verkligen ta bort tjänsten {serviceName}?",
424 "tabs.item.deleteService": "Delete service", 427 "tabs.item.deleteService": "Ta bort tjänst",
425 "tabs.item.disableAudio": "Inaktivera ljud", 428 "tabs.item.disableAudio": "Inaktivera ljud",
426 "tabs.item.disableDarkMode": "Disable Dark mode", 429 "tabs.item.disableDarkMode": "Inaktivera mörkt läge",
427 "tabs.item.disableNotifications": "Inaktivera aviseringar", 430 "tabs.item.disableNotifications": "Inaktivera aviseringar",
428 "tabs.item.disableService": "Disable service", 431 "tabs.item.disableService": "Inaktivera tjänst",
429 "tabs.item.enableAudio": "Aktivera ljud", 432 "tabs.item.enableAudio": "Aktivera ljud",
430 "tabs.item.enableDarkMode": "Enable Dark mode", 433 "tabs.item.enableDarkMode": "Aktivera mörkt läge",
431 "tabs.item.enableNotification": "Aktivera aviseringar", 434 "tabs.item.enableNotification": "Aktivera aviseringar",
432 "tabs.item.enableService": "Aktivera tjänst", 435 "tabs.item.enableService": "Aktivera tjänst",
433 "tabs.item.hibernateService": "Hibernate service", 436 "tabs.item.hibernateService": "Hibernate service",
434 "tabs.item.reload": "Ladda om", 437 "tabs.item.reload": "Ladda om",
435 "tabs.item.wakeUpService": "Wake up service", 438 "tabs.item.wakeUpService": "Väck enhet",
436 "validation.email": "{field} is not valid", 439 "validation.email": "{field} är felaktig",
437 "validation.minLength": "{field} should be at least {length} characters long", 440 "validation.minLength": "{field} bör vara minst {length} tecken lång",
438 "validation.oneRequired": "Minst en krävs", 441 "validation.oneRequired": "Minst en krävs",
439 "validation.required": "{field} is required", 442 "validation.required": "{field} krävs",
440 "validation.url": "{field} is not a valid URL", 443 "validation.url": "{field} är inte en giltig URL",
441 "webControls.back": "Tillbaka", 444 "webControls.back": "Tillbaka",
442 "webControls.forward": "Framåt", 445 "webControls.forward": "Framåt",
443 "webControls.goHome": "Hem", 446 "webControls.goHome": "Hem",
@@ -445,12 +448,12 @@
445 "webControls.reload": "Ladda om", 448 "webControls.reload": "Ladda om",
446 "welcome.loginButton": "Logga in på ditt konto", 449 "welcome.loginButton": "Logga in på ditt konto",
447 "welcome.signupButton": "Skapa ett gratis konto", 450 "welcome.signupButton": "Skapa ett gratis konto",
448 "workspaceDrawer.addNewWorkspaceLabel": "Add new workspace", 451 "workspaceDrawer.addNewWorkspaceLabel": "Skapa ny arbetsyta",
449 "workspaceDrawer.allServices": "Alla tjänster", 452 "workspaceDrawer.allServices": "Alla tjänster",
450 "workspaceDrawer.headline": "Arbetsytor", 453 "workspaceDrawer.headline": "Arbetsytor",
451 "workspaceDrawer.item.contextMenuEdit": "redigera", 454 "workspaceDrawer.item.contextMenuEdit": "redigera",
452 "workspaceDrawer.item.noServicesAddedYet": "Inga tjänster har lagts till", 455 "workspaceDrawer.item.noServicesAddedYet": "Inga tjänster har lagts till",
453 "workspaceDrawer.workspaceFeatureInfo": "<p>Ferdi Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time.</p><p>You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.</p>", 456 "workspaceDrawer.workspaceFeatureInfo": "<p>Ferdi-arbetsytor låter dig fokusera på det som är viktigt just nu. Konfigurera olika uppsättningar av tjänster och växla enkelt mellan dem när som helst.</p><p>Du bestämmer vilka tjänster du behöver när och var, så att vi kan hjälpa dig att hålla koll på läget - eller enkelt koppla bort från jobbet när du vill.</p>",
454 "workspaceDrawer.workspacesSettingsTooltip": "Edit workspaces settings", 457 "workspaceDrawer.workspacesSettingsTooltip": "Redigera inställningar för arbetsytor",
455 "workspaces.switchingIndicator.switchingTo": "Byter till" 458 "workspaces.switchingIndicator.switchingTo": "Byter till"
456} 459}
diff --git a/src/i18n/locales/te.json b/src/i18n/locales/te.json
index 812ec8c4c..d5cf072f6 100644
--- a/src/i18n/locales/te.json
+++ b/src/i18n/locales/te.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Enable Dark Mode", 211 "settings.app.form.darkMode": "Enable Dark Mode",
212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration", 212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Enable spell checking", 217 "settings.app.form.enableSpellchecking": "Enable spell checking",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Enable notifications", 312 "settings.service.form.enableNotification": "Enable notifications",
311 "settings.service.form.enableService": "Enable service", 313 "settings.service.form.enableService": "Enable service",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Unread message badges", 315 "settings.service.form.headlineBadges": "Unread message badges",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "General", 317 "settings.service.form.headlineGeneral": "General",
diff --git a/src/i18n/locales/tr.json b/src/i18n/locales/tr.json
index b0368a18a..99e4d4358 100644
--- a/src/i18n/locales/tr.json
+++ b/src/i18n/locales/tr.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Karanlık modu aç", 211 "settings.app.form.darkMode": "Karanlık modu aç",
212 "settings.app.form.enableGPUAcceleration": "Grafik İşlemci Ünitesi (GPU) Hızlandırıcısını Aktif et", 212 "settings.app.form.enableGPUAcceleration": "Grafik İşlemci Ünitesi (GPU) Hızlandırıcısını Aktif et",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Yazım denetimini etkinleştir", 217 "settings.app.form.enableSpellchecking": "Yazım denetimini etkinleştir",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Bildirimleri etkinleÅŸtir", 312 "settings.service.form.enableNotification": "Bildirimleri etkinleÅŸtir",
311 "settings.service.form.enableService": "Servisi etkinleÅŸtir", 313 "settings.service.form.enableService": "Servisi etkinleÅŸtir",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Okunmamış mesajlar", 315 "settings.service.form.headlineBadges": "Okunmamış mesajlar",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "Genel", 317 "settings.service.form.headlineGeneral": "Genel",
diff --git a/src/i18n/locales/uk.json b/src/i18n/locales/uk.json
index b03bf9072..d1cf6f9c7 100644
--- a/src/i18n/locales/uk.json
+++ b/src/i18n/locales/uk.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Переходь на Темну Сторону", 211 "settings.app.form.darkMode": "Переходь на Темну Сторону",
212 "settings.app.form.enableGPUAcceleration": "Ввімкнути приÑÐºÐ¾Ñ€ÐµÐ½Ð½Ñ GPU", 212 "settings.app.form.enableGPUAcceleration": "Ввімкнути приÑÐºÐ¾Ñ€ÐµÐ½Ð½Ñ GPU",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Увімкнути перевірку орфографії", 217 "settings.app.form.enableSpellchecking": "Увімкнути перевірку орфографії",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "Enable hibernation",
310 "settings.service.form.enableNotification": "Увімкнути ÑповіщеннÑ", 312 "settings.service.form.enableNotification": "Увімкнути ÑповіщеннÑ",
311 "settings.service.form.enableService": "Увімкнути ÑервіÑ", 313 "settings.service.form.enableService": "Увімкнути ÑервіÑ",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Значки непрочитаних повідомлень", 315 "settings.service.form.headlineBadges": "Значки непрочитаних повідомлень",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "Загальні", 317 "settings.service.form.headlineGeneral": "Загальні",
diff --git a/src/i18n/locales/vi.json b/src/i18n/locales/vi.json
index 668b75127..e658b959c 100644
--- a/src/i18n/locales/vi.json
+++ b/src/i18n/locales/vi.json
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "Cho phép chế Ä‘á»™ ná»n tối", 211 "settings.app.form.darkMode": "Cho phép chế Ä‘á»™ ná»n tối",
212 "settings.app.form.enableGPUAcceleration": "Bật Tăng tốc GPU", 212 "settings.app.form.enableGPUAcceleration": "Bật Tăng tốc GPU",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Cho phép khóa bằng mật khẩu", 214 "settings.app.form.enableLock": "Cho phép khóa bằng mật khẩu",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Luôn hiển thị Ferdi trong Menu Bar", 216 "settings.app.form.enableMenuBar": "Luôn hiển thị Ferdi trong Menu Bar",
215 "settings.app.form.enableSpellchecking": "Kích hoạt tính năng kiểm tra chính tả", 217 "settings.app.form.enableSpellchecking": "Kích hoạt tính năng kiểm tra chính tả",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -309,6 +311,7 @@
309 "settings.service.form.enableHibernation": "Kích hoạt ngủ đông", 311 "settings.service.form.enableHibernation": "Kích hoạt ngủ đông",
310 "settings.service.form.enableNotification": "Kích hoạt thông báo", 312 "settings.service.form.enableNotification": "Kích hoạt thông báo",
311 "settings.service.form.enableService": "Kích hoạt dịch vụ", 313 "settings.service.form.enableService": "Kích hoạt dịch vụ",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Huy hiệu tin nhắn chÆ°a Ä‘á»c", 315 "settings.service.form.headlineBadges": "Huy hiệu tin nhắn chÆ°a Ä‘á»c",
313 "settings.service.form.headlineDarkReaderSettings": "Cài đặt trình Ä‘á»c tối", 316 "settings.service.form.headlineDarkReaderSettings": "Cài đặt trình Ä‘á»c tối",
314 "settings.service.form.headlineGeneral": "Chung", 317 "settings.service.form.headlineGeneral": "Chung",
diff --git a/src/i18n/locales/zh-HANT.json b/src/i18n/locales/zh-HANT.json
index ec6d9085b..30c81436d 100644
--- a/src/i18n/locales/zh-HANT.json
+++ b/src/i18n/locales/zh-HANT.json
@@ -9,8 +9,8 @@
9 "connectionLostBanner.cta": "é‡æ–°è¼‰å…¥", 9 "connectionLostBanner.cta": "é‡æ–°è¼‰å…¥",
10 "connectionLostBanner.informationLink": "What happened?", 10 "connectionLostBanner.informationLink": "What happened?",
11 "connectionLostBanner.message": "Oh no! Ferdi lost the connection to {name}.", 11 "connectionLostBanner.message": "Oh no! Ferdi lost the connection to {name}.",
12 "feature.basicAuth.signIn": "Sign In", 12 "feature.basicAuth.signIn": "登入",
13 "feature.nightlyBuilds.activate": "Activate", 13 "feature.nightlyBuilds.activate": "å•Ÿå‹•",
14 "feature.nightlyBuilds.info": "Nightly builds are highly experimental versions of Ferdi that may contain unpolished or uncompleted features. These nightly builds are mainly used by developers to test their newly developed features and how they will perform in the final build. If you don't know what you are doing, we suggest not activating nightly builds.", 14 "feature.nightlyBuilds.info": "Nightly builds are highly experimental versions of Ferdi that may contain unpolished or uncompleted features. These nightly builds are mainly used by developers to test their newly developed features and how they will perform in the final build. If you don't know what you are doing, we suggest not activating nightly builds.",
15 "feature.nightlyBuilds.title": "Nightly Builds", 15 "feature.nightlyBuilds.title": "Nightly Builds",
16 "feature.publishDebugInfo.error": "There was an error while trying to publish the debug information. Please try again later or view the console for more information.", 16 "feature.publishDebugInfo.error": "There was an error while trying to publish the debug information. Please try again later or view the console for more information.",
@@ -22,25 +22,25 @@
22 "feature.publishDebugInfo.title": "Publish debug information", 22 "feature.publishDebugInfo.title": "Publish debug information",
23 "feature.quickSwitch.info": "Select a service with TAB, ↑ and ↓. Open a service with ENTER.", 23 "feature.quickSwitch.info": "Select a service with TAB, ↑ and ↓. Open a service with ENTER.",
24 "feature.quickSwitch.search": "æœå°‹...", 24 "feature.quickSwitch.search": "æœå°‹...",
25 "feature.quickSwitch.title": "QuickSwitch", 25 "feature.quickSwitch.title": "快速切æ›",
26 "global.api.unhealthy": "Can't connect to Ferdi online services", 26 "global.api.unhealthy": "Can't connect to Ferdi online services",
27 "global.cancel": "Cancel", 27 "global.cancel": "å–消",
28 "global.edit": "編輯", 28 "global.edit": "編輯",
29 "global.no": "No", 29 "global.no": "å¦",
30 "global.notConnectedToTheInternet": "您未連上網際網路", 30 "global.notConnectedToTheInternet": "您未連上網際網路",
31 "global.ok": "Ok", 31 "global.ok": "好",
32 "global.quit": "Quit", 32 "global.quit": "退出",
33 "global.quitConfirmation": "Do you really want to quit Ferdi?", 33 "global.quitConfirmation": "Do you really want to quit Ferdi?",
34 "global.save": "Save", 34 "global.save": "儲存",
35 "global.settings": "Settings", 35 "global.settings": "設定",
36 "global.spellchecker.useDefault": "使用系統é è¨­å€¼({default})", 36 "global.spellchecker.useDefault": "使用系統é è¨­å€¼({default})",
37 "global.spellchecking.autodetect": "自動檢測語言", 37 "global.spellchecking.autodetect": "自動檢測語言",
38 "global.spellchecking.autodetect.short": "自動", 38 "global.spellchecking.autodetect.short": "自動",
39 "global.spellchecking.language": "拼字檢查語言", 39 "global.spellchecking.language": "拼字檢查語言",
40 "global.submit": "Submit", 40 "global.submit": "é€å‡º",
41 "global.userAgentHelp": "Use 'https://whatmyuseragent.com/' (to discover) or 'https://developers.whatismybrowser.com/useragents/explore/' (to choose) your desired user agent and copy-paste it here.", 41 "global.userAgentHelp": "Use 'https://whatmyuseragent.com/' (to discover) or 'https://developers.whatismybrowser.com/useragents/explore/' (to choose) your desired user agent and copy-paste it here.",
42 "global.userAgentPref": "User Agent", 42 "global.userAgentPref": "User Agent",
43 "global.yes": "Yes", 43 "global.yes": "是",
44 "import.headline": "匯入您的 Ferdi 4 æœå‹™", 44 "import.headline": "匯入您的 Ferdi 4 æœå‹™",
45 "import.notSupportedHeadline": "æ­¤æœå‹™ä¸è¢« Ferdi 5 支æŒ", 45 "import.notSupportedHeadline": "æ­¤æœå‹™ä¸è¢« Ferdi 5 支æŒ",
46 "import.skip.label": "我想手動匯入", 46 "import.skip.label": "我想手動匯入",
@@ -56,7 +56,7 @@
56 "infobox.dismiss": "Dismiss", 56 "infobox.dismiss": "Dismiss",
57 "invite.email.label": "é›»å­éƒµä»¶ä¿¡ç®±", 57 "invite.email.label": "é›»å­éƒµä»¶ä¿¡ç®±",
58 "invite.headline.friends": "邀請三個人", 58 "invite.headline.friends": "邀請三個人",
59 "invite.name.label": "åå­", 59 "invite.name.label": "åå­",
60 "invite.skip.label": "我想晚點進行", 60 "invite.skip.label": "我想晚點進行",
61 "invite.submit.label": "發é€é‚€è«‹", 61 "invite.submit.label": "發é€é‚€è«‹",
62 "invite.successInfo": "å·²æˆåŠŸç™¼é€é‚€è«‹", 62 "invite.successInfo": "å·²æˆåŠŸç™¼é€é‚€è«‹",
@@ -89,20 +89,20 @@
89 "menu.app.hideOthers": "Hide Others", 89 "menu.app.hideOthers": "Hide Others",
90 "menu.app.unhide": "Unhide", 90 "menu.app.unhide": "Unhide",
91 "menu.edit": "編輯", 91 "menu.edit": "編輯",
92 "menu.edit.copy": "Copy", 92 "menu.edit.copy": "複製",
93 "menu.edit.cut": "Cut", 93 "menu.edit.cut": "剪下",
94 "menu.edit.delete": "刪除", 94 "menu.edit.delete": "刪除",
95 "menu.edit.emojiSymbols": "Emoji & Symbols", 95 "menu.edit.emojiSymbols": "Emoji & Symbols",
96 "menu.edit.findInPage": "Find in Page", 96 "menu.edit.findInPage": "在é é¢ä¸­å°‹æ‰¾",
97 "menu.edit.paste": "Paste", 97 "menu.edit.paste": "貼上",
98 "menu.edit.pasteAndMatchStyle": "Paste And Match Style", 98 "menu.edit.pasteAndMatchStyle": "Paste And Match Style",
99 "menu.edit.redo": "Redo", 99 "menu.edit.redo": "Redo",
100 "menu.edit.selectAll": "Select All", 100 "menu.edit.selectAll": "å…¨é¸",
101 "menu.edit.speech": "語音", 101 "menu.edit.speech": "語音",
102 "menu.edit.startDictation": "開始è½å¯«", 102 "menu.edit.startDictation": "開始è½å¯«",
103 "menu.edit.startSpeaking": "開始朗讀", 103 "menu.edit.startSpeaking": "開始朗讀",
104 "menu.edit.stopSpeaking": "åœæ­¢æœ—讀", 104 "menu.edit.stopSpeaking": "åœæ­¢æœ—讀",
105 "menu.edit.undo": "Undo", 105 "menu.edit.undo": "復原",
106 "menu.file": "檔案", 106 "menu.file": "檔案",
107 "menu.help": "Help", 107 "menu.help": "Help",
108 "menu.help.changelog": "更新日誌", 108 "menu.help.changelog": "更新日誌",
@@ -127,20 +127,20 @@
127 "menu.view.forward": "å‰é€²", 127 "menu.view.forward": "å‰é€²",
128 "menu.view.lockFerdi": "Lock Ferdi", 128 "menu.view.lockFerdi": "Lock Ferdi",
129 "menu.view.openQuickSwitch": "Open Quick Switch", 129 "menu.view.openQuickSwitch": "Open Quick Switch",
130 "menu.view.reloadFerdi": "Reload Ferdi", 130 "menu.view.reloadFerdi": "é‡æ–°è¼‰å…¥ Ferdi",
131 "menu.view.reloadService": "é‡æ–°è¼‰å…¥", 131 "menu.view.reloadService": "é‡æ–°è¼‰å…¥",
132 "menu.view.reloadTodos": "Reload ToDos", 132 "menu.view.reloadTodos": "Reload ToDos",
133 "menu.view.resetZoom": "Actual Size", 133 "menu.view.resetZoom": "實際大å°",
134 "menu.view.toggleDarkMode": "Toggle Dark Mode", 134 "menu.view.toggleDarkMode": "Toggle Dark Mode",
135 "menu.view.toggleDevTools": "切æ›é–‹ç™¼äººå“¡å·¥å…·", 135 "menu.view.toggleDevTools": "切æ›é–‹ç™¼äººå“¡å·¥å…·",
136 "menu.view.toggleFullScreen": "Toggle Full Screen", 136 "menu.view.toggleFullScreen": "Toggle Full Screen",
137 "menu.view.toggleServiceDevTools": "切æ›é–‹ç™¼äººå“¡æœå‹™å·¥å…·", 137 "menu.view.toggleServiceDevTools": "切æ›é–‹ç™¼äººå“¡æœå‹™å·¥å…·",
138 "menu.view.toggleTodosDevTools": "Toggle Todos Developer Tools", 138 "menu.view.toggleTodosDevTools": "Toggle Todos Developer Tools",
139 "menu.view.zoomIn": "Zoom In", 139 "menu.view.zoomIn": "放大",
140 "menu.view.zoomOut": "Zoom Out", 140 "menu.view.zoomOut": "縮å°",
141 "menu.window": "Window", 141 "menu.window": "視窗",
142 "menu.window.close": "Close", 142 "menu.window.close": "關閉",
143 "menu.window.minimize": "Minimize", 143 "menu.window.minimize": "最å°åŒ–",
144 "menu.workspaces": "工作å€", 144 "menu.workspaces": "工作å€",
145 "menu.workspaces.addNewWorkspace": "新增工作å€â€¦", 145 "menu.workspaces.addNewWorkspace": "新增工作å€â€¦",
146 "menu.workspaces.closeWorkspaceDrawer": "關閉工作å€", 146 "menu.workspaces.closeWorkspaceDrawer": "關閉工作å€",
@@ -152,13 +152,13 @@
152 "password.link.signup": "建立一個å…費帳戶", 152 "password.link.signup": "建立一個å…費帳戶",
153 "password.noUser": "No user with that email address was found", 153 "password.noUser": "No user with that email address was found",
154 "password.successInfo": "Your new password was sent to your email address", 154 "password.successInfo": "Your new password was sent to your email address",
155 "service.crashHandler.action": "Reload {name}", 155 "service.crashHandler.action": "é‡æ–°è¼‰å…¥ {name}",
156 "service.crashHandler.autoReload": "Trying to automatically restore {name} in {seconds} seconds", 156 "service.crashHandler.autoReload": "Trying to automatically restore {name} in {seconds} seconds",
157 "service.crashHandler.headline": "唉呀 糟糕!", 157 "service.crashHandler.headline": "唉呀 糟糕!",
158 "service.crashHandler.text": "{name} 造æˆäº†å•é¡Œ", 158 "service.crashHandler.text": "{name} 造æˆäº†å•é¡Œ",
159 "service.disabledHandler.action": "啟用 {name}", 159 "service.disabledHandler.action": "啟用 {name}",
160 "service.disabledHandler.headline": "{name} å·²åœç”¨", 160 "service.disabledHandler.headline": "{name} å·²åœç”¨",
161 "service.errorHandler.action": "Reload {name}", 161 "service.errorHandler.action": "é‡æ–°è¼‰å…¥ {name}",
162 "service.errorHandler.editAction": "編輯 {name}", 162 "service.errorHandler.editAction": "編輯 {name}",
163 "service.errorHandler.headline": "唉呀 糟糕!", 163 "service.errorHandler.headline": "唉呀 糟糕!",
164 "service.errorHandler.message": "錯誤", 164 "service.errorHandler.message": "錯誤",
@@ -169,7 +169,7 @@
169 "services.serverInfo": "Optionally, you can change your Ferdi server by clicking the cog in the bottom left corner. If you are switching over (from one of the hosted servers) to using Ferdi without an account, please be informed that you can export your data from that server and subsequently import it using the Help menu to resurrect all your workspaces and configured services!", 169 "services.serverInfo": "Optionally, you can change your Ferdi server by clicking the cog in the bottom left corner. If you are switching over (from one of the hosted servers) to using Ferdi without an account, please be informed that you can export your data from that server and subsequently import it using the Help menu to resurrect all your workspaces and configured services!",
170 "services.serverless": "Use Ferdi without an Account", 170 "services.serverless": "Use Ferdi without an Account",
171 "services.welcome": "歡迎使用 Ferdi", 171 "services.welcome": "歡迎使用 Ferdi",
172 "settings.account.account.editButton": "Edit account", 172 "settings.account.account.editButton": "更改帳戶資訊",
173 "settings.account.accountUnavailable": "帳號無法使用", 173 "settings.account.accountUnavailable": "帳號無法使用",
174 "settings.account.accountUnavailableInfo": "You are using Ferdi without an account. If you want to use Ferdi with an account and keep your services synchronized across installations, please select a server in the Settings tab then login.", 174 "settings.account.accountUnavailableInfo": "You are using Ferdi without an account. If you want to use Ferdi with an account and keep your services synchronized across installations, please select a server in the Settings tab then login.",
175 "settings.account.buttonSave": "更新帳戶資訊", 175 "settings.account.buttonSave": "更新帳戶資訊",
@@ -177,10 +177,10 @@
177 "settings.account.deleteEmailSent": "You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!", 177 "settings.account.deleteEmailSent": "You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!",
178 "settings.account.deleteInfo": "If you don't need your Ferdi account any longer, you can delete your account and all related data here.", 178 "settings.account.deleteInfo": "If you don't need your Ferdi account any longer, you can delete your account and all related data here.",
179 "settings.account.headline": "帳戶", 179 "settings.account.headline": "帳戶",
180 "settings.account.headlineAccount": "Account information", 180 "settings.account.headlineAccount": "帳戶資訊",
181 "settings.account.headlineDangerZone": "Danger Zone", 181 "settings.account.headlineDangerZone": "Danger Zone",
182 "settings.account.headlineInvoices": "Invoices", 182 "settings.account.headlineInvoices": "Invoices",
183 "settings.account.headlinePassword": "Change password", 183 "settings.account.headlinePassword": "更改密碼",
184 "settings.account.headlineProfile": "更新帳戶資訊", 184 "settings.account.headlineProfile": "更新帳戶資訊",
185 "settings.account.successInfo": "您的更改已經儲存", 185 "settings.account.successInfo": "您的更改已經儲存",
186 "settings.account.tryReloadServices": "å†è©¦ä¸€æ¬¡", 186 "settings.account.tryReloadServices": "å†è©¦ä¸€æ¬¡",
@@ -195,14 +195,14 @@
195 "settings.app.buttonSearchForUpdate": "檢查更新", 195 "settings.app.buttonSearchForUpdate": "檢查更新",
196 "settings.app.cacheInfo": "Ferdi cache is currently using {size} of disk space.", 196 "settings.app.cacheInfo": "Ferdi cache is currently using {size} of disk space.",
197 "settings.app.cacheNotCleared": "Couldn't clear all cache", 197 "settings.app.cacheNotCleared": "Couldn't clear all cache",
198 "settings.app.closeSettings": "Close settings", 198 "settings.app.closeSettings": "關閉設定",
199 "settings.app.currentVersion": "當å‰ç‰ˆæœ¬ï¼š", 199 "settings.app.currentVersion": "當å‰ç‰ˆæœ¬ï¼š",
200 "settings.app.form.accentColor": "強調é¡è‰²", 200 "settings.app.form.accentColor": "強調é¡è‰²",
201 "settings.app.form.adaptableDarkMode": "Synchronize dark mode with my OS's dark mode setting", 201 "settings.app.form.adaptableDarkMode": "Synchronize dark mode with my OS's dark mode setting",
202 "settings.app.form.alwaysShowWorkspaces": "Always show workspace drawer", 202 "settings.app.form.alwaysShowWorkspaces": "Always show workspace drawer",
203 "settings.app.form.autoLaunchInBackground": "背景啟動", 203 "settings.app.form.autoLaunchInBackground": "背景啟動",
204 "settings.app.form.autoLaunchOnStart": "開機時啟動", 204 "settings.app.form.autoLaunchOnStart": "開機時啟動",
205 "settings.app.form.automaticUpdates": "Enable updates", 205 "settings.app.form.automaticUpdates": "啟用更新",
206 "settings.app.form.beta": "包å«é–‹ç™¼ä¸­ç‰ˆæœ¬", 206 "settings.app.form.beta": "包å«é–‹ç™¼ä¸­ç‰ˆæœ¬",
207 "settings.app.form.clipboardNotifications": "Don't show notifications for clipboard events", 207 "settings.app.form.clipboardNotifications": "Don't show notifications for clipboard events",
208 "settings.app.form.closeToSystemTray": "Close Ferdi to system tray", 208 "settings.app.form.closeToSystemTray": "Close Ferdi to system tray",
@@ -210,7 +210,9 @@
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "Custom Todo Server",
211 "settings.app.form.darkMode": "啟用夜間模å¼", 211 "settings.app.form.darkMode": "啟用夜間模å¼",
212 "settings.app.form.enableGPUAcceleration": "開啟顯示å¡ï¼ˆGPU)加速", 212 "settings.app.form.enableGPUAcceleration": "開啟顯示å¡ï¼ˆGPU)加速",
213 "settings.app.form.enableGlobalHideShortcut": "Enable Global shortcut to hide Ferdi",
213 "settings.app.form.enableLock": "Enable Password Lock", 214 "settings.app.form.enableLock": "Enable Password Lock",
215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 216 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar",
215 "settings.app.form.enableSpellchecking": "Enable spell checking", 217 "settings.app.form.enableSpellchecking": "Enable spell checking",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 218 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray",
@@ -233,9 +235,9 @@
233 "settings.app.form.scheduledDNDEnabled": "Enable scheduled Do-not-Disturb", 235 "settings.app.form.scheduledDNDEnabled": "Enable scheduled Do-not-Disturb",
234 "settings.app.form.scheduledDNDEnd": "至", 236 "settings.app.form.scheduledDNDEnd": "至",
235 "settings.app.form.scheduledDNDStart": "從", 237 "settings.app.form.scheduledDNDStart": "從",
236 "settings.app.form.searchEngine": "Search engine", 238 "settings.app.form.searchEngine": "æœå°‹å¼•æ“Ž",
237 "settings.app.form.sentry": "Send telemetry data", 239 "settings.app.form.sentry": "Send telemetry data",
238 "settings.app.form.serviceRibbonWidth": "Sidebar width", 240 "settings.app.form.serviceRibbonWidth": "å´é‚Šæ¬„寬度",
239 "settings.app.form.showDisabledServices": "Display disabled services tabs", 241 "settings.app.form.showDisabledServices": "Display disabled services tabs",
240 "settings.app.form.showDragArea": "Show draggable area on window", 242 "settings.app.form.showDragArea": "Show draggable area on window",
241 "settings.app.form.showMessagesBadgesWhenMuted": "Show unread message badge when notifications are disabled", 243 "settings.app.form.showMessagesBadgesWhenMuted": "Show unread message badge when notifications are disabled",
@@ -249,7 +251,7 @@
249 "settings.app.headlineAppearance": "外觀", 251 "settings.app.headlineAppearance": "外觀",
250 "settings.app.headlineGeneral": "一般", 252 "settings.app.headlineGeneral": "一般",
251 "settings.app.headlineLanguage": "語言", 253 "settings.app.headlineLanguage": "語言",
252 "settings.app.headlinePrivacy": "Privacy", 254 "settings.app.headlinePrivacy": "éš±ç§",
253 "settings.app.headlineUpdates": "æ›´æ–°", 255 "settings.app.headlineUpdates": "æ›´æ–°",
254 "settings.app.hibernateInfo": "By default, Ferdi will keep all your services open and loaded in the background so they are ready when you want to use them. Service Hibernation will unload your services after a specified amount. This is useful to save RAM or keeping services from slowing down your computer.", 256 "settings.app.hibernateInfo": "By default, Ferdi will keep all your services open and loaded in the background so they are ready when you want to use them. Service Hibernation will unload your services after a specified amount. This is useful to save RAM or keeping services from slowing down your computer.",
255 "settings.app.inactivityLockInfo": "Minutes of inactivity, after which Ferdi should automatically lock. Use 0 to disable", 257 "settings.app.inactivityLockInfo": "Minutes of inactivity, after which Ferdi should automatically lock. Use 0 to disable",
@@ -262,7 +264,7 @@
262 "settings.app.scheduledDNDTimeInfo": "Times in 24-Hour-Format. End time can be before start time (e.g. start 17:00, end 09:00) to enable Do-not-Disturb overnight.", 264 "settings.app.scheduledDNDTimeInfo": "Times in 24-Hour-Format. End time can be before start time (e.g. start 17:00, end 09:00) to enable Do-not-Disturb overnight.",
263 "settings.app.sentryInfo": "Sending telemetry data allows us to find errors in Ferdi - we will not send any personal information like your message data!", 265 "settings.app.sentryInfo": "Sending telemetry data allows us to find errors in Ferdi - we will not send any personal information like your message data!",
264 "settings.app.spellCheckerLanguageInfo": "Ferdi uses your Mac's build-in spellchecker to check for typos. If you want to change the languages the spellchecker checks for, you can do so in your Mac's System Preferences.", 266 "settings.app.spellCheckerLanguageInfo": "Ferdi uses your Mac's build-in spellchecker to check for typos. If you want to change the languages the spellchecker checks for, you can do so in your Mac's System Preferences.",
265 "settings.app.subheadlineCache": "Cache", 267 "settings.app.subheadlineCache": "å¿«å–",
266 "settings.app.subheadlineFerdiProfile": "Ferdi Profile", 268 "settings.app.subheadlineFerdiProfile": "Ferdi Profile",
267 "settings.app.todoServerInfo": "This server will be used for the \"Ferdi Todo\" feature.", 269 "settings.app.todoServerInfo": "This server will be used for the \"Ferdi Todo\" feature.",
268 "settings.app.translationHelp": "Help us to translate Ferdi into your language.", 270 "settings.app.translationHelp": "Help us to translate Ferdi into your language.",
@@ -270,14 +272,14 @@
270 "settings.app.updateStatusAvailable": "有å¯ç”¨æ›´æ–°ï¼Œä¸‹è¼‰ä¸­...", 272 "settings.app.updateStatusAvailable": "有å¯ç”¨æ›´æ–°ï¼Œä¸‹è¼‰ä¸­...",
271 "settings.app.updateStatusSearching": "檢查更新中...", 273 "settings.app.updateStatusSearching": "檢查更新中...",
272 "settings.app.updateStatusUpToDate": "已經是最新版本了", 274 "settings.app.updateStatusUpToDate": "已經是最新版本了",
273 "settings.invite.headline": "Invite Friends", 275 "settings.invite.headline": "邀請好å‹",
274 "settings.navigation.account": "帳戶", 276 "settings.navigation.account": "帳戶",
275 "settings.navigation.availableServices": "å¯ç”¨æœå‹™", 277 "settings.navigation.availableServices": "å¯ç”¨æœå‹™",
276 "settings.navigation.logout": "登出", 278 "settings.navigation.logout": "登出",
277 "settings.navigation.supportFerdi": "關於 Ferdi", 279 "settings.navigation.supportFerdi": "關於 Ferdi",
278 "settings.navigation.team": "管ç†åœ˜éšŠ", 280 "settings.navigation.team": "管ç†åœ˜éšŠ",
279 "settings.navigation.yourServices": "您的æœå‹™", 281 "settings.navigation.yourServices": "您的æœå‹™",
280 "settings.navigation.yourWorkspaces": "Your workspaces", 282 "settings.navigation.yourWorkspaces": "您的工作å€",
281 "settings.recipes.all": "所有æœå‹™", 283 "settings.recipes.all": "所有æœå‹™",
282 "settings.recipes.custom": "Custom Services", 284 "settings.recipes.custom": "Custom Services",
283 "settings.recipes.customService.headline.communityRecipes": "Community 3rd Party Recipes", 285 "settings.recipes.customService.headline.communityRecipes": "Community 3rd Party Recipes",
@@ -285,12 +287,12 @@
285 "settings.recipes.customService.headline.devRecipes": "Your Development Service Recipes", 287 "settings.recipes.customService.headline.devRecipes": "Your Development Service Recipes",
286 "settings.recipes.customService.intro": "To add a custom service, copy the service recipe to:", 288 "settings.recipes.customService.intro": "To add a custom service, copy the service recipe to:",
287 "settings.recipes.customService.openDevDocs": "Developer Documentation", 289 "settings.recipes.customService.openDevDocs": "Developer Documentation",
288 "settings.recipes.customService.openFolder": "Open folder", 290 "settings.recipes.customService.openFolder": "開啟資料夾",
289 "settings.recipes.headline": "å¯ç”¨æœå‹™", 291 "settings.recipes.headline": "å¯ç”¨æœå‹™",
290 "settings.recipes.missingService": "Missing a service?", 292 "settings.recipes.missingService": "Missing a service?",
291 "settings.recipes.nothingFound": "Sorry, but no service matched your search term - but you can still probably add it using the \"Custom Website\" option. Please note that the website might show more services that have been added to Ferdi since the version that you are currently on. To get those new services, please consider upgrading to a newer version of Ferdi.", 293 "settings.recipes.nothingFound": "Sorry, but no service matched your search term - but you can still probably add it using the \"Custom Website\" option. Please note that the website might show more services that have been added to Ferdi since the version that you are currently on. To get those new services, please consider upgrading to a newer version of Ferdi.",
292 "settings.recipes.servicesSuccessfulAddedInfo": "新增æœå‹™æˆåŠŸ", 294 "settings.recipes.servicesSuccessfulAddedInfo": "新增æœå‹™æˆåŠŸ",
293 "settings.searchService": "Search service", 295 "settings.searchService": "æœå°‹æœå‹™",
294 "settings.service.error.goBack": "返回", 296 "settings.service.error.goBack": "返回",
295 "settings.service.error.headline": "錯誤", 297 "settings.service.error.headline": "錯誤",
296 "settings.service.error.message": "無法載入æœå‹™å…ƒä»¶", 298 "settings.service.error.message": "無法載入æœå‹™å…ƒä»¶",
@@ -301,14 +303,15 @@
301 "settings.service.form.darkReaderBrightness": "Dark Reader Brightness", 303 "settings.service.form.darkReaderBrightness": "Dark Reader Brightness",
302 "settings.service.form.darkReaderContrast": "Dark Reader Contrast", 304 "settings.service.form.darkReaderContrast": "Dark Reader Contrast",
303 "settings.service.form.darkReaderSepia": "Dark Reader Sepia", 305 "settings.service.form.darkReaderSepia": "Dark Reader Sepia",
304 "settings.service.form.deleteButton": "Delete service", 306 "settings.service.form.deleteButton": "刪除æœå‹™",
305 "settings.service.form.editServiceHeadline": "編輯 {name}", 307 "settings.service.form.editServiceHeadline": "編輯 {name}",
306 "settings.service.form.enableAudio": "啟用音效", 308 "settings.service.form.enableAudio": "啟用音效",
307 "settings.service.form.enableBadge": "Show unread message badges", 309 "settings.service.form.enableBadge": "Show unread message badges",
308 "settings.service.form.enableDarkMode": "啟用夜間模å¼", 310 "settings.service.form.enableDarkMode": "啟用夜間模å¼",
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "啓用休眠",
310 "settings.service.form.enableNotification": "啟用通知", 312 "settings.service.form.enableNotification": "啟用通知",
311 "settings.service.form.enableService": "啟用æœå‹™", 313 "settings.service.form.enableService": "啟用æœå‹™",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Unread message badges", 315 "settings.service.form.headlineBadges": "Unread message badges",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings",
314 "settings.service.form.headlineGeneral": "一般", 317 "settings.service.form.headlineGeneral": "一般",
@@ -320,19 +323,19 @@
320 "settings.service.form.indirectMessages": "é‡å°å…¨éƒ¨è¨Šæ¯é¡¯ç¤ºé€šçŸ¥", 323 "settings.service.form.indirectMessages": "é‡å°å…¨éƒ¨è¨Šæ¯é¡¯ç¤ºé€šçŸ¥",
321 "settings.service.form.isHibernatedEnabledInfo": "When enabled, a service will be shut down after a period of time to save system resources.", 324 "settings.service.form.isHibernatedEnabledInfo": "When enabled, a service will be shut down after a period of time to save system resources.",
322 "settings.service.form.isMutedInfo": "When disabled, all notification sounds and audio playback are muted", 325 "settings.service.form.isMutedInfo": "When disabled, all notification sounds and audio playback are muted",
323 "settings.service.form.name": "åå­", 326 "settings.service.form.name": "åå­",
324 "settings.service.form.onlyShowFavoritesInUnreadCount": "Only show Favorites in unread count", 327 "settings.service.form.onlyShowFavoritesInUnreadCount": "Only show Favorites in unread count",
325 "settings.service.form.openDarkmodeCss": "Open darkmode.css", 328 "settings.service.form.openDarkmodeCss": "é–‹å•Ÿ darkmode.css",
326 "settings.service.form.openUserCss": "Open user.css", 329 "settings.service.form.openUserCss": "é–‹å•Ÿ user.css",
327 "settings.service.form.openUserJs": "Open user.js", 330 "settings.service.form.openUserJs": "é–‹å•Ÿ user.js",
328 "settings.service.form.proxy.headline": "HTTP/HTTPS Proxy Settings", 331 "settings.service.form.proxy.headline": "HTTP/HTTPS Proxy Settings",
329 "settings.service.form.proxy.host": "Proxy Host/IP", 332 "settings.service.form.proxy.host": "Proxy Host/IP",
330 "settings.service.form.proxy.info": "Proxy settings will not be synchronized with the Ferdi servers.", 333 "settings.service.form.proxy.info": "Proxy settings will not be synchronized with the Ferdi servers.",
331 "settings.service.form.proxy.isEnabled": "使用代ç†", 334 "settings.service.form.proxy.isEnabled": "使用代ç†",
332 "settings.service.form.proxy.password": "Password (optional)", 335 "settings.service.form.proxy.password": "密碼 (é¸å¡«)",
333 "settings.service.form.proxy.port": "連接埠", 336 "settings.service.form.proxy.port": "連接埠",
334 "settings.service.form.proxy.restartInfo": "Please restart Ferdi after changing proxy Settings.", 337 "settings.service.form.proxy.restartInfo": "Please restart Ferdi after changing proxy Settings.",
335 "settings.service.form.proxy.user": "User (optional)", 338 "settings.service.form.proxy.user": "使用者 (é¸å¡«)",
336 "settings.service.form.recipeFileInfo": "Your user files will be inserted into the webpage so you can customize services in any way you like. User files are only stored locally and are not transferred to other computers using the same account.", 339 "settings.service.form.recipeFileInfo": "Your user files will be inserted into the webpage so you can customize services in any way you like. User files are only stored locally and are not transferred to other computers using the same account.",
337 "settings.service.form.saveButton": "儲存", 340 "settings.service.form.saveButton": "儲存",
338 "settings.service.form.tabHosted": "Hosted", 341 "settings.service.form.tabHosted": "Hosted",
@@ -355,14 +358,14 @@
355 "settings.supportFerdi.headline": "關於 Ferdi", 358 "settings.supportFerdi.headline": "關於 Ferdi",
356 "settings.supportFerdi.openSurvey": "Open survey", 359 "settings.supportFerdi.openSurvey": "Open survey",
357 "settings.supportFerdi.textDonation": "If you feel like supporting Ferdi development with a donation, you can do so on both,", 360 "settings.supportFerdi.textDonation": "If you feel like supporting Ferdi development with a donation, you can do so on both,",
358 "settings.supportFerdi.textDonationAnd": "and", 361 "settings.supportFerdi.textDonationAnd": "和",
359 "settings.supportFerdi.textExpenses": "While volunteers do most of the work, we still need to pay for servers and certificates. As a community, we are fully transparent on funds we collect and spend - see our", 362 "settings.supportFerdi.textExpenses": "While volunteers do most of the work, we still need to pay for servers and certificates. As a community, we are fully transparent on funds we collect and spend - see our",
360 "settings.supportFerdi.textGitHubSponsors": "GitHub Sponsors", 363 "settings.supportFerdi.textGitHubSponsors": "GitHub Sponsors",
361 "settings.supportFerdi.textListContributors": "Full list of contributors", 364 "settings.supportFerdi.textListContributors": "Full list of contributors",
362 "settings.supportFerdi.textListContributorsHere": "here", 365 "settings.supportFerdi.textListContributorsHere": "這裡",
363 "settings.supportFerdi.textOpenCollective": "Open Collective", 366 "settings.supportFerdi.textOpenCollective": "Open Collective",
364 "settings.supportFerdi.textSupportWelcome": "Support is always welcome. You can find a list of the help we need", 367 "settings.supportFerdi.textSupportWelcome": "Support is always welcome. You can find a list of the help we need",
365 "settings.supportFerdi.textSupportWelcomeHere": "here", 368 "settings.supportFerdi.textSupportWelcomeHere": "這裡",
366 "settings.supportFerdi.textVolunteers": "The development of Ferdi is done by volunteers. People who use Ferdi like you. They maintain, fix, and improve Ferdi in their spare time.", 369 "settings.supportFerdi.textVolunteers": "The development of Ferdi is done by volunteers. People who use Ferdi like you. They maintain, fix, and improve Ferdi in their spare time.",
367 "settings.supportFerdi.title": "Do you like Ferdi?", 370 "settings.supportFerdi.title": "Do you like Ferdi?",
368 "settings.team.contentHeadline": "Franz Team Management", 371 "settings.team.contentHeadline": "Franz Team Management",
@@ -378,20 +381,20 @@
378 "settings.user.form.accountType.non-profit": "éžç‡Ÿåˆ©", 381 "settings.user.form.accountType.non-profit": "éžç‡Ÿåˆ©",
379 "settings.user.form.currentPassword": "舊密碼", 382 "settings.user.form.currentPassword": "舊密碼",
380 "settings.user.form.email": "é›»å­éƒµä»¶ä¿¡ç®±", 383 "settings.user.form.email": "é›»å­éƒµä»¶ä¿¡ç®±",
381 "settings.user.form.firstname": "First Name", 384 "settings.user.form.firstname": "åå­—",
382 "settings.user.form.lastname": "Last Name", 385 "settings.user.form.lastname": "姓æ°",
383 "settings.user.form.newPassword": "新密碼", 386 "settings.user.form.newPassword": "新密碼",
384 "settings.workspace.add.form.name": "åå­", 387 "settings.workspace.add.form.name": "åå­",
385 "settings.workspace.add.form.submitButton": "建立工作å€", 388 "settings.workspace.add.form.submitButton": "建立工作å€",
386 "settings.workspace.form.buttonDelete": "刪除工作å€", 389 "settings.workspace.form.buttonDelete": "刪除工作å€",
387 "settings.workspace.form.buttonSave": "儲存工作å€åŸŸ", 390 "settings.workspace.form.buttonSave": "儲存工作å€åŸŸ",
388 "settings.workspace.form.keepLoaded": "Keep this workspace loaded*", 391 "settings.workspace.form.keepLoaded": "Keep this workspace loaded*",
389 "settings.workspace.form.keepLoadedInfo": "*This option will be overwritten by the global \"Keep all workspaces loaded\" option.", 392 "settings.workspace.form.keepLoadedInfo": "*This option will be overwritten by the global \"Keep all workspaces loaded\" option.",
390 "settings.workspace.form.name": "åå­", 393 "settings.workspace.form.name": "åå­",
391 "settings.workspace.form.servicesInWorkspaceHeadline": "Services in this Workspace", 394 "settings.workspace.form.servicesInWorkspaceHeadline": "Services in this Workspace",
392 "settings.workspace.form.yourWorkspaces": "Your workspaces", 395 "settings.workspace.form.yourWorkspaces": "您的工作å€",
393 "settings.workspaces.deletedInfo": "Workspace has been deleted", 396 "settings.workspaces.deletedInfo": "Workspace has been deleted",
394 "settings.workspaces.headline": "Your workspaces", 397 "settings.workspaces.headline": "您的工作å€",
395 "settings.workspaces.noWorkspacesAdded": "You haven't created any workspaces yet.", 398 "settings.workspaces.noWorkspacesAdded": "You haven't created any workspaces yet.",
396 "settings.workspaces.tryReloadWorkspaces": "å†è©¦ä¸€æ¬¡", 399 "settings.workspaces.tryReloadWorkspaces": "å†è©¦ä¸€æ¬¡",
397 "settings.workspaces.updatedInfo": "您的更改已經儲存", 400 "settings.workspaces.updatedInfo": "您的更改已經儲存",
@@ -400,7 +403,7 @@
400 "settings.workspaces.workspacesRequestFailed": "Could not load your workspaces", 403 "settings.workspaces.workspacesRequestFailed": "Could not load your workspaces",
401 "setupAssistant.headline": "Let's get started", 404 "setupAssistant.headline": "Let's get started",
402 "setupAssistant.subheadline": "Choose from our most used services and get back on top of your messaging now.", 405 "setupAssistant.subheadline": "Choose from our most used services and get back on top of your messaging now.",
403 "setupAssistant.submit.label": "Let's go", 406 "setupAssistant.submit.label": "讓我們開始å§",
404 "sidebar.addNewService": "添加新æœå‹™", 407 "sidebar.addNewService": "添加新æœå‹™",
405 "sidebar.closeTodosDrawer": "Close Ferdi Todos", 408 "sidebar.closeTodosDrawer": "Close Ferdi Todos",
406 "sidebar.closeWorkspaceDrawer": "關閉工作å€", 409 "sidebar.closeWorkspaceDrawer": "關閉工作å€",
@@ -411,9 +414,9 @@
411 "sidebar.unmuteApp": "啟用通知", 414 "sidebar.unmuteApp": "啟用通知",
412 "signup.email.label": "é›»å­éƒµä»¶ä¿¡ç®±", 415 "signup.email.label": "é›»å­éƒµä»¶ä¿¡ç®±",
413 "signup.emailDuplicate": "此電å­éƒµä»¶ä¿¡ç®±å·²è¢«è¨»å†Š", 416 "signup.emailDuplicate": "此電å­éƒµä»¶ä¿¡ç®±å·²è¢«è¨»å†Š",
414 "signup.firstname.label": "First Name", 417 "signup.firstname.label": "åå­—",
415 "signup.headline": "註冊", 418 "signup.headline": "註冊",
416 "signup.lastname.label": "Last Name", 419 "signup.lastname.label": "姓æ°",
417 "signup.legal.info": "在建立帳戶åŒæ™‚,您åŒæ„:", 420 "signup.legal.info": "在建立帳戶åŒæ™‚,您åŒæ„:",
418 "signup.legal.privacy": "éš±ç§è²æ˜Ž", 421 "signup.legal.privacy": "éš±ç§è²æ˜Ž",
419 "signup.legal.terms": "æœå‹™æ¢æ¬¾", 422 "signup.legal.terms": "æœå‹™æ¢æ¬¾",
@@ -421,13 +424,13 @@
421 "signup.password.label": "密碼", 424 "signup.password.label": "密碼",
422 "signup.submit.label": "建立帳戶", 425 "signup.submit.label": "建立帳戶",
423 "tabs.item.confirmDeleteService": "Do you really want to delete the {serviceName} service?", 426 "tabs.item.confirmDeleteService": "Do you really want to delete the {serviceName} service?",
424 "tabs.item.deleteService": "Delete service", 427 "tabs.item.deleteService": "刪除æœå‹™",
425 "tabs.item.disableAudio": "åœç”¨éŸ³æ•ˆ", 428 "tabs.item.disableAudio": "åœç”¨éŸ³æ•ˆ",
426 "tabs.item.disableDarkMode": "Disable Dark mode", 429 "tabs.item.disableDarkMode": "åœç”¨æ·±è‰²æ¨¡å¼",
427 "tabs.item.disableNotifications": "åœç”¨é€šçŸ¥", 430 "tabs.item.disableNotifications": "åœç”¨é€šçŸ¥",
428 "tabs.item.disableService": "Disable service", 431 "tabs.item.disableService": "åœç”¨æœå‹™",
429 "tabs.item.enableAudio": "啟用音效", 432 "tabs.item.enableAudio": "啟用音效",
430 "tabs.item.enableDarkMode": "Enable Dark mode", 433 "tabs.item.enableDarkMode": "啟用深色模å¼",
431 "tabs.item.enableNotification": "啟用通知", 434 "tabs.item.enableNotification": "啟用通知",
432 "tabs.item.enableService": "啟用æœå‹™", 435 "tabs.item.enableService": "啟用æœå‹™",
433 "tabs.item.hibernateService": "Hibernate service", 436 "tabs.item.hibernateService": "Hibernate service",
diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json
index 648d75a6d..836a067f5 100644
--- a/src/i18n/locales/zh.json
+++ b/src/i18n/locales/zh.json
@@ -1,15 +1,15 @@
1{ 1{
2 "app.errorHandler.action": "é‡æ–°åŠ è½½", 2 "app.errorHandler.action": "é‡æ–°åŠ è½½",
3 "app.errorHandler.headline": "Something went wrong.", 3 "app.errorHandler.headline": "å‘生未知错误。",
4 "changeserver.customServerLabel": "Custom server", 4 "changeserver.customServerLabel": "自定义æœåŠ¡å™¨",
5 "changeserver.headline": "Change server", 5 "changeserver.headline": "更改æœåŠ¡å™¨",
6 "changeserver.label": "æœåŠ¡å™¨ï¼š", 6 "changeserver.label": "æœåŠ¡å™¨ï¼š",
7 "changeserver.urlError": "输入有效的URL", 7 "changeserver.urlError": "输入有效的URL",
8 "changeserver.warning": "Ferdiæ供的é¢å¤–设置将ä¸ä¼šè¢«ä¿å­˜", 8 "changeserver.warning": "Ferdiæ供的é¢å¤–设置将ä¸ä¼šè¢«ä¿å­˜",
9 "connectionLostBanner.cta": "é‡æ–°åŠ è½½æœåŠ¡", 9 "connectionLostBanner.cta": "é‡æ–°åŠ è½½æœåŠ¡",
10 "connectionLostBanner.informationLink": "å‘生了什么?", 10 "connectionLostBanner.informationLink": "å‘生了什么?",
11 "connectionLostBanner.message": "哦ä¸ï¼Ferdi失去了与 {name} 的连接。", 11 "connectionLostBanner.message": "哦ä¸ï¼Ferdi失去了与 {name} 的连接。",
12 "feature.basicAuth.signIn": "Sign In", 12 "feature.basicAuth.signIn": "登录",
13 "feature.nightlyBuilds.activate": "激活", 13 "feature.nightlyBuilds.activate": "激活",
14 "feature.nightlyBuilds.info": "æ¯å¤œç‰ˆ(Nightly builds) 是Ferdi的实验性版本,å¯èƒ½åŒ…å«æœªå®Œå–„或未完æˆçš„功能。这些æ¯å¤œç‰ˆä¸»è¦ç”±å¼€å‘人员æ¥æµ‹è¯•ä»–们新开å‘的功能åŠå®ƒä»¬åœ¨æœ€ç»ˆç‰ˆæœ¬çš„的表现。如果您ä¸çŸ¥é“自己在åšä»€ä¹ˆï¼Œæˆ‘们建议您ä¸è¦æ¿€æ´»æ¯å¤œç‰ˆã€‚", 14 "feature.nightlyBuilds.info": "æ¯å¤œç‰ˆ(Nightly builds) 是Ferdi的实验性版本,å¯èƒ½åŒ…å«æœªå®Œå–„或未完æˆçš„功能。这些æ¯å¤œç‰ˆä¸»è¦ç”±å¼€å‘人员æ¥æµ‹è¯•ä»–们新开å‘的功能åŠå®ƒä»¬åœ¨æœ€ç»ˆç‰ˆæœ¬çš„的表现。如果您ä¸çŸ¥é“自己在åšä»€ä¹ˆï¼Œæˆ‘们建议您ä¸è¦æ¿€æ´»æ¯å¤œç‰ˆã€‚",
15 "feature.nightlyBuilds.title": "æ¯å¤œç‰ˆ", 15 "feature.nightlyBuilds.title": "æ¯å¤œç‰ˆ",
@@ -18,35 +18,35 @@
18 "feature.publishDebugInfo.privacy": "éšç§æƒæ”¿ç­–", 18 "feature.publishDebugInfo.privacy": "éšç§æƒæ”¿ç­–",
19 "feature.publishDebugInfo.publish": "接å—并å‘布", 19 "feature.publishDebugInfo.publish": "接å—并å‘布",
20 "feature.publishDebugInfo.published": "您的调试日志已ç»å‘布,现在å¯ç”¨äºŽ", 20 "feature.publishDebugInfo.published": "您的调试日志已ç»å‘布,现在å¯ç”¨äºŽ",
21 "feature.publishDebugInfo.terms": "Terms of service", 21 "feature.publishDebugInfo.terms": "æœåŠ¡æ¡æ¬¾",
22 "feature.publishDebugInfo.title": "å‘布调试信æ¯", 22 "feature.publishDebugInfo.title": "å‘布调试信æ¯",
23 "feature.quickSwitch.info": "使用 TAB ,↑ å’Œ ↓ 选择æœåŠ¡ã€‚使用回车键(ENTER)打开æœåŠ¡", 23 "feature.quickSwitch.info": "使用 TAB ,↑ å’Œ ↓ 选择æœåŠ¡ã€‚使用回车键(ENTER)打开æœåŠ¡",
24 "feature.quickSwitch.search": "æœç´¢...", 24 "feature.quickSwitch.search": "æœç´¢...",
25 "feature.quickSwitch.title": "快速切æ¢", 25 "feature.quickSwitch.title": "快速切æ¢",
26 "global.api.unhealthy": "Can't connect to Ferdi online services", 26 "global.api.unhealthy": "无法链接到 Ferdi 在线æœåŠ¡",
27 "global.cancel": "Cancel", 27 "global.cancel": "å–消",
28 "global.edit": "编辑", 28 "global.edit": "编辑",
29 "global.no": "No", 29 "global.no": "å¦",
30 "global.notConnectedToTheInternet": "您没有连接到互è”网。", 30 "global.notConnectedToTheInternet": "您没有连接到互è”网。",
31 "global.ok": "Ok", 31 "global.ok": "确定",
32 "global.quit": "Quit", 32 "global.quit": "退出",
33 "global.quitConfirmation": "Do you really want to quit Ferdi?", 33 "global.quitConfirmation": "确定è¦é€€å‡ºå—?",
34 "global.save": "Save", 34 "global.save": "ä¿å­˜",
35 "global.settings": "Settings", 35 "global.settings": "设置",
36 "global.spellchecker.useDefault": "使用系统默认值 ({default})", 36 "global.spellchecker.useDefault": "使用系统默认值 ({default})",
37 "global.spellchecking.autodetect": "自动检测语言", 37 "global.spellchecking.autodetect": "自动检测语言",
38 "global.spellchecking.autodetect.short": "自动", 38 "global.spellchecking.autodetect.short": "自动",
39 "global.spellchecking.language": "拼写检查语言", 39 "global.spellchecking.language": "拼写检查语言",
40 "global.submit": "Submit", 40 "global.submit": "æ交",
41 "global.userAgentHelp": "使用 'https://whatmyuseragent.com/' (以å‘现)或 'https://developers.whatismybrowser.com/useragents/explore/' (以选择) 你所需è¦çš„用户代ç†å¹¶å¤åˆ¶ç²˜è´´åˆ°è¿™é‡Œã€‚", 41 "global.userAgentHelp": "使用 'https://whatmyuseragent.com/' (以å‘现)或 'https://developers.whatismybrowser.com/useragents/explore/' (以选择) 你所需è¦çš„用户代ç†å¹¶å¤åˆ¶ç²˜è´´åˆ°è¿™é‡Œã€‚",
42 "global.userAgentPref": "æµè§ˆå™¨æ ‡è¯†ï¼ˆç”¨æˆ·ä»£ç†ï¼‰", 42 "global.userAgentPref": "æµè§ˆå™¨æ ‡è¯†ï¼ˆç”¨æˆ·ä»£ç†ï¼‰",
43 "global.yes": "Yes", 43 "global.yes": "是",
44 "import.headline": "导入你的 Ferdi 4 æœåŠ¡", 44 "import.headline": "导入你的 Ferdi 4 æœåŠ¡",
45 "import.notSupportedHeadline": "Ferdi 5尚未支æŒçš„æœåŠ¡", 45 "import.notSupportedHeadline": "Ferdi 5尚未支æŒçš„æœåŠ¡",
46 "import.skip.label": "我想手动添加æœåŠ¡", 46 "import.skip.label": "我想手动添加æœåŠ¡",
47 "import.submit.label": "Import {count} services", 47 "import.submit.label": "导入 {count} æœåŠ¡",
48 "infobar.authRequestFailed": "å°è¯•è¿›è¡Œèº«ä»½éªŒè¯æ—¶å‡ºé”™ã€‚如果此错误ä»å­˜åœ¨ï¼Œè¯·å°è¯•æ³¨é”€å¹¶é‡æ–°ç™»å½•ã€‚", 48 "infobar.authRequestFailed": "å°è¯•è¿›è¡Œèº«ä»½éªŒè¯æ—¶å‡ºé”™ã€‚如果此错误ä»å­˜åœ¨ï¼Œè¯·å°è¯•æ³¨é”€å¹¶é‡æ–°ç™»å½•ã€‚",
49 "infobar.buttonChangelog": "What is new?", 49 "infobar.buttonChangelog": "新功能",
50 "infobar.buttonInstallUpdate": "é‡å¯å¹¶å®‰è£…æ›´æ–°", 50 "infobar.buttonInstallUpdate": "é‡å¯å¹¶å®‰è£…æ›´æ–°",
51 "infobar.buttonReloadServices": "é‡è½½æœåŠ¡", 51 "infobar.buttonReloadServices": "é‡è½½æœåŠ¡",
52 "infobar.hide": "éšè—", 52 "infobar.hide": "éšè—",
@@ -59,398 +59,401 @@
59 "invite.name.label": "å称", 59 "invite.name.label": "å称",
60 "invite.skip.label": "ç¨åŽå†åš", 60 "invite.skip.label": "ç¨åŽå†åš",
61 "invite.submit.label": "å‘é€é‚€è¯·", 61 "invite.submit.label": "å‘é€é‚€è¯·",
62 "invite.successInfo": "Invitations sent successfully", 62 "invite.successInfo": "å·²æˆåŠŸå‘é€é‚€è¯·ã€‚",
63 "locked.headline": "å·²é”定", 63 "locked.headline": "å·²é”定",
64 "locked.info": "Ferdi is currently locked. Please unlock Ferdi with your password to see your messages.", 64 "locked.info": "Ferdiç›®å‰è¢«é”定。请使用密ç è§£é”Ferdi以查看您的消æ¯ã€‚",
65 "locked.invalidCredentials": "Password invalid", 65 "locked.invalidCredentials": "密ç æ— æ•ˆ",
66 "locked.password.label": "Password", 66 "locked.password.label": "密ç ",
67 "locked.submit.label": "Unlock", 67 "locked.submit.label": "解é”",
68 "locked.touchId": "Unlock with Touch ID", 68 "locked.touchId": "使用 Touch ID 解é”",
69 "locked.touchIdPrompt": "unlock via Touch ID", 69 "locked.touchIdPrompt": "使用 Touch ID 解é”",
70 "locked.unlockWithPassword": "Unlock with Password", 70 "locked.unlockWithPassword": "使用密ç è§£é”",
71 "login.changeServer": "Change server", 71 "login.changeServer": "更改æœåŠ¡å™¨",
72 "login.customServerQuestion": "Using a custom Ferdi server?", 72 "login.customServerQuestion": "Using a custom Ferdi server?",
73 "login.customServerSuggestion": "Try importing your Franz account", 73 "login.customServerSuggestion": "Try importing your Franz account",
74 "login.email.label": "电å­é‚®ç®±åœ°å€", 74 "login.email.label": "电å­é‚®ç®±åœ°å€",
75 "login.headline": "Sign in", 75 "login.headline": "登录",
76 "login.invalidCredentials": "Email or password not valid", 76 "login.invalidCredentials": "电å­é‚®ä»¶æˆ–密ç æ— æ•ˆ",
77 "login.link.password": "Reset password", 77 "login.link.password": "修改密ç ",
78 "login.link.signup": "Create a free account", 78 "login.link.signup": "创建å…费账户",
79 "login.password.label": "Password", 79 "login.password.label": "密ç ",
80 "login.serverLogout": "Your session expired, please login again.", 80 "login.serverLogout": "您的登录信æ¯å·²è¿‡æœŸï¼Œè¯·é‡æ–°ç™»å½•ã€‚",
81 "login.submit.label": "Sign in", 81 "login.submit.label": "登录",
82 "login.tokenExpired": "Your session expired, please login again.", 82 "login.tokenExpired": "您的登录信æ¯å·²è¿‡æœŸï¼Œè¯·é‡æ–°ç™»å½•ã€‚",
83 "menu.Todoss.closeTodosDrawer": "Close Todos drawer", 83 "menu.Todoss.closeTodosDrawer": "关闭Todos抽屉æ ",
84 "menu.Todoss.openTodosDrawer": "Open Todos drawer", 84 "menu.Todoss.openTodosDrawer": "打开Todos抽屉æ ",
85 "menu.app.about": "About Ferdi", 85 "menu.app.about": "关于 Ferdi",
86 "menu.app.autohideMenuBar": "Auto-hide menu bar", 86 "menu.app.autohideMenuBar": "自动éšè—èœå•æ ",
87 "menu.app.checkForUpdates": "Check for updates", 87 "menu.app.checkForUpdates": "检查更新",
88 "menu.app.hide": "éšè—", 88 "menu.app.hide": "éšè—",
89 "menu.app.hideOthers": "Hide Others", 89 "menu.app.hideOthers": "éšè—其它",
90 "menu.app.unhide": "Unhide", 90 "menu.app.unhide": "å–消éšè—",
91 "menu.edit": "编辑", 91 "menu.edit": "编辑",
92 "menu.edit.copy": "Copy", 92 "menu.edit.copy": "å¤åˆ¶",
93 "menu.edit.cut": "Cut", 93 "menu.edit.cut": "剪切",
94 "menu.edit.delete": "删除", 94 "menu.edit.delete": "删除",
95 "menu.edit.emojiSymbols": "Emoji 和符å·", 95 "menu.edit.emojiSymbols": "Emoji 和符å·",
96 "menu.edit.findInPage": "页内æœç´¢", 96 "menu.edit.findInPage": "页内æœç´¢",
97 "menu.edit.paste": "Paste", 97 "menu.edit.paste": "粘贴",
98 "menu.edit.pasteAndMatchStyle": "Paste And Match Style", 98 "menu.edit.pasteAndMatchStyle": "粘贴并匹é…æ ·å¼",
99 "menu.edit.redo": "Redo", 99 "menu.edit.redo": "æ¢å¤",
100 "menu.edit.selectAll": "Select All", 100 "menu.edit.selectAll": "全选",
101 "menu.edit.speech": "语音", 101 "menu.edit.speech": "语音",
102 "menu.edit.startDictation": "Start Dictation", 102 "menu.edit.startDictation": "Start Dictation",
103 "menu.edit.startSpeaking": "开始说è¯", 103 "menu.edit.startSpeaking": "开始说è¯",
104 "menu.edit.stopSpeaking": "åœæ­¢è¯´è¯", 104 "menu.edit.stopSpeaking": "åœæ­¢è¯´è¯",
105 "menu.edit.undo": "Undo", 105 "menu.edit.undo": "撤销",
106 "menu.file": "文件", 106 "menu.file": "文件",
107 "menu.help": "Help", 107 "menu.help": "帮助",
108 "menu.help.changelog": "更新日志", 108 "menu.help.changelog": "更新日志",
109 "menu.help.debugInfo": "å¤åˆ¶è°ƒè¯•ä¿¡æ¯", 109 "menu.help.debugInfo": "å¤åˆ¶è°ƒè¯•ä¿¡æ¯",
110 "menu.help.debugInfoCopiedBody": "Your Debug Information has been copied to your clipboard.", 110 "menu.help.debugInfoCopiedBody": "您的调试信æ¯å·²å¤åˆ¶åˆ°å‰ªè´´æ¿ã€‚",
111 "menu.help.debugInfoCopiedHeadline": "Ferdi Debug Information", 111 "menu.help.debugInfoCopiedHeadline": "å¤åˆ¶è°ƒè¯•ä¿¡æ¯",
112 "menu.help.importExportData": "Import/Export Configuration Data", 112 "menu.help.importExportData": "导入/导出é…置数æ®",
113 "menu.help.learnMore": "Learn More", 113 "menu.help.learnMore": "了解更多",
114 "menu.help.privacy": "Privacy Statement", 114 "menu.help.privacy": "éšç§å£°æ˜Ž",
115 "menu.help.publishDebugInfo": "å‘布调试信æ¯", 115 "menu.help.publishDebugInfo": "å‘布调试信æ¯",
116 "menu.help.support": "支æŒ", 116 "menu.help.support": "支æŒ",
117 "menu.help.tos": "æœåŠ¡æ¡æ¬¾", 117 "menu.help.tos": "æœåŠ¡æ¡æ¬¾",
118 "menu.services": "æœåŠ¡", 118 "menu.services": "æœåŠ¡",
119 "menu.services.activatePreviousService": "Activate previous service", 119 "menu.services.activatePreviousService": "激活上一个æœåŠ¡",
120 "menu.services.addNewService": "Add New Service...", 120 "menu.services.addNewService": "添加新æœåŠ¡",
121 "menu.services.goHome": "主页", 121 "menu.services.goHome": "主页",
122 "menu.services.setNextServiceActive": "Activate next service", 122 "menu.services.setNextServiceActive": "激活下一个æœåŠ¡",
123 "menu.todos": "待办事项", 123 "menu.todos": "待办事项",
124 "menu.todos.enableTodos": "å¯ç”¨å¾…办事项", 124 "menu.todos.enableTodos": "å¯ç”¨å¾…办事项",
125 "menu.view": "查看", 125 "menu.view": "查看",
126 "menu.view.back": "返回", 126 "menu.view.back": "返回",
127 "menu.view.forward": "Forward", 127 "menu.view.forward": "å‘å‰",
128 "menu.view.lockFerdi": "Lock Ferdi", 128 "menu.view.lockFerdi": "é”定Ferdi",
129 "menu.view.openQuickSwitch": "Open Quick Switch", 129 "menu.view.openQuickSwitch": "打开快速切æ¢",
130 "menu.view.reloadFerdi": "Reload Ferdi", 130 "menu.view.reloadFerdi": "é‡å¯Ferdi",
131 "menu.view.reloadService": "é‡æ–°åŠ è½½æœåŠ¡", 131 "menu.view.reloadService": "é‡æ–°åŠ è½½æœåŠ¡",
132 "menu.view.reloadTodos": "Reload ToDos", 132 "menu.view.reloadTodos": "é‡å¯ToDos",
133 "menu.view.resetZoom": "Actual Size", 133 "menu.view.resetZoom": "实际尺寸",
134 "menu.view.toggleDarkMode": "Toggle Dark Mode", 134 "menu.view.toggleDarkMode": "切æ¢æš—色主题",
135 "menu.view.toggleDevTools": "Toggle Developer Tools", 135 "menu.view.toggleDevTools": "切æ¢å¼€å‘人员工具",
136 "menu.view.toggleFullScreen": "Toggle Full Screen", 136 "menu.view.toggleFullScreen": "切æ¢å…¨å±",
137 "menu.view.toggleServiceDevTools": "Toggle Service Developer Tools", 137 "menu.view.toggleServiceDevTools": "切æ¢æœåŠ¡å¼€å‘工具",
138 "menu.view.toggleTodosDevTools": "Toggle Todos Developer Tools", 138 "menu.view.toggleTodosDevTools": "切æ¢Todoså¼€å‘工具",
139 "menu.view.zoomIn": "Zoom In", 139 "menu.view.zoomIn": "放大",
140 "menu.view.zoomOut": "Zoom Out", 140 "menu.view.zoomOut": "缩å°",
141 "menu.window": "Window", 141 "menu.window": "窗å£",
142 "menu.window.close": "Close", 142 "menu.window.close": "关闭",
143 "menu.window.minimize": "Minimize", 143 "menu.window.minimize": "最å°åŒ–",
144 "menu.workspaces": "Workspaces", 144 "menu.workspaces": "工作区",
145 "menu.workspaces.addNewWorkspace": "Add New Workspace...", 145 "menu.workspaces.addNewWorkspace": "新建工作区…",
146 "menu.workspaces.closeWorkspaceDrawer": "Close workspace drawer", 146 "menu.workspaces.closeWorkspaceDrawer": "关闭工作区抽屉æ ",
147 "menu.workspaces.defaultWorkspace": "All services", 147 "menu.workspaces.defaultWorkspace": "所有æœåŠ¡",
148 "menu.workspaces.openWorkspaceDrawer": "Open workspace drawer", 148 "menu.workspaces.openWorkspaceDrawer": "打开工作区抽屉æ ",
149 "password.email.label": "电å­é‚®ç®±åœ°å€", 149 "password.email.label": "电å­é‚®ç®±åœ°å€",
150 "password.headline": "Reset password", 150 "password.headline": "修改密ç ",
151 "password.link.login": "Sign in to your account", 151 "password.link.login": "登录到您的å¸æˆ·",
152 "password.link.signup": "Create a free account", 152 "password.link.signup": "创建å…费账户",
153 "password.noUser": "No user with that email address was found", 153 "password.noUser": "找ä¸åˆ°è¯¥ç”µå­é‚®ä»¶åœ°å€çš„用户",
154 "password.successInfo": "Your new password was sent to your email address", 154 "password.successInfo": "新密ç å·²å‘é€åˆ°æ‚¨çš„邮箱",
155 "service.crashHandler.action": "Reload {name}", 155 "service.crashHandler.action": "é‡æ–°åŠ è½½{name}",
156 "service.crashHandler.autoReload": "Trying to automatically restore {name} in {seconds} seconds", 156 "service.crashHandler.autoReload": "å°è¯•åœ¨ {name} 秒åŽè‡ªåŠ¨æ¢å¤ {seconds}",
157 "service.crashHandler.headline": "Oh no!", 157 "service.crashHandler.headline": "糟糕ï¼",
158 "service.crashHandler.text": "{name} has caused an error.", 158 "service.crashHandler.text": "{name} 造æˆäº†ä¸€ä¸ªé”™è¯¯ã€‚",
159 "service.disabledHandler.action": "Enable {name}", 159 "service.disabledHandler.action": "å¯ç”¨ {name}",
160 "service.disabledHandler.headline": "{name} is disabled", 160 "service.disabledHandler.headline": "{name} å·²ç¦ç”¨",
161 "service.errorHandler.action": "Reload {name}", 161 "service.errorHandler.action": "é‡æ–°åŠ è½½{name}",
162 "service.errorHandler.editAction": "Edit {name}", 162 "service.errorHandler.editAction": "编辑 {name}",
163 "service.errorHandler.headline": "Oh no!", 163 "service.errorHandler.headline": "糟糕ï¼",
164 "service.errorHandler.message": "Error", 164 "service.errorHandler.message": "错误",
165 "service.errorHandler.text": "{name} has failed to load.", 165 "service.errorHandler.text": "{name} 加载失败。",
166 "service.webviewLoader.loading": "Loading {service}", 166 "service.webviewLoader.loading": "正在加载 {service}",
167 "services.getStarted": "Get started", 167 "services.getStarted": "马上体验",
168 "services.login": "Please login to use Ferdi.", 168 "services.login": "请登录åŽä½¿ç”¨ Ferdi。",
169 "services.serverInfo": "Optionally, you can change your Ferdi server by clicking the cog in the bottom left corner. If you are switching over (from one of the hosted servers) to using Ferdi without an account, please be informed that you can export your data from that server and subsequently import it using the Help menu to resurrect all your workspaces and configured services!", 169 "services.serverInfo": "或者,您å¯ä»¥é€šè¿‡å•å‡»å·¦ä¸‹è§’的齿轮æ¥æ›´æ”¹æ‚¨çš„ Ferdi æœåŠ¡å™¨ã€‚ 如果您è¦ï¼ˆä»Žä¸€å°æ‰˜ç®¡æœåŠ¡å™¨ï¼‰åˆ‡æ¢åˆ°æ²¡æœ‰å¸æˆ·çš„ Ferdi,请注æ„,您å¯ä»¥ä»Žè¯¥æœåŠ¡å™¨å¯¼å‡ºæ•°æ®ï¼Œç„¶åŽä½¿ç”¨â€œå¸®åŠ©â€èœå•å°†å…¶å¯¼å…¥ï¼Œä»¥æ¢å¤æ‰€æœ‰å·¥ä½œåŒºå’Œé…置的æœåŠ¡ï¼",
170 "services.serverless": "Use Ferdi without an Account", 170 "services.serverless": "使用ä¸å¸¦è´¦æˆ·çš„Ferdi",
171 "services.welcome": "Welcome to Ferdi", 171 "services.welcome": "欢迎使用 Ferdi",
172 "settings.account.account.editButton": "Edit account", 172 "settings.account.account.editButton": "编辑å¸æˆ·",
173 "settings.account.accountUnavailable": "Account is unavailable", 173 "settings.account.accountUnavailable": "账户ä¸å¯ç”¨",
174 "settings.account.accountUnavailableInfo": "You are using Ferdi without an account. If you want to use Ferdi with an account and keep your services synchronized across installations, please select a server in the Settings tab then login.", 174 "settings.account.accountUnavailableInfo": "You are using Ferdi without an account. If you want to use Ferdi with an account and keep your services synchronized across installations, please select a server in the Settings tab then login.",
175 "settings.account.buttonSave": "Update profile", 175 "settings.account.buttonSave": "æ›´æ–°é…置文件",
176 "settings.account.deleteAccount": "Delete account", 176 "settings.account.deleteAccount": "删除账户",
177 "settings.account.deleteEmailSent": "You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!", 177 "settings.account.deleteEmailSent": "您会收到一å°å«æœ‰ç¡®è®¤åˆ é™¤å¸æˆ·çš„电å­é‚®ä»¶ã€‚您的å¸æˆ·å’Œæ•°æ®å°†æ— æ³•æ¢å¤ï¼",
178 "settings.account.deleteInfo": "If you don't need your Ferdi account any longer, you can delete your account and all related data here.", 178 "settings.account.deleteInfo": "如果您ä¸å†éœ€è¦æ‚¨çš„ Ferdi å¸æˆ·ï¼Œæ‚¨å¯ä»¥åœ¨è¿™é‡Œåˆ é™¤æ‚¨çš„å¸æˆ·å’Œæ‰€æœ‰ç›¸å…³æ•°æ®ã€‚",
179 "settings.account.headline": "Account", 179 "settings.account.headline": "账户",
180 "settings.account.headlineAccount": "Account information", 180 "settings.account.headlineAccount": "账户信æ¯",
181 "settings.account.headlineDangerZone": "Danger Zone", 181 "settings.account.headlineDangerZone": "å±é™©æ“作",
182 "settings.account.headlineInvoices": "Invoices", 182 "settings.account.headlineInvoices": "Invoices",
183 "settings.account.headlinePassword": "Change password", 183 "settings.account.headlinePassword": "更改密ç ",
184 "settings.account.headlineProfile": "Update profile", 184 "settings.account.headlineProfile": "æ›´æ–°é…置文件",
185 "settings.account.successInfo": "Your changes have been saved", 185 "settings.account.successInfo": "您的更改已ä¿å­˜",
186 "settings.account.tryReloadServices": "Try again", 186 "settings.account.tryReloadServices": "é‡æ–°å°è¯•",
187 "settings.account.tryReloadUserInfoRequest": "Try again", 187 "settings.account.tryReloadUserInfoRequest": "é‡æ–°å°è¯•",
188 "settings.account.userInfoRequestFailed": "Could not load user information", 188 "settings.account.userInfoRequestFailed": "无法加载用户信æ¯ã€‚",
189 "settings.account.yourLicense": "Your Ferdi License:", 189 "settings.account.yourLicense": "您的 Ferdi 许å¯è¯ï¼š",
190 "settings.app.accentColorInfo": "Write your accent color in a CSS-compatible format. (Default: {defaultAccentColor})", 190 "settings.app.accentColorInfo": "ä½ å¯ä»¥å†™å…¥ä¸€ä¸ªCSSæ ¼å¼æ‰€æ”¯æŒçš„强调色。(默认: {defaultAccentColor})",
191 "settings.app.buttonClearAllCache": "Clear cache", 191 "settings.app.buttonClearAllCache": "清除缓存",
192 "settings.app.buttonInstallUpdate": "é‡å¯å¹¶å®‰è£…æ›´æ–°", 192 "settings.app.buttonInstallUpdate": "é‡å¯å¹¶å®‰è£…æ›´æ–°",
193 "settings.app.buttonOpenFerdiProfileFolder": "Open Profile folder", 193 "settings.app.buttonOpenFerdiProfileFolder": "打开é…置文件所在目录",
194 "settings.app.buttonOpenFerdiServiceRecipesFolder": "Open Service Recipes folder", 194 "settings.app.buttonOpenFerdiServiceRecipesFolder": "Open Service Recipes folder",
195 "settings.app.buttonSearchForUpdate": "Check for updates", 195 "settings.app.buttonSearchForUpdate": "检查更新",
196 "settings.app.cacheInfo": "Ferdi cache is currently using {size} of disk space.", 196 "settings.app.cacheInfo": "Ferdi 缓存目å‰å ç”¨äº† {size} ç£ç›˜ç©ºé—´ã€‚",
197 "settings.app.cacheNotCleared": "Couldn't clear all cache", 197 "settings.app.cacheNotCleared": "无法清除所有缓存",
198 "settings.app.closeSettings": "Close settings", 198 "settings.app.closeSettings": "关闭设置",
199 "settings.app.currentVersion": "Current version:", 199 "settings.app.currentVersion": "当å‰ç‰ˆæœ¬ï¼š",
200 "settings.app.form.accentColor": "Accent color", 200 "settings.app.form.accentColor": "强调色",
201 "settings.app.form.adaptableDarkMode": "Synchronize dark mode with my OS's dark mode setting", 201 "settings.app.form.adaptableDarkMode": "åŒæ­¥ç³»ç»Ÿæš—色模å¼",
202 "settings.app.form.alwaysShowWorkspaces": "Always show workspace drawer", 202 "settings.app.form.alwaysShowWorkspaces": "总是显示工作区抽屉æ ",
203 "settings.app.form.autoLaunchInBackground": "Open in background", 203 "settings.app.form.autoLaunchInBackground": "在åŽå°æ‰“å¼€",
204 "settings.app.form.autoLaunchOnStart": "Launch Ferdi on start", 204 "settings.app.form.autoLaunchOnStart": "开机å¯åŠ¨Ferdi\n",
205 "settings.app.form.automaticUpdates": "Enable updates", 205 "settings.app.form.automaticUpdates": "å¯ç”¨æ›´æ–°",
206 "settings.app.form.beta": "Include beta versions", 206 "settings.app.form.beta": "包å«æµ‹è¯•ç‰ˆ",
207 "settings.app.form.clipboardNotifications": "Don't show notifications for clipboard events", 207 "settings.app.form.clipboardNotifications": "ä¸æ˜¾ç¤ºå‰ªè´´æ¿äº‹ä»¶çš„通知",
208 "settings.app.form.closeToSystemTray": "Close Ferdi to system tray", 208 "settings.app.form.closeToSystemTray": "关闭Ferdi到系统托盘",
209 "settings.app.form.confirmOnQuit": "Confirm when quitting Ferdi", 209 "settings.app.form.confirmOnQuit": "退出Ferdi 时确认",
210 "settings.app.form.customTodoServer": "Custom Todo Server", 210 "settings.app.form.customTodoServer": "自定义TodoæœåŠ¡å™¨",
211 "settings.app.form.darkMode": "Enable Dark Mode", 211 "settings.app.form.darkMode": "å¼€å¯æ·±è‰²ä¸»é¢˜",
212 "settings.app.form.enableGPUAcceleration": "Enable GPU Acceleration", 212 "settings.app.form.enableGPUAcceleration": "å¯ç”¨ GPU 加速",
213 "settings.app.form.enableLock": "Enable Password Lock", 213 "settings.app.form.enableGlobalHideShortcut": "å¯ç”¨å…¨å±€å¿«æ·é”®ä»¥éšè—Ferdi",
214 "settings.app.form.enableMenuBar": "Always show Ferdi in Menu Bar", 214 "settings.app.form.enableLock": "å¯ç”¨å¯†ç é”",
215 "settings.app.form.enableSpellchecking": "Enable spell checking", 215 "settings.app.form.enableLongPressServiceHint": "Enable service shortcut hint on long press",
216 "settings.app.form.enableSystemTray": "Always show Ferdi in System Tray", 216 "settings.app.form.enableMenuBar": "总是在èœå•æ ä¸­æ˜¾ç¤º Ferdi",
217 "settings.app.form.enableTodos": "Enable Ferdi Todos", 217 "settings.app.form.enableSpellchecking": "å¯ç”¨æ‹¼å†™æ£€æŸ¥",
218 "settings.app.form.hibernateOnStartup": "Keep services in hibernation on startup", 218 "settings.app.form.enableSystemTray": "总是在系统托盘中显示 Ferdi",
219 "settings.app.form.hibernationStrategy": "Hibernation strategy", 219 "settings.app.form.enableTodos": "å¯ç”¨ Ferdi Todos",
220 "settings.app.form.iconSize": "Service icon size", 220 "settings.app.form.hibernateOnStartup": "å¯åŠ¨æ—¶ä¿æŒä¼‘眠æœåŠ¡",
221 "settings.app.form.inactivityLock": "Lock after inactivity", 221 "settings.app.form.hibernationStrategy": "休眠策略",
222 "settings.app.form.keepAllWorkspacesLoaded": "Keep all workspaces loaded", 222 "settings.app.form.iconSize": "æœåŠ¡å›¾æ ‡å¤§å°",
223 "settings.app.form.language": "Language", 223 "settings.app.form.inactivityLock": "自动é”定",
224 "settings.app.form.lockPassword": "Password", 224 "settings.app.form.keepAllWorkspacesLoaded": "ä¿æŒæ‰€æœ‰å·¥ä½œåŒºåŠ è½½",
225 "settings.app.form.minimizeToSystemTray": "Minimize Ferdi to system tray", 225 "settings.app.form.language": "语言(Language)",
226 "settings.app.form.lockPassword": "密ç ",
227 "settings.app.form.minimizeToSystemTray": "最å°åŒ–Ferdi到系统托盘",
226 "settings.app.form.navigationBarBehaviour": "Navigation bar behaviour", 228 "settings.app.form.navigationBarBehaviour": "Navigation bar behaviour",
227 "settings.app.form.notifyTaskBarOnMessage": "Notify TaskBar/Dock on new message", 229 "settings.app.form.notifyTaskBarOnMessage": "Notify TaskBar/Dock on new message",
228 "settings.app.form.passwordToggle": "Password toggle", 230 "settings.app.form.passwordToggle": "Password toggle",
229 "settings.app.form.predefinedTodoServer": "Todo Server", 231 "settings.app.form.predefinedTodoServer": "TodoæœåŠ¡å™¨",
230 "settings.app.form.privateNotifications": "Don't show message content in notifications", 232 "settings.app.form.privateNotifications": "ä¸åœ¨é€šçŸ¥ä¸­æ˜¾ç¤ºæ¶ˆæ¯å†…容",
231 "settings.app.form.reloadAfterResume": "Reload Ferdi after system resume", 233 "settings.app.form.reloadAfterResume": "系统æ¢å¤åŽé‡æ–°åŠ è½½ Ferdi",
232 "settings.app.form.runInBackground": "Keep Ferdi in background when closing the window", 234 "settings.app.form.runInBackground": "关闭窗å£æ—¶ä¿æŒFerdiåŽå°çŠ¶æ€",
233 "settings.app.form.scheduledDNDEnabled": "Enable scheduled Do-not-Disturb", 235 "settings.app.form.scheduledDNDEnabled": "Enable scheduled Do-not-Disturb",
234 "settings.app.form.scheduledDNDEnd": "To", 236 "settings.app.form.scheduledDNDEnd": "至",
235 "settings.app.form.scheduledDNDStart": "From", 237 "settings.app.form.scheduledDNDStart": "从",
236 "settings.app.form.searchEngine": "Search engine", 238 "settings.app.form.searchEngine": "æœç´¢å¼•æ“Ž",
237 "settings.app.form.sentry": "Send telemetry data", 239 "settings.app.form.sentry": "å‘é€é¥æµ‹æ•°æ®",
238 "settings.app.form.serviceRibbonWidth": "Sidebar width", 240 "settings.app.form.serviceRibbonWidth": "侧边æ å®½åº¦",
239 "settings.app.form.showDisabledServices": "Display disabled services tabs", 241 "settings.app.form.showDisabledServices": "显示ç¦ç”¨çš„æœåŠ¡æ ‡ç­¾",
240 "settings.app.form.showDragArea": "Show draggable area on window", 242 "settings.app.form.showDragArea": "在窗å£ä¸Šæ˜¾ç¤ºå¯æ‹–动区域",
241 "settings.app.form.showMessagesBadgesWhenMuted": "Show unread message badge when notifications are disabled", 243 "settings.app.form.showMessagesBadgesWhenMuted": "ç¦ç”¨é€šçŸ¥æ—¶æ˜¾ç¤ºæœªè¯»æ¶ˆæ¯å¾½ç« ",
242 "settings.app.form.splitMode": "Enable Split View Mode", 244 "settings.app.form.splitMode": "å¯ç”¨åˆ†å‰²è§†å›¾æ¨¡å¼",
243 "settings.app.form.startMinimized": "Start minimized", 245 "settings.app.form.startMinimized": "å¯åŠ¨æ—¶æœ€å°åŒ–",
244 "settings.app.form.universalDarkMode": "Enable universal Dark Mode", 246 "settings.app.form.universalDarkMode": "Enable universal Dark Mode",
245 "settings.app.form.useTouchIdToUnlock": "Allow using TouchID to unlock Ferdi", 247 "settings.app.form.useTouchIdToUnlock": "å…许使用 TouchID 解é”Ferdi",
246 "settings.app.form.useVerticalStyle": "Use horizontal style", 248 "settings.app.form.useVerticalStyle": "使用水平样å¼",
247 "settings.app.form.wakeUpStrategy": "Wake up strategy", 249 "settings.app.form.wakeUpStrategy": "唤醒策略",
248 "settings.app.headlineAdvanced": "Advanced", 250 "settings.app.headlineAdvanced": "高级",
249 "settings.app.headlineAppearance": "Appearance", 251 "settings.app.headlineAppearance": "外观",
250 "settings.app.headlineGeneral": "General", 252 "settings.app.headlineGeneral": "一般信æ¯",
251 "settings.app.headlineLanguage": "Language", 253 "settings.app.headlineLanguage": "语言(Language)",
252 "settings.app.headlinePrivacy": "Privacy", 254 "settings.app.headlinePrivacy": "éšç§æ”¿ç­–",
253 "settings.app.headlineUpdates": "Updates", 255 "settings.app.headlineUpdates": "æ›´æ–°",
254 "settings.app.hibernateInfo": "By default, Ferdi will keep all your services open and loaded in the background so they are ready when you want to use them. Service Hibernation will unload your services after a specified amount. This is useful to save RAM or keeping services from slowing down your computer.", 256 "settings.app.hibernateInfo": "默认情况下,Ferdiå°†ä¿æŒæ‚¨æ‰€æœ‰çš„æœåŠ¡åœ¨åŽå°æ‰“开并加载,这样当您想è¦ä½¿ç”¨å®ƒä»¬æ—¶ä»–们就å¯ä»¥äº†ã€‚ æœåŠ¡ä¼‘眠将在指定数é‡åŽå¸è½½æ‚¨çš„æœåŠ¡ã€‚这有助于ä¿å­˜å†…存或ä¿æŒæœåŠ¡ä»¥å‡æ…¢æ‚¨çš„计算机速度。",
255 "settings.app.inactivityLockInfo": "Minutes of inactivity, after which Ferdi should automatically lock. Use 0 to disable", 257 "settings.app.inactivityLockInfo": "Minutes of inactivity, after which Ferdi should automatically lock. Use 0 to disable",
256 "settings.app.languageDisclaimer": "Official translations are English & German. All other languages are community based translations.", 258 "settings.app.languageDisclaimer": "官方翻译为英语和德语,所有其他语言都是基于社区的翻译。",
257 "settings.app.lockInfo": "Password Lock allows you to keep your messages protected.\nUsing Password Lock, you will be prompted to enter your password everytime you start Ferdi or lock Ferdi yourself using the lock symbol in the bottom left corner or the shortcut {lockShortcut}.", 259 "settings.app.lockInfo": "Password Lock allows you to keep your messages protected.\nUsing Password Lock, you will be prompted to enter your password everytime you start Ferdi or lock Ferdi yourself using the lock symbol in the bottom left corner or the shortcut {lockShortcut}.",
258 "settings.app.lockedPassword": "Password", 260 "settings.app.lockedPassword": "密ç ",
259 "settings.app.lockedPasswordInfo": "Please make sure to set a password you'll remember.\nIf you loose this password, you will have to reinstall Ferdi.", 261 "settings.app.lockedPasswordInfo": "请务必设置您将记ä½çš„密ç ã€‚\n如果您丢失了这个密ç ï¼Œæ‚¨å°†ä¸å¾—ä¸é‡æ–°å®‰è£…Ferdi。",
260 "settings.app.restartRequired": "Changes require restart", 262 "settings.app.restartRequired": "当å‰çš„å˜æ›´éœ€è¦é‡æ–°å¯åŠ¨",
261 "settings.app.scheduledDNDInfo": "Scheduled Do-not-Disturb allows you to define a period of time in which you do not want to get Notifications from Ferdi.", 263 "settings.app.scheduledDNDInfo": "Scheduled Do-not-Disturb allows you to define a period of time in which you do not want to get Notifications from Ferdi.",
262 "settings.app.scheduledDNDTimeInfo": "Times in 24-Hour-Format. End time can be before start time (e.g. start 17:00, end 09:00) to enable Do-not-Disturb overnight.", 264 "settings.app.scheduledDNDTimeInfo": "Times in 24-Hour-Format. End time can be before start time (e.g. start 17:00, end 09:00) to enable Do-not-Disturb overnight.",
263 "settings.app.sentryInfo": "Sending telemetry data allows us to find errors in Ferdi - we will not send any personal information like your message data!", 265 "settings.app.sentryInfo": "å‘é€é¥æµ‹æ•°æ®ä½¿æˆ‘们能够找到Ferdi错误——我们ä¸ä¼šå‘é€ä»»ä½•ä¸ªäººä¿¡æ¯ï¼Œå¦‚您的消æ¯æ•°æ®ï¼",
264 "settings.app.spellCheckerLanguageInfo": "Ferdi uses your Mac's build-in spellchecker to check for typos. If you want to change the languages the spellchecker checks for, you can do so in your Mac's System Preferences.", 266 "settings.app.spellCheckerLanguageInfo": "Ferdi uses your Mac's build-in spellchecker to check for typos. If you want to change the languages the spellchecker checks for, you can do so in your Mac's System Preferences.",
265 "settings.app.subheadlineCache": "Cache", 267 "settings.app.subheadlineCache": "缓存",
266 "settings.app.subheadlineFerdiProfile": "Ferdi Profile", 268 "settings.app.subheadlineFerdiProfile": "Ferdi Profile",
267 "settings.app.todoServerInfo": "This server will be used for the \"Ferdi Todo\" feature.", 269 "settings.app.todoServerInfo": "æ­¤æœåŠ¡å™¨å°†ç”¨äºŽ\"Ferdi Todo\"功能。",
268 "settings.app.translationHelp": "Help us to translate Ferdi into your language.", 270 "settings.app.translationHelp": "帮助我们翻译 Ferdi æˆæ‚¨çš„语言",
269 "settings.app.universalDarkModeInfo": "Universal Dark Mode tries to dynamically generate dark mode styles for services that are otherwise not currently supported.", 271 "settings.app.universalDarkModeInfo": "Universal Dark Mode tries to dynamically generate dark mode styles for services that are otherwise not currently supported.",
270 "settings.app.updateStatusAvailable": "Update available, downloading...", 272 "settings.app.updateStatusAvailable": "æ›´æ–°å¯ç”¨ï¼Œæ­£åœ¨ä¸‹è½½...",
271 "settings.app.updateStatusSearching": "Is searching for update", 273 "settings.app.updateStatusSearching": "正在æœç´¢æ›´æ–°",
272 "settings.app.updateStatusUpToDate": "You are using the latest version of Ferdi", 274 "settings.app.updateStatusUpToDate": "您正在使用最新版本的Ferdi",
273 "settings.invite.headline": "Invite Friends", 275 "settings.invite.headline": "邀请好å‹",
274 "settings.navigation.account": "Account", 276 "settings.navigation.account": "账户",
275 "settings.navigation.availableServices": "Available services", 277 "settings.navigation.availableServices": "å¯ç”¨çš„æœåŠ¡",
276 "settings.navigation.logout": "Logout", 278 "settings.navigation.logout": "注销",
277 "settings.navigation.supportFerdi": "About Ferdi", 279 "settings.navigation.supportFerdi": "关于 Ferdi",
278 "settings.navigation.team": "Manage Team", 280 "settings.navigation.team": "Manage Team",
279 "settings.navigation.yourServices": "Your services", 281 "settings.navigation.yourServices": "您的æœåŠ¡",
280 "settings.navigation.yourWorkspaces": "Your workspaces", 282 "settings.navigation.yourWorkspaces": "您的工作区",
281 "settings.recipes.all": "All services", 283 "settings.recipes.all": "所有æœåŠ¡",
282 "settings.recipes.custom": "Custom Services", 284 "settings.recipes.custom": "自定义æœåŠ¡",
283 "settings.recipes.customService.headline.communityRecipes": "Community 3rd Party Recipes", 285 "settings.recipes.customService.headline.communityRecipes": "Community 3rd Party Recipes",
284 "settings.recipes.customService.headline.customRecipes": "Custom 3rd Party Recipes", 286 "settings.recipes.customService.headline.customRecipes": "Custom 3rd Party Recipes",
285 "settings.recipes.customService.headline.devRecipes": "Your Development Service Recipes", 287 "settings.recipes.customService.headline.devRecipes": "Your Development Service Recipes",
286 "settings.recipes.customService.intro": "To add a custom service, copy the service recipe to:", 288 "settings.recipes.customService.intro": "To add a custom service, copy the service recipe to:",
287 "settings.recipes.customService.openDevDocs": "Developer Documentation", 289 "settings.recipes.customService.openDevDocs": "å¼€å‘者文档",
288 "settings.recipes.customService.openFolder": "Open folder", 290 "settings.recipes.customService.openFolder": "打开文件夹",
289 "settings.recipes.headline": "Available services", 291 "settings.recipes.headline": "å¯ç”¨çš„æœåŠ¡",
290 "settings.recipes.missingService": "Missing a service?", 292 "settings.recipes.missingService": "找ä¸åˆ°æœåŠ¡ï¼Ÿ",
291 "settings.recipes.nothingFound": "Sorry, but no service matched your search term - but you can still probably add it using the \"Custom Website\" option. Please note that the website might show more services that have been added to Ferdi since the version that you are currently on. To get those new services, please consider upgrading to a newer version of Ferdi.", 293 "settings.recipes.nothingFound": "对ä¸èµ·ï¼Œæ²¡æœ‰ä»»ä½•æœåŠ¡åŒ¹é…您的æœç´¢è¯ - 但您ä»ç„¶å¯ä»¥ä½¿ç”¨â€œè‡ªå®šä¹‰ç½‘ç«™â€é€‰é¡¹æ·»åŠ å®ƒã€‚ 请注æ„,网站å¯èƒ½ä¼šæ˜¾ç¤ºè‡ªæ‚¨å½“å‰ä½¿ç”¨çš„版本以æ¥æ·»åŠ åˆ°Ferdi的更多æœåŠ¡ã€‚ 为了获得这些新æœåŠ¡ï¼Œè¯·è€ƒè™‘å‡çº§ä¸ºè¾ƒæ–°ç‰ˆæœ¬çš„费尔迪。",
292 "settings.recipes.servicesSuccessfulAddedInfo": "Service successfully added", 294 "settings.recipes.servicesSuccessfulAddedInfo": "æœåŠ¡æ·»åŠ æˆåŠŸ",
293 "settings.searchService": "Search service", 295 "settings.searchService": "æœç´¢æœåŠ¡",
294 "settings.service.error.goBack": "Back to services", 296 "settings.service.error.goBack": "返回æœåŠ¡åˆ—表",
295 "settings.service.error.headline": "Error", 297 "settings.service.error.headline": "错误",
296 "settings.service.error.message": "Could not load service recipe.", 298 "settings.service.error.message": "Could not load service recipe.",
297 "settings.service.form.addServiceHeadline": "Add {name}", 299 "settings.service.form.addServiceHeadline": "添加 {name}",
298 "settings.service.form.availableServices": "Available services", 300 "settings.service.form.availableServices": "å¯ç”¨çš„æœåŠ¡",
299 "settings.service.form.customUrl": "Custom server", 301 "settings.service.form.customUrl": "自定义æœåŠ¡å™¨",
300 "settings.service.form.customUrlValidationError": "Could not validate custom {name} server.", 302 "settings.service.form.customUrlValidationError": "无法验è¯è‡ªå®šä¹‰ {name} æœåŠ¡å™¨ã€‚",
301 "settings.service.form.darkReaderBrightness": "Dark Reader Brightness", 303 "settings.service.form.darkReaderBrightness": "Dark Reader Brightness",
302 "settings.service.form.darkReaderContrast": "Dark Reader Contrast", 304 "settings.service.form.darkReaderContrast": "Dark Reader Contrast",
303 "settings.service.form.darkReaderSepia": "Dark Reader Sepia", 305 "settings.service.form.darkReaderSepia": "Dark Reader Sepia",
304 "settings.service.form.deleteButton": "Delete service", 306 "settings.service.form.deleteButton": "删除æœåŠ¡",
305 "settings.service.form.editServiceHeadline": "Edit {name}", 307 "settings.service.form.editServiceHeadline": "编辑 {name}",
306 "settings.service.form.enableAudio": "Enable audio", 308 "settings.service.form.enableAudio": "å¯ç”¨éŸ³é¢‘",
307 "settings.service.form.enableBadge": "Show unread message badges", 309 "settings.service.form.enableBadge": "Show unread message badges",
308 "settings.service.form.enableDarkMode": "Enable Dark Mode", 310 "settings.service.form.enableDarkMode": "å¼€å¯æ·±è‰²ä¸»é¢˜",
309 "settings.service.form.enableHibernation": "Enable hibernation", 311 "settings.service.form.enableHibernation": "å¯ç”¨ä¼‘眠",
310 "settings.service.form.enableNotification": "Enable notifications", 312 "settings.service.form.enableNotification": "å¼€å¯é€šçŸ¥",
311 "settings.service.form.enableService": "Enable service", 313 "settings.service.form.enableService": "å¯ç”¨æœåŠ¡",
314 "settings.service.form.enableWakeUp": "Enable wake up",
312 "settings.service.form.headlineBadges": "Unread message badges", 315 "settings.service.form.headlineBadges": "Unread message badges",
313 "settings.service.form.headlineDarkReaderSettings": "Dark Reader Settings", 316 "settings.service.form.headlineDarkReaderSettings": "暗色阅读器设置",
314 "settings.service.form.headlineGeneral": "General", 317 "settings.service.form.headlineGeneral": "一般信æ¯",
315 "settings.service.form.headlineNotifications": "Notifications", 318 "settings.service.form.headlineNotifications": "通知消æ¯",
316 "settings.service.form.icon": "Custom icon", 319 "settings.service.form.icon": "自定义图标",
317 "settings.service.form.iconDelete": "删除", 320 "settings.service.form.iconDelete": "删除",
318 "settings.service.form.iconUpload": "Drop your image, or click here", 321 "settings.service.form.iconUpload": "拖入图åƒæˆ–点击这里",
319 "settings.service.form.indirectMessageInfo": "You will be notified about all new messages in a channel, not just @username, @channel, @here, ...", 322 "settings.service.form.indirectMessageInfo": "您将收到一个频é“中所有新消æ¯çš„通知,而ä¸ä»…仅是@username, @channel, @here...",
320 "settings.service.form.indirectMessages": "Show message badge for all new messages", 323 "settings.service.form.indirectMessages": "Show message badge for all new messages",
321 "settings.service.form.isHibernatedEnabledInfo": "When enabled, a service will be shut down after a period of time to save system resources.", 324 "settings.service.form.isHibernatedEnabledInfo": "å¯ç”¨æ—¶ï¼ŒæœåŠ¡å°†åœ¨ä¸€æ®µæ—¶é—´åŽå…³é—­ï¼Œä»¥èŠ‚çœç³»ç»Ÿèµ„æºã€‚",
322 "settings.service.form.isMutedInfo": "When disabled, all notification sounds and audio playback are muted", 325 "settings.service.form.isMutedInfo": "ç¦ç”¨æ—¶ï¼Œæ‰€æœ‰é€šçŸ¥å£°éŸ³å’ŒéŸ³é¢‘将被é™éŸ³",
323 "settings.service.form.name": "å称", 326 "settings.service.form.name": "å称",
324 "settings.service.form.onlyShowFavoritesInUnreadCount": "Only show Favorites in unread count", 327 "settings.service.form.onlyShowFavoritesInUnreadCount": "Only show Favorites in unread count",
325 "settings.service.form.openDarkmodeCss": "Open darkmode.css", 328 "settings.service.form.openDarkmodeCss": "打开 darkmode.css",
326 "settings.service.form.openUserCss": "Open user.css", 329 "settings.service.form.openUserCss": "打开 user.css",
327 "settings.service.form.openUserJs": "Open user.js", 330 "settings.service.form.openUserJs": "打开user.js",
328 "settings.service.form.proxy.headline": "HTTP/HTTPS Proxy Settings", 331 "settings.service.form.proxy.headline": "HTTP/HTTPS 代ç†è®¾ç½®",
329 "settings.service.form.proxy.host": "Proxy Host/IP", 332 "settings.service.form.proxy.host": "代ç†ä¸»æœº/IP",
330 "settings.service.form.proxy.info": "Proxy settings will not be synchronized with the Ferdi servers.", 333 "settings.service.form.proxy.info": "代ç†è®¾ç½®å°†ä¸ä¼šä¸Ž Ferdi æœåŠ¡å™¨åŒæ­¥ã€‚",
331 "settings.service.form.proxy.isEnabled": "Use Proxy", 334 "settings.service.form.proxy.isEnabled": "使用代ç†",
332 "settings.service.form.proxy.password": "Password (optional)", 335 "settings.service.form.proxy.password": "å¯†ç  (å¯é€‰)",
333 "settings.service.form.proxy.port": "Port", 336 "settings.service.form.proxy.port": "端å£",
334 "settings.service.form.proxy.restartInfo": "Please restart Ferdi after changing proxy Settings.", 337 "settings.service.form.proxy.restartInfo": "更改代ç†è®¾ç½®åŽï¼Œè¯·é‡å¯ Ferdi。",
335 "settings.service.form.proxy.user": "User (optional)", 338 "settings.service.form.proxy.user": "用户 (å¯é€‰)",
336 "settings.service.form.recipeFileInfo": "Your user files will be inserted into the webpage so you can customize services in any way you like. User files are only stored locally and are not transferred to other computers using the same account.", 339 "settings.service.form.recipeFileInfo": "您的用户文件将被æ’入网页,以便您å¯ä»¥ä»¥ä»»ä½•æ–¹å¼è‡ªå®šä¹‰æœåŠ¡ã€‚ 用户文件åªå­˜å‚¨åœ¨æœ¬åœ°ï¼Œä¸ä¼šè¢«åŒä¸€è´¦æˆ·åŒæ­¥åˆ°å…¶ä»–计算机。",
337 "settings.service.form.saveButton": "Save service", 340 "settings.service.form.saveButton": "ä¿å­˜æœåŠ¡",
338 "settings.service.form.tabHosted": "Hosted", 341 "settings.service.form.tabHosted": "托管的",
339 "settings.service.form.tabOnPremise": "Self hosted â­ï¸", 342 "settings.service.form.tabOnPremise": "自我托管 â­ï¸",
340 "settings.service.form.team": "Team", 343 "settings.service.form.team": "团队",
341 "settings.service.form.useHostedService": "Use the hosted {name} service.", 344 "settings.service.form.useHostedService": "使用托管的 {name} æœåŠ¡ã€‚",
342 "settings.service.form.yourServices": "Your services", 345 "settings.service.form.yourServices": "您的æœåŠ¡",
343 "settings.services.deletedInfo": "Service has been deleted", 346 "settings.services.deletedInfo": "æœåŠ¡å·²è¢«åˆ é™¤",
344 "settings.services.discoverServices": "Discover services", 347 "settings.services.discoverServices": "å‘现æœåŠ¡",
345 "settings.services.headline": "Your services", 348 "settings.services.headline": "您的æœåŠ¡",
346 "settings.services.noServicesAdded": "Start by adding a service.", 349 "settings.services.noServicesAdded": "从添加æœåŠ¡å¼€å§‹ã€‚",
347 "settings.services.nothingFound": "Sorry, but no service matched your search term - but you can still probably add it using the \"Custom Website\" option. Please note that the website might show more services that have been added to Ferdi since the version that you are currently on. To get those new services, please consider upgrading to a newer version of Ferdi.", 350 "settings.services.nothingFound": "对ä¸èµ·ï¼Œæ²¡æœ‰ä»»ä½•æœåŠ¡åŒ¹é…您的æœç´¢è¯ - 但您ä»ç„¶å¯ä»¥ä½¿ç”¨â€œè‡ªå®šä¹‰ç½‘ç«™â€é€‰é¡¹æ·»åŠ å®ƒã€‚ 请注æ„,网站å¯èƒ½ä¼šæ˜¾ç¤ºè‡ªæ‚¨å½“å‰ä½¿ç”¨çš„版本以æ¥æ·»åŠ åˆ°Ferdi的更多æœåŠ¡ã€‚ 为了获得这些新æœåŠ¡ï¼Œè¯·è€ƒè™‘å‡çº§ä¸ºè¾ƒæ–°ç‰ˆæœ¬çš„费尔迪。",
348 "settings.services.servicesRequestFailed": "Could not load your services", 351 "settings.services.servicesRequestFailed": "无法加载您的æœåŠ¡",
349 "settings.services.tooltip.isDisabled": "Service is disabled", 352 "settings.services.tooltip.isDisabled": "æœåŠ¡å·²ç¦ç”¨",
350 "settings.services.tooltip.isMuted": "All sounds are muted", 353 "settings.services.tooltip.isMuted": "所有声音被é™éŸ³",
351 "settings.services.tooltip.notificationsDisabled": "Notifications are disabled", 354 "settings.services.tooltip.notificationsDisabled": "通知已ç¦ç”¨",
352 "settings.services.updatedInfo": "Your changes have been saved", 355 "settings.services.updatedInfo": "您的更改已ä¿å­˜",
353 "settings.supportFerdi.aboutIntro": "<p>Ferdi is an open-source and a community-lead application.</p><p>Thanks to the people who make this possbile:</p>", 356 "settings.supportFerdi.aboutIntro": "<p>Ferdi是一个开æºçš„软件。</p><p>感谢那些使Ferdiæˆä¸ºå¯èƒ½çš„人:</p>",
354 "settings.supportFerdi.bannerText": "Do you want to help us improve Ferdi?", 357 "settings.supportFerdi.bannerText": "你想è¦å¸®åŠ©æˆ‘们改进Ferdiå—?",
355 "settings.supportFerdi.headline": "About Ferdi", 358 "settings.supportFerdi.headline": "关于 Ferdi",
356 "settings.supportFerdi.openSurvey": "Open survey", 359 "settings.supportFerdi.openSurvey": "打开调查",
357 "settings.supportFerdi.textDonation": "If you feel like supporting Ferdi development with a donation, you can do so on both,", 360 "settings.supportFerdi.textDonation": "如果你想è¦æèµ Ferdiçš„å¼€å‘,你å¯ä»¥é€šè¿‡ï¼š",
358 "settings.supportFerdi.textDonationAnd": "and", 361 "settings.supportFerdi.textDonationAnd": "和",
359 "settings.supportFerdi.textExpenses": "While volunteers do most of the work, we still need to pay for servers and certificates. As a community, we are fully transparent on funds we collect and spend - see our", 362 "settings.supportFerdi.textExpenses": "虽然是志愿者åšå¤§éƒ¨åˆ†å·¥ä½œï¼Œä½†æˆ‘们ä»ç„¶éœ€è¦ç”¨é’±æ¥è´­ä¹°æœåŠ¡å™¨å’Œè¯ä¹¦ã€‚ 作为一个社区应用,我们在募集和花费的资金上是完全公开的—— 看看我们的",
360 "settings.supportFerdi.textGitHubSponsors": "GitHub Sponsors", 363 "settings.supportFerdi.textGitHubSponsors": "GitHub Sponsors",
361 "settings.supportFerdi.textListContributors": "Full list of contributors", 364 "settings.supportFerdi.textListContributors": "贡献者列表",
362 "settings.supportFerdi.textListContributorsHere": "here", 365 "settings.supportFerdi.textListContributorsHere": "这里",
363 "settings.supportFerdi.textOpenCollective": "Open Collective", 366 "settings.supportFerdi.textOpenCollective": "Open Collective",
364 "settings.supportFerdi.textSupportWelcome": "Support is always welcome. You can find a list of the help we need", 367 "settings.supportFerdi.textSupportWelcome": "支æŒæ€»æ˜¯å—欢迎的。您å¯ä»¥æ‰¾åˆ°æˆ‘们需è¦çš„帮助列表",
365 "settings.supportFerdi.textSupportWelcomeHere": "here", 368 "settings.supportFerdi.textSupportWelcomeHere": "这里",
366 "settings.supportFerdi.textVolunteers": "The development of Ferdi is done by volunteers. People who use Ferdi like you. They maintain, fix, and improve Ferdi in their spare time.", 369 "settings.supportFerdi.textVolunteers": "Ferdi是由志愿者开å‘而æˆã€‚他们用空闲时间维护ã€ä¿®å¤å’Œæ”¹è¿›Ferdi。",
367 "settings.supportFerdi.title": "Do you like Ferdi?", 370 "settings.supportFerdi.title": "你喜欢Ferdiå—?",
368 "settings.team.contentHeadline": "Franz Team Management", 371 "settings.team.contentHeadline": "Franz Team Management",
369 "settings.team.copy": "Franz's Team Management allows you to manage Franz Subscriptions for multiple users. Please keep in mind that having a Franz Premium subscription will give you no advantages in using Ferdi: The only reason you still have access to Team Management is so you can manage your legacy Franz Teams and so that you don't loose any functionality in managing your account.", 372 "settings.team.copy": "Franz's Team Management allows you to manage Franz Subscriptions for multiple users. Please keep in mind that having a Franz Premium subscription will give you no advantages in using Ferdi: The only reason you still have access to Team Management is so you can manage your legacy Franz Teams and so that you don't loose any functionality in managing your account.",
370 "settings.team.headline": "Team", 373 "settings.team.headline": "团队",
371 "settings.team.intro": "You are currently using Franz Servers, which is why you have access to Team Management.", 374 "settings.team.intro": "You are currently using Franz Servers, which is why you have access to Team Management.",
372 "settings.team.manageAction": "Manage your Team on meetfranz.com", 375 "settings.team.manageAction": "Manage your Team on meetfranz.com",
373 "settings.team.teamsUnavailable": "Teams are unavailable", 376 "settings.team.teamsUnavailable": "Teams are unavailable",
374 "settings.team.teamsUnavailableInfo": "Teams are currently only available when using the Franz Server and after paying for Franz Professional. Please change your server to https://api.franzinfra.com to use teams.", 377 "settings.team.teamsUnavailableInfo": "Teams are currently only available when using the Franz Server and after paying for Franz Professional. Please change your server to https://api.franzinfra.com to use teams.",
375 "settings.user.form.accountType.company": "Company", 378 "settings.user.form.accountType.company": "å…¬å¸",
376 "settings.user.form.accountType.individual": "Individual", 379 "settings.user.form.accountType.individual": "个人",
377 "settings.user.form.accountType.label": "Account type", 380 "settings.user.form.accountType.label": "è´¦å·ç±»åž‹",
378 "settings.user.form.accountType.non-profit": "Non-Profit", 381 "settings.user.form.accountType.non-profit": "éžç›ˆåˆ©çš„",
379 "settings.user.form.currentPassword": "Current password", 382 "settings.user.form.currentPassword": "当å‰å¯†ç ",
380 "settings.user.form.email": "Email", 383 "settings.user.form.email": "Email",
381 "settings.user.form.firstname": "First Name", 384 "settings.user.form.firstname": "åå­—",
382 "settings.user.form.lastname": "Last Name", 385 "settings.user.form.lastname": "姓æ°",
383 "settings.user.form.newPassword": "New password", 386 "settings.user.form.newPassword": "新密ç ",
384 "settings.workspace.add.form.name": "å称", 387 "settings.workspace.add.form.name": "å称",
385 "settings.workspace.add.form.submitButton": "Create workspace", 388 "settings.workspace.add.form.submitButton": "创建工作区",
386 "settings.workspace.form.buttonDelete": "Delete workspace", 389 "settings.workspace.form.buttonDelete": "删除工作区",
387 "settings.workspace.form.buttonSave": "Save workspace", 390 "settings.workspace.form.buttonSave": "ä¿å­˜å·¥ä½œåŒº",
388 "settings.workspace.form.keepLoaded": "Keep this workspace loaded*", 391 "settings.workspace.form.keepLoaded": "ä¿æŒæ­¤å·¥ä½œåŒºåŠ è½½*",
389 "settings.workspace.form.keepLoadedInfo": "*This option will be overwritten by the global \"Keep all workspaces loaded\" option.", 392 "settings.workspace.form.keepLoadedInfo": "*此选项将被全局选项“ä¿æŒæ‰€æœ‰å·¥ä½œåŒºåŠ è½½â€æ‰€è¦†ç›–。",
390 "settings.workspace.form.name": "å称", 393 "settings.workspace.form.name": "å称",
391 "settings.workspace.form.servicesInWorkspaceHeadline": "Services in this Workspace", 394 "settings.workspace.form.servicesInWorkspaceHeadline": "此工作区中的æœåŠ¡",
392 "settings.workspace.form.yourWorkspaces": "Your workspaces", 395 "settings.workspace.form.yourWorkspaces": "您的工作区",
393 "settings.workspaces.deletedInfo": "Workspace has been deleted", 396 "settings.workspaces.deletedInfo": "工作区已被删除",
394 "settings.workspaces.headline": "Your workspaces", 397 "settings.workspaces.headline": "您的工作区",
395 "settings.workspaces.noWorkspacesAdded": "You haven't created any workspaces yet.", 398 "settings.workspaces.noWorkspacesAdded": "您尚未创建任何工作区。",
396 "settings.workspaces.tryReloadWorkspaces": "Try again", 399 "settings.workspaces.tryReloadWorkspaces": "é‡æ–°å°è¯•",
397 "settings.workspaces.updatedInfo": "Your changes have been saved", 400 "settings.workspaces.updatedInfo": "您的更改已ä¿å­˜",
398 "settings.workspaces.workspaceFeatureHeadline": "Less is More: Introducing Ferdi Workspaces", 401 "settings.workspaces.workspaceFeatureHeadline": "Less is More: Introducing Ferdi Workspaces",
399 "settings.workspaces.workspaceFeatureInfo": "Ferdi Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time. You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.", 402 "settings.workspaces.workspaceFeatureInfo": "Ferdi工作区å¯ä»¥è®©æ‚¨ä¸“注于当下最é‡è¦çš„事情。 设置ä¸åŒçš„æœåŠ¡é›†åˆï¼Œè®©æ‚¨éšæ—¶å¯ä»¥çš„在它们之间轻æ¾çš„切æ¢ã€‚ ",
400 "settings.workspaces.workspacesRequestFailed": "Could not load your workspaces", 403 "settings.workspaces.workspacesRequestFailed": "无法加载您的工作区",
401 "setupAssistant.headline": "Let's get started", 404 "setupAssistant.headline": "我们开始å§ï¼",
402 "setupAssistant.subheadline": "Choose from our most used services and get back on top of your messaging now.", 405 "setupAssistant.subheadline": "Choose from our most used services and get back on top of your messaging now.",
403 "setupAssistant.submit.label": "Let's go", 406 "setupAssistant.submit.label": "让我们开始å§",
404 "sidebar.addNewService": "Add new service", 407 "sidebar.addNewService": "添加æœåŠ¡",
405 "sidebar.closeTodosDrawer": "Close Ferdi Todos", 408 "sidebar.closeTodosDrawer": "关闭Ferdi Todos",
406 "sidebar.closeWorkspaceDrawer": "Close workspace drawer", 409 "sidebar.closeWorkspaceDrawer": "关闭工作区抽屉æ ",
407 "sidebar.lockFerdi": "Lock Ferdi", 410 "sidebar.lockFerdi": "é”定Ferdi",
408 "sidebar.muteApp": "Disable notifications & audio", 411 "sidebar.muteApp": "ç¦ç”¨é€šçŸ¥å’ŒéŸ³é¢‘",
409 "sidebar.openTodosDrawer": "Open Ferdi Todos", 412 "sidebar.openTodosDrawer": "å¼€å¯Ferdi Todos",
410 "sidebar.openWorkspaceDrawer": "Open workspace drawer", 413 "sidebar.openWorkspaceDrawer": "打开工作区抽屉æ ",
411 "sidebar.unmuteApp": "Enable notifications & audio", 414 "sidebar.unmuteApp": "å¯ç”¨é€šçŸ¥å’ŒéŸ³é¢‘",
412 "signup.email.label": "电å­é‚®ç®±åœ°å€", 415 "signup.email.label": "电å­é‚®ç®±åœ°å€",
413 "signup.emailDuplicate": "A user with that email address already exists", 416 "signup.emailDuplicate": "该电å­é‚®ä»¶åœ°å€çš„用户已ç»å­˜åœ¨",
414 "signup.firstname.label": "First Name", 417 "signup.firstname.label": "åå­—",
415 "signup.headline": "Sign up", 418 "signup.headline": "登录",
416 "signup.lastname.label": "Last Name", 419 "signup.lastname.label": "姓æ°",
417 "signup.legal.info": "By creating a Ferdi account you accept the", 420 "signup.legal.info": "创建å¸æˆ·åˆ™è¡¨æ˜Žæ‚¨æŽ¥å—我们的",
418 "signup.legal.privacy": "Privacy Statement", 421 "signup.legal.privacy": "éšç§å£°æ˜Ž",
419 "signup.legal.terms": "Terms of service", 422 "signup.legal.terms": "æœåŠ¡æ¡æ¬¾",
420 "signup.link.login": "Already have an account, sign in?", 423 "signup.link.login": "已有账户?请登录",
421 "signup.password.label": "Password", 424 "signup.password.label": "密ç ",
422 "signup.submit.label": "Create account", 425 "signup.submit.label": "创建账户",
423 "tabs.item.confirmDeleteService": "Do you really want to delete the {serviceName} service?", 426 "tabs.item.confirmDeleteService": "你真的è¦åˆ é™¤ {serviceName} æœåŠ¡ä¹ˆï¼Ÿ",
424 "tabs.item.deleteService": "Delete service", 427 "tabs.item.deleteService": "删除æœåŠ¡",
425 "tabs.item.disableAudio": "Disable audio", 428 "tabs.item.disableAudio": "ç¦ç”¨éŸ³é¢‘",
426 "tabs.item.disableDarkMode": "Disable Dark mode", 429 "tabs.item.disableDarkMode": "ç¦ç”¨æš—色模å¼",
427 "tabs.item.disableNotifications": "Disable notifications", 430 "tabs.item.disableNotifications": "ç¦ç”¨é€šçŸ¥",
428 "tabs.item.disableService": "Disable service", 431 "tabs.item.disableService": "ç¦ç”¨æœåŠ¡",
429 "tabs.item.enableAudio": "Enable audio", 432 "tabs.item.enableAudio": "å¯ç”¨éŸ³é¢‘",
430 "tabs.item.enableDarkMode": "Enable Dark mode", 433 "tabs.item.enableDarkMode": "å¼€å¯æ·±è‰²ä¸»é¢˜",
431 "tabs.item.enableNotification": "Enable notifications", 434 "tabs.item.enableNotification": "å¼€å¯é€šçŸ¥",
432 "tabs.item.enableService": "Enable service", 435 "tabs.item.enableService": "å¯ç”¨æœåŠ¡",
433 "tabs.item.hibernateService": "Hibernate service", 436 "tabs.item.hibernateService": "休眠æœåŠ¡",
434 "tabs.item.reload": "é‡æ–°åŠ è½½", 437 "tabs.item.reload": "é‡æ–°åŠ è½½",
435 "tabs.item.wakeUpService": "Wake up service", 438 "tabs.item.wakeUpService": "唤醒æœåŠ¡",
436 "validation.email": "{field} is not valid", 439 "validation.email": "{field} 无效",
437 "validation.minLength": "{field} should be at least {length} characters long", 440 "validation.minLength": "{field} 的长度应该大于 {length} 个字符",
438 "validation.oneRequired": "At least one is required", 441 "validation.oneRequired": "至少需è¦ä¸€ä¸ª",
439 "validation.required": "{field} is required", 442 "validation.required": "{field} 为必填字段",
440 "validation.url": "{field} is not a valid URL", 443 "validation.url": "{field} ä¸æ˜¯ä¸€ä¸ªæœ‰æ•ˆçš„ URL",
441 "webControls.back": "返回", 444 "webControls.back": "返回",
442 "webControls.forward": "Forward", 445 "webControls.forward": "å‘å‰",
443 "webControls.goHome": "主页", 446 "webControls.goHome": "主页",
444 "webControls.openInBrowser": "Open in Browser", 447 "webControls.openInBrowser": "在æµè§ˆå™¨ä¸­æ‰“å¼€",
445 "webControls.reload": "é‡æ–°åŠ è½½", 448 "webControls.reload": "é‡æ–°åŠ è½½",
446 "welcome.loginButton": "Login to your account", 449 "welcome.loginButton": "登录您的账å·",
447 "welcome.signupButton": "Create a free account", 450 "welcome.signupButton": "创建å…费账户",
448 "workspaceDrawer.addNewWorkspaceLabel": "Add new workspace", 451 "workspaceDrawer.addNewWorkspaceLabel": "新建工作区…",
449 "workspaceDrawer.allServices": "All services", 452 "workspaceDrawer.allServices": "所有æœåŠ¡",
450 "workspaceDrawer.headline": "Workspaces", 453 "workspaceDrawer.headline": "工作区",
451 "workspaceDrawer.item.contextMenuEdit": "edit", 454 "workspaceDrawer.item.contextMenuEdit": "编辑",
452 "workspaceDrawer.item.noServicesAddedYet": "No services added yet", 455 "workspaceDrawer.item.noServicesAddedYet": "尚未添加任何æœåŠ¡",
453 "workspaceDrawer.workspaceFeatureInfo": "<p>Ferdi Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time.</p><p>You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.</p>", 456 "workspaceDrawer.workspaceFeatureInfo": "<p>Ferdi Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time.</p><p>You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.</p>",
454 "workspaceDrawer.workspacesSettingsTooltip": "Edit workspaces settings", 457 "workspaceDrawer.workspacesSettingsTooltip": "工作区设置",
455 "workspaces.switchingIndicator.switchingTo": "Switching to" 458 "workspaces.switchingIndicator.switchingTo": "切æ¢åˆ°"
456} 459}
diff --git a/src/index.js b/src/index.ts
index 9f384f5fa..b4a754289 100644
--- a/src/index.js
+++ b/src/index.ts
@@ -1,6 +1,13 @@
1/* eslint-disable import/first */ 1/* eslint-disable import/first */
2 2
3import { app, BrowserWindow, globalShortcut, ipcMain, session, dialog } from 'electron'; 3import {
4 app,
5 BrowserWindow,
6 globalShortcut,
7 ipcMain,
8 session,
9 dialog,
10} from 'electron';
4 11
5import { emptyDirSync, ensureFileSync } from 'fs-extra'; 12import { emptyDirSync, ensureFileSync } from 'fs-extra';
6import { join } from 'path'; 13import { join } from 'path';
@@ -13,12 +20,7 @@ initializeRemote();
13 20
14import { DEFAULT_APP_SETTINGS, DEFAULT_WINDOW_OPTIONS } from './config'; 21import { DEFAULT_APP_SETTINGS, DEFAULT_WINDOW_OPTIONS } from './config';
15 22
16import { 23import { isMac, isWindows, isLinux, altKey } from './environment';
17 isMac,
18 isWindows,
19 isLinux,
20 altKey,
21} from './environment';
22import { 24import {
23 isDevMode, 25 isDevMode,
24 aboutAppDetails, 26 aboutAppDetails,
@@ -34,7 +36,8 @@ import DBus from './lib/DBus';
34import Settings from './electron/Settings'; 36import Settings from './electron/Settings';
35import handleDeepLink from './electron/deepLinking'; 37import handleDeepLink from './electron/deepLinking';
36import { isPositionValid } from './electron/windowUtils'; 38import { isPositionValid } from './electron/windowUtils';
37import { appId } from './package.json'; // eslint-disable-line import/no-unresolved 39// @ts-expect-error Cannot find module './package.json' or its corresponding type declarations.
40import { appId } from './package.json';
38import './electron/exception'; 41import './electron/exception';
39 42
40import { asarPath } from './helpers/asar-helpers'; 43import { asarPath } from './helpers/asar-helpers';
@@ -49,13 +52,18 @@ app.userAgentFallback = userAgent();
49 52
50// Keep a global reference of the window object, if you don't, the window will 53// Keep a global reference of the window object, if you don't, the window will
51// be closed automatically when the JavaScript object is garbage collected. 54// be closed automatically when the JavaScript object is garbage collected.
52let mainWindow; 55let mainWindow: BrowserWindow | undefined;
53let willQuitApp = false; 56let willQuitApp = false;
54 57
55// Register methods to be called once the window has been loaded. 58// Register methods to be called once the window has been loaded.
56let onDidLoadFns = []; 59let onDidLoadFns: any[] | null = [];
57 60
58function onDidLoad(fn) { 61function onDidLoad(fn: {
62 (window: BrowserWindow): void;
63 (window: BrowserWindow): void;
64 (window: BrowserWindow): void;
65 (arg0: BrowserWindow): void;
66}) {
59 if (onDidLoadFns) { 67 if (onDidLoadFns) {
60 onDidLoadFns.push(fn); 68 onDidLoadFns.push(fn);
61 } else if (mainWindow) { 69 } else if (mainWindow) {
@@ -76,10 +84,10 @@ if (isWindows) {
76const settings = new Settings('app', DEFAULT_APP_SETTINGS); 84const settings = new Settings('app', DEFAULT_APP_SETTINGS);
77const proxySettings = new Settings('proxy'); 85const proxySettings = new Settings('proxy');
78 86
79const retrieveSettingValue = (key, defaultValue = true) => 87const retrieveSettingValue = (key: string, defaultValue: boolean) =>
80 ifUndefinedBoolean(settings.get(key), defaultValue); 88 ifUndefinedBoolean(settings.get(key), defaultValue);
81 89
82if (retrieveSettingValue('sentry')) { 90if (retrieveSettingValue('sentry', DEFAULT_APP_SETTINGS.sentry)) {
83 // eslint-disable-next-line global-require 91 // eslint-disable-next-line global-require
84 require('./sentry'); 92 require('./sentry');
85} 93}
@@ -96,7 +104,7 @@ const gotTheLock = liftSingleInstanceLock
96if (!gotTheLock) { 104if (!gotTheLock) {
97 app.quit(); 105 app.quit();
98} else { 106} else {
99 app.on('second-instance', (event, argv) => { 107 app.on('second-instance', (_event, argv) => {
100 // Someone tried to run a second instance, we should focus our window. 108 // Someone tried to run a second instance, we should focus our window.
101 if (mainWindow) { 109 if (mainWindow) {
102 if (!mainWindow.isVisible()) { 110 if (!mainWindow.isVisible()) {
@@ -108,7 +116,7 @@ if (!gotTheLock) {
108 mainWindow.focus(); 116 mainWindow.focus();
109 117
110 if (isWindows) { 118 if (isWindows) {
111 onDidLoad(window => { 119 onDidLoad((window: BrowserWindow) => {
112 // Keep only command line / deep linked arguments 120 // Keep only command line / deep linked arguments
113 const url = argv.slice(1); 121 const url = argv.slice(1);
114 if (url) { 122 if (url) {
@@ -145,6 +153,7 @@ if (!gotTheLock) {
145// https://github.com/electron/electron/issues/9046 153// https://github.com/electron/electron/issues/9046
146if ( 154if (
147 isLinux && 155 isLinux &&
156 process.env.XDG_CURRENT_DESKTOP &&
148 ['Pantheon', 'Unity:Unity7'].includes(process.env.XDG_CURRENT_DESKTOP) 157 ['Pantheon', 'Unity:Unity7'].includes(process.env.XDG_CURRENT_DESKTOP)
149) { 158) {
150 process.env.XDG_CURRENT_DESKTOP = 'Unity'; 159 process.env.XDG_CURRENT_DESKTOP = 'Unity';
@@ -196,16 +205,20 @@ const createWindow = () => {
196 frame: isLinux, 205 frame: isLinux,
197 backgroundColor, 206 backgroundColor,
198 webPreferences: { 207 webPreferences: {
199 spellcheck: retrieveSettingValue('enableSpellchecking'), 208 spellcheck: retrieveSettingValue(
209 'enableSpellchecking',
210 DEFAULT_APP_SETTINGS.enableSpellchecking,
211 ),
200 nodeIntegration: true, 212 nodeIntegration: true,
201 contextIsolation: false, 213 contextIsolation: false,
202 webviewTag: true, 214 webviewTag: true,
203 preload: join(__dirname, 'sentry.js'), 215 preload: join(__dirname, 'sentry.js'),
216 // @ts-expect-error Object literal may only specify known properties, and 'enableRemoteModule' does not exist in type 'WebPreferences'.
204 enableRemoteModule: true, 217 enableRemoteModule: true,
205 }, 218 },
206 }); 219 });
207 220
208 app.on('web-contents-created', (e, contents) => { 221 app.on('web-contents-created', (_e, contents) => {
209 if (contents.getType() === 'webview') { 222 if (contents.getType() === 'webview') {
210 contents.on('new-window', event => { 223 contents.on('new-window', event => {
211 event.preventDefault(); 224 event.preventDefault();
@@ -225,7 +238,7 @@ const createWindow = () => {
225 }); 238 });
226 239
227 // Initialize System Tray 240 // Initialize System Tray
228 const trayIcon = new Tray(); 241 const trayIcon: Tray = new Tray();
229 242
230 // Initialize DBus interface 243 // Initialize DBus interface
231 const dbus = new DBus(trayIcon); 244 const dbus = new DBus(trayIcon);
@@ -256,7 +269,7 @@ const createWindow = () => {
256 269
257 // Windows deep linking handling on app launch 270 // Windows deep linking handling on app launch
258 if (isWindows) { 271 if (isWindows) {
259 onDidLoad(window => { 272 onDidLoad((window: BrowserWindow) => {
260 const url = process.argv.slice(1); 273 const url = process.argv.slice(1);
261 if (url) { 274 if (url) {
262 handleDeepLink(window, url.toString()); 275 handleDeepLink(window, url.toString());
@@ -270,24 +283,35 @@ const createWindow = () => {
270 // Dereference the window object, usually you would store windows 283 // Dereference the window object, usually you would store windows
271 // in an array if your app supports multi windows, this is the time 284 // in an array if your app supports multi windows, this is the time
272 // when you should delete the corresponding element. 285 // when you should delete the corresponding element.
273 if (!willQuitApp && retrieveSettingValue('runInBackground')) { 286 if (
287 !willQuitApp &&
288 retrieveSettingValue(
289 'runInBackground',
290 DEFAULT_APP_SETTINGS.runInBackground,
291 )
292 ) {
274 e.preventDefault(); 293 e.preventDefault();
275 if (isWindows) { 294 if (isWindows) {
276 debug('Window: minimize'); 295 debug('Window: minimize');
277 mainWindow.minimize(); 296 mainWindow?.minimize();
278 297
279 if (retrieveSettingValue('closeToSystemTray')) { 298 if (
299 retrieveSettingValue(
300 'closeToSystemTray',
301 DEFAULT_APP_SETTINGS.closeToSystemTray,
302 )
303 ) {
280 debug('Skip taskbar: true'); 304 debug('Skip taskbar: true');
281 mainWindow.setSkipTaskbar(true); 305 mainWindow?.setSkipTaskbar(true);
282 } 306 }
283 } else if (isMac && mainWindow.isFullScreen()) { 307 } else if (isMac && mainWindow?.isFullScreen()) {
284 debug('Window: leaveFullScreen and hide'); 308 debug('Window: leaveFullScreen and hide');
285 mainWindow.once('show', () => mainWindow.setFullScreen(true)); 309 mainWindow.once('show', () => mainWindow?.setFullScreen(true));
286 mainWindow.once('leave-full-screen', () => mainWindow.hide()); 310 mainWindow.once('leave-full-screen', () => mainWindow?.hide());
287 mainWindow.setFullScreen(false); 311 mainWindow.setFullScreen(false);
288 } else { 312 } else {
289 debug('Window: hide'); 313 debug('Window: hide');
290 mainWindow.hide(); 314 mainWindow?.hide();
291 } 315 }
292 } else { 316 } else {
293 dbus.stop(); 317 dbus.stop();
@@ -298,35 +322,49 @@ const createWindow = () => {
298 // For Windows we need to store a flag to properly restore the window 322 // For Windows we need to store a flag to properly restore the window
299 // if the window was maximized before minimizing it so system tray 323 // if the window was maximized before minimizing it so system tray
300 mainWindow.on('minimize', () => { 324 mainWindow.on('minimize', () => {
325 // @ts-expect-error Property 'wasMaximized' does not exist on type 'App'.
301 app.wasMaximized = app.isMaximized; 326 app.wasMaximized = app.isMaximized;
302 327
303 if (retrieveSettingValue('minimizeToSystemTray')) { 328 if (
329 retrieveSettingValue(
330 'minimizeToSystemTray',
331 DEFAULT_APP_SETTINGS.minimizeToSystemTray,
332 )
333 ) {
304 debug('Skip taskbar: true'); 334 debug('Skip taskbar: true');
305 mainWindow.setSkipTaskbar(true); 335 mainWindow?.setSkipTaskbar(true);
306 trayIcon.show(); 336 trayIcon.show();
307 } 337 }
308 }); 338 });
309 339
310 mainWindow.on('maximize', () => { 340 mainWindow.on('maximize', () => {
311 debug('Window: maximize'); 341 debug('Window: maximize');
342 // @ts-expect-error Property 'isMaximized' does not exist on type 'App'.
312 app.isMaximized = true; 343 app.isMaximized = true;
313 }); 344 });
314 345
315 mainWindow.on('unmaximize', () => { 346 mainWindow.on('unmaximize', () => {
316 debug('Window: unmaximize'); 347 debug('Window: unmaximize');
348 // @ts-expect-error Property 'isMaximized' does not exist on type 'App'.
317 app.isMaximized = false; 349 app.isMaximized = false;
318 }); 350 });
319 351
320 mainWindow.on('restore', () => { 352 mainWindow.on('restore', () => {
321 debug('Window: restore'); 353 debug('Window: restore');
322 mainWindow.setSkipTaskbar(false); 354 mainWindow?.setSkipTaskbar(false);
323 355
356 // @ts-expect-error Property 'wasMaximized' does not exist on type 'App'.
324 if (app.wasMaximized) { 357 if (app.wasMaximized) {
325 debug('Window: was maximized before, maximize window'); 358 debug('Window: was maximized before, maximize window');
326 mainWindow.maximize(); 359 mainWindow?.maximize();
327 } 360 }
328 361
329 if (!retrieveSettingValue('enableSystemTray')) { 362 if (
363 !retrieveSettingValue(
364 'enableSystemTray',
365 DEFAULT_APP_SETTINGS.enableSystemTray,
366 )
367 ) {
330 debug('Tray: hiding tray icon'); 368 debug('Tray: hiding tray icon');
331 trayIcon.hide(); 369 trayIcon.hide();
332 } 370 }
@@ -340,10 +378,10 @@ const createWindow = () => {
340 378
341 mainWindow.on('show', () => { 379 mainWindow.on('show', () => {
342 debug('Skip taskbar: true'); 380 debug('Skip taskbar: true');
343 mainWindow.setSkipTaskbar(false); 381 mainWindow?.setSkipTaskbar(false);
344 }); 382 });
345 383
346 app.mainWindow = mainWindow; 384 // @ts-expect-error Property 'isMaximized' does not exist on type 'App'.
347 app.isMaximized = mainWindow.isMaximized(); 385 app.isMaximized = mainWindow.isMaximized();
348 386
349 mainWindow.webContents.on('new-window', (e, url) => { 387 mainWindow.webContents.on('new-window', (e, url) => {
@@ -351,17 +389,26 @@ const createWindow = () => {
351 openExternalUrl(url); 389 openExternalUrl(url);
352 }); 390 });
353 391
354 if (retrieveSettingValue('startMinimized', false)) { 392 if (
393 retrieveSettingValue('startMinimized', DEFAULT_APP_SETTINGS.startMinimized)
394 ) {
355 mainWindow.hide(); 395 mainWindow.hide();
356 } else { 396 } else {
357 mainWindow.show(); 397 mainWindow.show();
358 } 398 }
359 399
360 app.whenReady().then(() => { 400 app.whenReady().then(() => {
361 // Toggle the window on 'Alt+X' 401 if (
362 globalShortcut.register(`${altKey()}+X`, () => { 402 retrieveSettingValue(
363 trayIcon.trayMenuTemplate[0].click(); 403 'enableGlobalHideShortcut',
364 }); 404 DEFAULT_APP_SETTINGS.enableGlobalHideShortcut,
405 )
406 ) {
407 // Toggle the window on 'Alt+X'
408 globalShortcut.register(`${altKey()}+X`, () => {
409 trayIcon.trayMenuTemplate[0].click();
410 });
411 }
365 }); 412 });
366}; 413};
367 414
@@ -401,18 +448,18 @@ app.on('ready', () => {
401 448
402 // Register App URL 449 // Register App URL
403 const protocolClient = isDevMode ? 'ferdi-dev' : 'ferdi'; 450 const protocolClient = isDevMode ? 'ferdi-dev' : 'ferdi';
404 if (!app.isDefaultProtocolClient(protocolClient)) { 451 if (!app.isDefaultProtocolClient(protocolClient, process.execPath)) {
405 app.setAsDefaultProtocolClient(protocolClient); 452 app.setAsDefaultProtocolClient(protocolClient, process.execPath);
406 } 453 }
407 454
408 if (isWindows) { 455 if (isWindows) {
409 const extraArgs = isDevMode ? `${__dirname} ` : ''; 456 const extraArgs = isDevMode ? `${__dirname} ` : '';
410 const iconPath = asarPath( 457 const iconPath = asarPath(
411 join( 458 join(
412 isDevMode ? `${__dirname}../src/` : __dirname, 459 isDevMode ? `${__dirname}../src/` : __dirname,
413 'assets/images/taskbar/win32/display.ico', 460 'assets/images/taskbar/win32/display.ico',
414 ), 461 ),
415 ); 462 );
416 app.setUserTasks([ 463 app.setUserTasks([
417 { 464 {
418 program: process.execPath, 465 program: process.execPath,
@@ -428,7 +475,7 @@ app.on('ready', () => {
428 iconPath, 475 iconPath,
429 iconIndex: 0, 476 iconIndex: 0,
430 title: 'Quit Ferdi', 477 title: 'Quit Ferdi',
431 description: null, 478 description: '',
432 }, 479 },
433 ]); 480 ]);
434 } 481 }
@@ -444,26 +491,28 @@ app.on('ready', () => {
444const noop = () => null; 491const noop = () => null;
445let authCallback = noop; 492let authCallback = noop;
446 493
447app.on('login', (event, webContents, request, authInfo, callback) => { 494app.on('login', (event, _webContents, _request, authInfo, callback) => {
495 // @ts-expect-error Type '(username?: string | undefined, password?: string | undefined) => void' is not assignable to type '() => null'.
448 authCallback = callback; 496 authCallback = callback;
449 debug('browser login event', authInfo); 497 debug('browser login event', authInfo);
450 event.preventDefault(); 498 event.preventDefault();
451 499
452 if (!authInfo.isProxy && authInfo.scheme === 'basic') { 500 if (!authInfo.isProxy && authInfo.scheme === 'basic') {
453 debug('basic auth handler', authInfo); 501 debug('basic auth handler', authInfo);
454 basicAuthHandler(mainWindow, authInfo); 502 basicAuthHandler(mainWindow!, authInfo);
455 } 503 }
456}); 504});
457 505
458// TODO: evaluate if we need to store the authCallback for every service 506// TODO: evaluate if we need to store the authCallback for every service
459ipcMain.on('feature-basic-auth-credentials', (e, { user, password }) => { 507ipcMain.on('feature-basic-auth-credentials', (_e, { user, password }) => {
460 debug('Received basic auth credentials', user, '********'); 508 debug('Received basic auth credentials', user, '********');
461 509
510 // @ts-expect-error Expected 0 arguments, but got 2.
462 authCallback(user, password); 511 authCallback(user, password);
463 authCallback = noop; 512 authCallback = noop;
464}); 513});
465 514
466ipcMain.on('open-browser-window', (e, { url, serviceId }) => { 515ipcMain.on('open-browser-window', (_e, { url, serviceId }) => {
467 const serviceSession = session.fromPartition(`persist:service-${serviceId}`); 516 const serviceSession = session.fromPartition(`persist:service-${serviceId}`);
468 const child = new BrowserWindow({ 517 const child = new BrowserWindow({
469 parent: mainWindow, 518 parent: mainWindow,
@@ -481,7 +530,7 @@ ipcMain.on('open-browser-window', (e, { url, serviceId }) => {
481 530
482ipcMain.on( 531ipcMain.on(
483 'modifyRequestHeaders', 532 'modifyRequestHeaders',
484 (e, { modifiedRequestHeaders, serviceId }) => { 533 (_e, { modifiedRequestHeaders, serviceId }) => {
485 debug( 534 debug(
486 `Received modifyRequestHeaders ${modifiedRequestHeaders} for serviceId ${serviceId}`, 535 `Received modifyRequestHeaders ${modifiedRequestHeaders} for serviceId ${serviceId}`,
487 ); 536 );
@@ -502,7 +551,7 @@ ipcMain.on(
502 }, 551 },
503); 552);
504 553
505ipcMain.on('knownCertificateHosts', (e, { knownHosts, serviceId }) => { 554ipcMain.on('knownCertificateHosts', (_e, { knownHosts, serviceId }) => {
506 debug( 555 debug(
507 `Received knownCertificateHosts ${knownHosts} for serviceId ${serviceId}`, 556 `Received knownCertificateHosts ${knownHosts} for serviceId ${serviceId}`,
508 ); 557 );
@@ -511,7 +560,10 @@ ipcMain.on('knownCertificateHosts', (e, { knownHosts, serviceId }) => {
511 .setCertificateVerifyProc((request, callback) => { 560 .setCertificateVerifyProc((request, callback) => {
512 // To know more about these callbacks: https://www.electronjs.org/docs/api/session#sessetcertificateverifyprocproc 561 // To know more about these callbacks: https://www.electronjs.org/docs/api/session#sessetcertificateverifyprocproc
513 const { hostname } = request; 562 const { hostname } = request;
514 if (knownHosts.find(item => item.includes(hostname)).length > 0) { 563 if (
564 knownHosts.find((item: string | string[]) => item.includes(hostname))
565 .length > 0
566 ) {
515 callback(0); 567 callback(0);
516 } else { 568 } else {
517 callback(-2); 569 callback(-2);
@@ -522,6 +574,7 @@ ipcMain.on('knownCertificateHosts', (e, { knownHosts, serviceId }) => {
522ipcMain.on('feature-basic-auth-cancel', () => { 574ipcMain.on('feature-basic-auth-cancel', () => {
523 debug('Cancel basic auth'); 575 debug('Cancel basic auth');
524 576
577 // @ts-expect-error Expected 0 arguments, but got 2.
525 authCallback(null); 578 authCallback(null);
526 authCallback = noop; 579 authCallback = noop;
527}); 580});
@@ -530,7 +583,7 @@ ipcMain.on('feature-basic-auth-cancel', () => {
530 583
531ipcMain.on('find-in-page', (e, text, options) => { 584ipcMain.on('find-in-page', (e, text, options) => {
532 const { sender: webContents } = e; 585 const { sender: webContents } = e;
533 if (webContents !== mainWindow.webContents && typeof text === 'string') { 586 if (webContents !== mainWindow?.webContents && typeof text === 'string') {
534 const sanitizedOptions = {}; 587 const sanitizedOptions = {};
535 for (const option of ['forward', 'findNext', 'matchCase']) { 588 for (const option of ['forward', 'findNext', 'matchCase']) {
536 if (option in options) { 589 if (option in options) {
@@ -547,7 +600,7 @@ ipcMain.on('find-in-page', (e, text, options) => {
547 600
548ipcMain.on('stop-find-in-page', (e, action) => { 601ipcMain.on('stop-find-in-page', (e, action) => {
549 const { sender: webContents } = e; 602 const { sender: webContents } = e;
550 if (webContents !== mainWindow.webContents) { 603 if (webContents !== mainWindow?.webContents) {
551 const validActions = [ 604 const validActions = [
552 'clearSelection', 605 'clearSelection',
553 'keepSelection', 606 'keepSelection',
@@ -560,7 +613,7 @@ ipcMain.on('stop-find-in-page', (e, action) => {
560 e.returnValue = null; 613 e.returnValue = null;
561}); 614});
562 615
563ipcMain.on('set-spellchecker-locales', (e, { locale, serviceId }) => { 616ipcMain.on('set-spellchecker-locales', (_e, { locale, serviceId }) => {
564 if (serviceId === undefined) { 617 if (serviceId === undefined) {
565 return; 618 return;
566 } 619 }
@@ -578,7 +631,12 @@ ipcMain.on('set-spellchecker-locales', (e, { locale, serviceId }) => {
578app.on('window-all-closed', () => { 631app.on('window-all-closed', () => {
579 // On OS X it is common for applications and their menu bar 632 // On OS X it is common for applications and their menu bar
580 // to stay active until the user quits explicitly with Cmd + Q 633 // to stay active until the user quits explicitly with Cmd + Q
581 if (retrieveSettingValue('runInBackground')) { 634 if (
635 retrieveSettingValue(
636 'runInBackground',
637 DEFAULT_APP_SETTINGS.runInBackground,
638 )
639 ) {
582 debug('Window: all windows closed, quit app'); 640 debug('Window: all windows closed, quit app');
583 app.quit(); 641 app.quit();
584 } else { 642 } else {
@@ -589,8 +647,10 @@ app.on('window-all-closed', () => {
589app.on('before-quit', event => { 647app.on('before-quit', event => {
590 const yesButtonIndex = 0; 648 const yesButtonIndex = 0;
591 let selection = yesButtonIndex; 649 let selection = yesButtonIndex;
592 if (retrieveSettingValue('confirmOnQuit')) { 650 if (
593 selection = dialog.showMessageBoxSync(app.mainWindow, { 651 retrieveSettingValue('confirmOnQuit', DEFAULT_APP_SETTINGS.confirmOnQuit)
652 ) {
653 selection = dialog.showMessageBoxSync(mainWindow!, {
594 type: 'question', 654 type: 'question',
595 message: 'Quit', 655 message: 'Quit',
596 detail: 'Do you really want to quit Ferdi?', 656 detail: 'Do you really want to quit Ferdi?',
@@ -610,12 +670,12 @@ app.on('activate', () => {
610 if (mainWindow === null) { 670 if (mainWindow === null) {
611 createWindow(); 671 createWindow();
612 } else { 672 } else {
613 mainWindow.show(); 673 mainWindow?.show();
614 } 674 }
615}); 675});
616 676
617app.on('web-contents-created', (createdEvent, contents) => { 677app.on('web-contents-created', (_createdEvent, contents) => {
618 contents.on('new-window', (event, url, frameNme, disposition) => { 678 contents.on('new-window', (event, _url, _frameNme, disposition) => {
619 if (disposition === 'foreground-tab') event.preventDefault(); 679 if (disposition === 'foreground-tab') event.preventDefault();
620 }); 680 });
621}); 681});
@@ -625,7 +685,7 @@ app.on('will-finish-launching', () => {
625 app.on('open-url', (event, url) => { 685 app.on('open-url', (event, url) => {
626 event.preventDefault(); 686 event.preventDefault();
627 687
628 onDidLoad(window => { 688 onDidLoad((window: BrowserWindow) => {
629 debug('open-url event', url); 689 debug('open-url event', url);
630 handleDeepLink(window, url); 690 handleDeepLink(window, url);
631 }); 691 });
diff --git a/src/internal-server/app/Controllers/Http/RecipeController.js b/src/internal-server/app/Controllers/Http/RecipeController.js
index d44839db1..1b0ac7035 100644
--- a/src/internal-server/app/Controllers/Http/RecipeController.js
+++ b/src/internal-server/app/Controllers/Http/RecipeController.js
@@ -56,7 +56,6 @@ class RecipeController {
56 let remoteResults = []; 56 let remoteResults = [];
57 // eslint-disable-next-line eqeqeq 57 // eslint-disable-next-line eqeqeq
58 if (Env.get('CONNECT_WITH_FRANZ') == 'true') { 58 if (Env.get('CONNECT_WITH_FRANZ') == 'true') {
59 // eslint-disable-line eqeqeq
60 remoteResults = JSON.parse( 59 remoteResults = JSON.parse(
61 await ( 60 await (
62 await fetch( 61 await fetch(
@@ -112,7 +111,6 @@ class RecipeController {
112 } 111 }
113 // eslint-disable-next-line eqeqeq 112 // eslint-disable-next-line eqeqeq
114 if (Env.get('CONNECT_WITH_FRANZ') == 'true') { 113 if (Env.get('CONNECT_WITH_FRANZ') == 'true') {
115 // eslint-disable-line eqeqeq
116 return response.redirect(`${RECIPES_URL}/download/${service}`); 114 return response.redirect(`${RECIPES_URL}/download/${service}`);
117 } 115 }
118 return response.status(400).send({ 116 return response.status(400).send({
diff --git a/src/internal-server/app/Controllers/Http/ServiceController.js b/src/internal-server/app/Controllers/Http/ServiceController.js
index 133473b68..dedb5a12b 100644
--- a/src/internal-server/app/Controllers/Http/ServiceController.js
+++ b/src/internal-server/app/Controllers/Http/ServiceController.js
@@ -36,7 +36,7 @@ class ServiceController {
36 } while ( 36 } while (
37 (await Service.query().where('serviceId', serviceId).fetch()).rows 37 (await Service.query().where('serviceId', serviceId).fetch()).rows
38 .length > 0 38 .length > 0
39 ); // eslint-disable-line no-await-in-loop 39 );
40 40
41 await Service.create({ 41 await Service.create({
42 serviceId, 42 serviceId,
diff --git a/src/internal-server/app/Controllers/Http/UserController.js b/src/internal-server/app/Controllers/Http/UserController.js
index 25b5277bd..2ecc8241c 100644
--- a/src/internal-server/app/Controllers/Http/UserController.js
+++ b/src/internal-server/app/Controllers/Http/UserController.js
@@ -171,7 +171,6 @@ class UserController {
171 return response.status(401).send(errorMessage); 171 return response.status(401).send(errorMessage);
172 } 172 }
173 173
174 // eslint-disable-next-line prefer-destructuring
175 token = content.token; 174 token = content.token;
176 } catch (error) { 175 } catch (error) {
177 return response.status(401).send({ 176 return response.status(401).send({
@@ -300,7 +299,7 @@ class UserController {
300 } while ( 299 } while (
301 (await Workspace.query().where('workspaceId', newWorkspaceId).fetch()) 300 (await Workspace.query().where('workspaceId', newWorkspaceId).fetch())
302 .rows.length > 0 301 .rows.length > 0
303 ); // eslint-disable-line no-await-in-loop 302 );
304 303
305 if ( 304 if (
306 workspace.services && 305 workspace.services &&
@@ -340,7 +339,7 @@ class UserController {
340 } while ( 339 } while (
341 (await Service.query().where('serviceId', newServiceId).fetch()).rows 340 (await Service.query().where('serviceId', newServiceId).fetch()).rows
342 .length > 0 341 .length > 0
343 ); // eslint-disable-line no-await-in-loop 342 );
344 343
345 // store the old serviceId as the key for future lookup 344 // store the old serviceId as the key for future lookup
346 serviceIdTranslation[service.serviceId] = newServiceId; 345 serviceIdTranslation[service.serviceId] = newServiceId;
diff --git a/src/internal-server/app/Controllers/Http/WorkspaceController.js b/src/internal-server/app/Controllers/Http/WorkspaceController.js
index 9d461135e..528721f13 100644
--- a/src/internal-server/app/Controllers/Http/WorkspaceController.js
+++ b/src/internal-server/app/Controllers/Http/WorkspaceController.js
@@ -27,7 +27,7 @@ class WorkspaceController {
27 } while ( 27 } while (
28 (await Workspace.query().where('workspaceId', workspaceId).fetch()).rows 28 (await Workspace.query().where('workspaceId', workspaceId).fetch()).rows
29 .length > 0 29 .length > 0
30 ); // eslint-disable-line no-await-in-loop 30 );
31 31
32 const order = (await Workspace.all()).rows.length; 32 const order = (await Workspace.all()).rows.length;
33 const { name } = data; 33 const { name } = data;
diff --git a/src/internal-server/config/app.js b/src/internal-server/config/app.js
index 0a1644932..379190734 100644
--- a/src/internal-server/config/app.js
+++ b/src/internal-server/config/app.js
@@ -2,7 +2,6 @@
2const Env = use('Env'); 2const Env = use('Env');
3 3
4module.exports = { 4module.exports = {
5
6 /* 5 /*
7 |-------------------------------------------------------------------------- 6 |--------------------------------------------------------------------------
8 | Application Name 7 | Application Name
diff --git a/src/internal-server/config/bodyParser.js b/src/internal-server/config/bodyParser.js
index 8a5406f9e..ef2eedf40 100644
--- a/src/internal-server/config/bodyParser.js
+++ b/src/internal-server/config/bodyParser.js
@@ -58,9 +58,7 @@ module.exports = {
58 | 58 |
59 */ 59 */
60 raw: { 60 raw: {
61 types: [ 61 types: ['text/*'],
62 'text/*',
63 ],
64 }, 62 },
65 63
66 /* 64 /*
@@ -72,9 +70,7 @@ module.exports = {
72 | 70 |
73 */ 71 */
74 form: { 72 form: {
75 types: [ 73 types: ['application/x-www-form-urlencoded'],
76 'application/x-www-form-urlencoded',
77 ],
78 }, 74 },
79 75
80 /* 76 /*
@@ -86,9 +82,7 @@ module.exports = {
86 | 82 |
87 */ 83 */
88 files: { 84 files: {
89 types: [ 85 types: ['multipart/form-data'],
90 'multipart/form-data',
91 ],
92 86
93 /* 87 /*
94 |-------------------------------------------------------------------------- 88 |--------------------------------------------------------------------------
diff --git a/src/internal-server/database/migrations/1503250034279_user.js b/src/internal-server/database/migrations/1503250034279_user.js
index 80b49020a..d502e4fa0 100644
--- a/src/internal-server/database/migrations/1503250034279_user.js
+++ b/src/internal-server/database/migrations/1503250034279_user.js
@@ -3,7 +3,7 @@ const Schema = use('Schema');
3 3
4class UserSchema extends Schema { 4class UserSchema extends Schema {
5 up() { 5 up() {
6 this.create('users', (table) => { 6 this.create('users', table => {
7 table.increments(); 7 table.increments();
8 table.json('settings'); 8 table.json('settings');
9 table.timestamps(); 9 table.timestamps();
diff --git a/src/internal-server/database/migrations/1566385379883_service_schema.js b/src/internal-server/database/migrations/1566385379883_service_schema.js
index d887ef193..d8087248f 100644
--- a/src/internal-server/database/migrations/1566385379883_service_schema.js
+++ b/src/internal-server/database/migrations/1566385379883_service_schema.js
@@ -3,7 +3,7 @@ const Schema = use('Schema');
3 3
4class ServiceSchema extends Schema { 4class ServiceSchema extends Schema {
5 up() { 5 up() {
6 this.create('services', (table) => { 6 this.create('services', table => {
7 table.increments(); 7 table.increments();
8 table.string('serviceId', 80).notNullable(); 8 table.string('serviceId', 80).notNullable();
9 table.string('name', 80).notNullable(); 9 table.string('name', 80).notNullable();
diff --git a/src/internal-server/database/migrations/1566554231482_recipe_schema.js b/src/internal-server/database/migrations/1566554231482_recipe_schema.js
index 514d57600..41bebdacb 100644
--- a/src/internal-server/database/migrations/1566554231482_recipe_schema.js
+++ b/src/internal-server/database/migrations/1566554231482_recipe_schema.js
@@ -3,7 +3,7 @@ const Schema = use('Schema');
3 3
4class RecipeSchema extends Schema { 4class RecipeSchema extends Schema {
5 up() { 5 up() {
6 this.create('recipes', (table) => { 6 this.create('recipes', table => {
7 table.increments(); 7 table.increments();
8 table.string('name', 80).notNullable(); 8 table.string('name', 80).notNullable();
9 table.string('recipeId', 254).notNullable().unique(); 9 table.string('recipeId', 254).notNullable().unique();
diff --git a/src/internal-server/database/migrations/1566554359294_workspace_schema.js b/src/internal-server/database/migrations/1566554359294_workspace_schema.js
index 421a406b5..3e5385f45 100644
--- a/src/internal-server/database/migrations/1566554359294_workspace_schema.js
+++ b/src/internal-server/database/migrations/1566554359294_workspace_schema.js
@@ -3,7 +3,7 @@ const Schema = use('Schema');
3 3
4class WorkspaceSchema extends Schema { 4class WorkspaceSchema extends Schema {
5 up() { 5 up() {
6 this.create('workspaces', (table) => { 6 this.create('workspaces', table => {
7 table.increments(); 7 table.increments();
8 table.string('workspaceId', 80).notNullable().unique(); 8 table.string('workspaceId', 80).notNullable().unique();
9 table.string('name', 80).notNullable(); 9 table.string('name', 80).notNullable();
diff --git a/src/internal-server/start.ts b/src/internal-server/start.ts
index 392f7cf41..62311b21e 100644
--- a/src/internal-server/start.ts
+++ b/src/internal-server/start.ts
@@ -17,27 +17,27 @@
17 17
18import fold from '@adonisjs/fold'; 18import fold from '@adonisjs/fold';
19import { Ignitor } from '@adonisjs/ignitor'; 19import { Ignitor } from '@adonisjs/ignitor';
20import fs from 'fs-extra'; 20import { existsSync, readFile, statSync, chmodSync, writeFile } from 'fs-extra';
21import path from 'path'; 21import { join } from 'path';
22import { LOCAL_HOSTNAME } from '../config'; 22import { LOCAL_HOSTNAME } from '../config';
23import { isWindows } from '../environment'; 23import { isWindows } from '../environment';
24 24
25process.env.ENV_PATH = path.join(__dirname, 'env.ini'); 25process.env.ENV_PATH = join(__dirname, 'env.ini');
26 26
27export const server = async (userPath: string, port: number) => { 27export const server = async (userPath: string, port: number) => {
28 const dbPath = path.join(userPath, 'server.sqlite'); 28 const dbPath = join(userPath, 'server.sqlite');
29 const dbTemplatePath = path.join(__dirname, 'database', 'template.sqlite'); 29 const dbTemplatePath = join(__dirname, 'database', 'template.sqlite');
30 30
31 if (!fs.existsSync(dbPath)) { 31 if (!existsSync(dbPath)) {
32 // Manually copy file 32 // Manually copy file
33 // We can't use copyFile here as it will cause the file to be readonly on Windows 33 // We can't use copyFile here as it will cause the file to be readonly on Windows
34 const dbTemplate = await fs.readFile(dbTemplatePath); 34 const dbTemplate = await readFile(dbTemplatePath);
35 await fs.writeFile(dbPath, dbTemplate); 35 await writeFile(dbPath, dbTemplate);
36 36
37 // Change permissions to ensure to file is not read-only 37 // Change permissions to ensure to file is not read-only
38 if (isWindows) { 38 if (isWindows) {
39 // eslint-disable-next-line no-bitwise 39 // eslint-disable-next-line no-bitwise
40 fs.chmodSync(dbPath, fs.statSync(dbPath).mode | 146); 40 chmodSync(dbPath, statSync(dbPath).mode | 146);
41 } 41 }
42 } 42 }
43 43
diff --git a/src/internal-server/start/app.js b/src/internal-server/start/app.js
index 8b1a49f57..7ca544085 100644
--- a/src/internal-server/start/app.js
+++ b/src/internal-server/start/app.js
@@ -28,9 +28,7 @@ const providers = [
28| Providers for migrations, tests etc. 28| Providers for migrations, tests etc.
29| 29|
30*/ 30*/
31const aceProviders = [ 31const aceProviders = ['@adonisjs/lucid/providers/MigrationsProvider'];
32 '@adonisjs/lucid/providers/MigrationsProvider',
33];
34 32
35/* 33/*
36|-------------------------------------------------------------------------- 34|--------------------------------------------------------------------------
@@ -57,5 +55,8 @@ const aliases = {};
57const commands = []; 55const commands = [];
58 56
59module.exports = { 57module.exports = {
60 providers, aceProviders, aliases, commands, 58 providers,
59 aceProviders,
60 aliases,
61 commands,
61}; 62};
diff --git a/src/internal-server/start/routes.js b/src/internal-server/start/routes.js
index db74479c9..50b9448cf 100644
--- a/src/internal-server/start/routes.js
+++ b/src/internal-server/start/routes.js
@@ -66,7 +66,6 @@ Route.group(() => {
66 // Static responses 66 // Static responses
67 Route.get('features/:mode?', 'StaticController.features'); 67 Route.get('features/:mode?', 'StaticController.features');
68 Route.get('services', 'StaticController.emptyArray'); 68 Route.get('services', 'StaticController.emptyArray');
69 Route.get('news', 'StaticController.emptyArray');
70}) 69})
71 .prefix(API_VERSION) 70 .prefix(API_VERSION)
72 .middleware(OnlyAllowFerdi); 71 .middleware(OnlyAllowFerdi);
diff --git a/src/internal-server/test.ts b/src/internal-server/test.ts
index 437a2813b..46d1b1e4a 100644
--- a/src/internal-server/test.ts
+++ b/src/internal-server/test.ts
@@ -1,9 +1,9 @@
1import path from 'path'; 1import { join } from 'path';
2import fs from 'fs-extra'; 2import { ensureDirSync } from 'fs-extra';
3import { server } from './start'; 3import { server } from './start';
4 4
5const dummyUserFolder = path.join(__dirname, 'user_data'); 5const dummyUserFolder = join(__dirname, 'user_data');
6 6
7fs.ensureDirSync(dummyUserFolder); 7ensureDirSync(dummyUserFolder);
8 8
9server(dummyUserFolder, 45_568); 9server(dummyUserFolder, 45_568);
diff --git a/src/jsUtils.ts b/src/jsUtils.ts
index b1baad7c5..d7ea4eb40 100644
--- a/src/jsUtils.ts
+++ b/src/jsUtils.ts
@@ -1,3 +1,14 @@
1export const ifUndefinedString = (source: string | undefined | null, defaultValue: string): string => (source !== undefined && source !== null ? source : defaultValue); 1export const ifUndefinedString = (
2export const ifUndefinedBoolean = (source: boolean | undefined | null, defaultValue: boolean): boolean => Boolean(source !== undefined && source !== null ? source : defaultValue); 2 source: string | undefined | null,
3export const ifUndefinedNumber = (source: number | undefined | null, defaultValue: number): number => Number(source !== undefined && source !== null ? source : defaultValue); 3 defaultValue: string,
4): string => (source !== undefined && source !== null ? source : defaultValue);
5export const ifUndefinedBoolean = (
6 source: boolean | undefined | null,
7 defaultValue: boolean,
8): boolean =>
9 Boolean(source !== undefined && source !== null ? source : defaultValue);
10export const ifUndefinedNumber = (
11 source: number | undefined | null,
12 defaultValue: number,
13): number =>
14 Number(source !== undefined && source !== null ? source : defaultValue);
diff --git a/src/lib/Menu.js b/src/lib/Menu.js
index cd86f1669..b1cbaf992 100644
--- a/src/lib/Menu.js
+++ b/src/lib/Menu.js
@@ -20,7 +20,7 @@ import {
20 addNewServiceShortcutKey, 20 addNewServiceShortcutKey,
21 muteFerdiShortcutKey, 21 muteFerdiShortcutKey,
22} from '../environment'; 22} from '../environment';
23import { aboutAppDetails } from '../environment-remote'; 23import { aboutAppDetails, ferdiVersion } from '../environment-remote';
24import { todosStore } from '../features/todos'; 24import { todosStore } from '../features/todos';
25import { todoActions } from '../features/todos/actions'; 25import { todoActions } from '../features/todos/actions';
26import { workspaceActions } from '../features/workspaces/actions'; 26import { workspaceActions } from '../features/workspaces/actions';
@@ -515,7 +515,7 @@ const _titleBarTemplateFactory = (intl, locked) => [
515 label: intl.formatMessage(menuItems.changelog), 515 label: intl.formatMessage(menuItems.changelog),
516 click() { 516 click() {
517 openExternalUrl( 517 openExternalUrl(
518 `${GITHUB_FERDI_URL}/ferdi/blob/develop/CHANGELOG.md`, 518 `${GITHUB_FERDI_URL}/ferdi/releases/tag/v${ferdiVersion}`,
519 true, 519 true,
520 ); 520 );
521 }, 521 },
diff --git a/src/lib/Tray.js b/src/lib/Tray.js
index 7360611cd..e7afc3552 100644
--- a/src/lib/Tray.js
+++ b/src/lib/Tray.js
@@ -1,5 +1,11 @@
1import { 1import {
2 app, Menu, nativeImage, nativeTheme, systemPreferences, Tray, ipcMain, 2 app,
3 Menu,
4 nativeImage,
5 nativeTheme,
6 systemPreferences,
7 Tray,
8 ipcMain,
3} from 'electron'; 9} from 'electron';
4import { join } from 'path'; 10import { join } from 'path';
5import macosVersion from 'macos-version'; 11import macosVersion from 'macos-version';
@@ -65,7 +71,9 @@ export default class TrayIcon {
65 71
66 if (appSettings.type === 'app') { 72 if (appSettings.type === 'app') {
67 const { isAppMuted } = appSettings.data; 73 const { isAppMuted } = appSettings.data;
68 this.trayMenuTemplate[1].label = isAppMuted ? 'Enable Notifications && Audio' : 'Disable Notifications && Audio'; 74 this.trayMenuTemplate[1].label = isAppMuted
75 ? 'Enable Notifications && Audio'
76 : 'Disable Notifications && Audio';
69 this.trayMenu = Menu.buildFromTemplate(this.trayMenuTemplate); 77 this.trayMenu = Menu.buildFromTemplate(this.trayMenuTemplate);
70 if (isLinux) { 78 if (isLinux) {
71 this.trayIcon.setContextMenu(this.trayMenu); 79 this.trayIcon.setContextMenu(this.trayMenu);
@@ -108,9 +116,12 @@ export default class TrayIcon {
108 } 116 }
109 117
110 if (isMac) { 118 if (isMac) {
111 this.themeChangeSubscriberId = systemPreferences.subscribeNotification('AppleInterfaceThemeChangedNotification', () => { 119 this.themeChangeSubscriberId = systemPreferences.subscribeNotification(
112 this._refreshIcon(); 120 'AppleInterfaceThemeChangedNotification',
113 }); 121 () => {
122 this._refreshIcon();
123 },
124 );
114 } 125 }
115 } 126 }
116 127
@@ -150,7 +161,8 @@ export default class TrayIcon {
150 _getAssetFromIndicator(indicator) { 161 _getAssetFromIndicator(indicator) {
151 if (indicator === '•') { 162 if (indicator === '•') {
152 return INDICATOR_TRAY_INDIRECT; 163 return INDICATOR_TRAY_INDIRECT;
153 } if (indicator !== 0) { 164 }
165 if (indicator !== 0) {
154 return INDICATOR_TRAY_UNREAD; 166 return INDICATOR_TRAY_UNREAD;
155 } 167 }
156 return INDICATOR_TRAY_PLAIN; 168 return INDICATOR_TRAY_PLAIN;
@@ -159,11 +171,16 @@ export default class TrayIcon {
159 _refreshIcon() { 171 _refreshIcon() {
160 if (!this.trayIcon) return; 172 if (!this.trayIcon) return;
161 173
162 this.trayIcon.setImage(this._getAsset('tray', this._getAssetFromIndicator(this.indicator))); 174 this.trayIcon.setImage(
175 this._getAsset('tray', this._getAssetFromIndicator(this.indicator)),
176 );
163 177
164 if (isMac) { 178 if (isMac) {
165 this.trayIcon.setPressedImage( 179 this.trayIcon.setPressedImage(
166 this._getAsset('tray', `${this._getAssetFromIndicator(this.indicator)}-active`), 180 this._getAsset(
181 'tray',
182 `${this._getAssetFromIndicator(this.indicator)}-active`,
183 ),
167 ); 184 );
168 } 185 }
169 } 186 }
@@ -171,12 +188,24 @@ export default class TrayIcon {
171 _getAsset(type, asset) { 188 _getAsset(type, asset) {
172 let { platform } = process; 189 let { platform } = process;
173 190
174 if (isMac && (nativeTheme.shouldUseDarkColors || macosVersion.isGreaterThanOrEqualTo('11'))) { 191 if (
192 isMac &&
193 (nativeTheme.shouldUseDarkColors ||
194 macosVersion.isGreaterThanOrEqualTo('11'))
195 ) {
175 platform = `${platform}-dark`; 196 platform = `${platform}-dark`;
176 } 197 }
177 198
178 return nativeImage.createFromPath(join( 199 return nativeImage.createFromPath(
179 __dirname, '..', 'assets', 'images', type, platform, `${asset}.${FILE_EXTENSION}`, 200 join(
180 )); 201 __dirname,
202 '..',
203 'assets',
204 'images',
205 type,
206 platform,
207 `${asset}.${FILE_EXTENSION}`,
208 ),
209 );
181 } 210 }
182} 211}
diff --git a/src/models/News.ts b/src/models/News.ts
deleted file mode 100644
index 4fc21f590..000000000
--- a/src/models/News.ts
+++ /dev/null
@@ -1,33 +0,0 @@
1import { ifUndefinedString, ifUndefinedBoolean } from '../jsUtils';
2
3interface INews {
4 id: string;
5 message: string;
6 type: string;
7 sticky: boolean | undefined;
8}
9
10export default class News {
11 id: string = '';
12
13 message: string = '';
14
15 type: string = 'primary';
16
17 sticky: boolean = false;
18
19 constructor(data: INews) {
20 if (!data) {
21 throw new Error('News config not valid');
22 }
23
24 if (!data.id) {
25 throw new Error('News requires Id');
26 }
27
28 this.id = data.id;
29 this.message = ifUndefinedString(data.message, this.message);
30 this.type = ifUndefinedString(data.type, this.type);
31 this.sticky = ifUndefinedBoolean(data.sticky, this.sticky);
32 }
33}
diff --git a/src/models/Recipe.ts b/src/models/Recipe.ts
index 859c75df0..959b43fd9 100644
--- a/src/models/Recipe.ts
+++ b/src/models/Recipe.ts
@@ -1,10 +1,8 @@
1import semver from 'semver'; 1import semver from 'semver';
2import { pathExistsSync } from 'fs-extra'; 2import { pathExistsSync } from 'fs-extra';
3import { join } from 'path'; 3import { join } from 'path';
4import { 4import { DEFAULT_SERVICE_SETTINGS } from '../config';
5 ifUndefinedString, 5import { ifUndefinedString, ifUndefinedBoolean } from '../jsUtils';
6 ifUndefinedBoolean,
7} from '../jsUtils';
8 6
9interface IRecipe { 7interface IRecipe {
10 id: string; 8 id: string;
@@ -30,20 +28,6 @@ interface IRecipe {
30 }; 28 };
31} 29}
32 30
33// Note: Do NOT change these default values. If they change, then the corresponding changes in the recipes needs to be done
34// TODO: Need to reconcile other properties
35const DEFAULT_RECIPE_SETTINGS = {
36 hasDirectMessages: true,
37 hasIndirectMessages: false,
38 hasNotificationSound: false,
39 hasTeamId: false,
40 hasCustomUrl: false,
41 hasHostedOption: false,
42 allowFavoritesDelineationInUnreadCount: false,
43 disablewebsecurity: false,
44 autoHibernate: false,
45};
46
47export default class Recipe { 31export default class Recipe {
48 id: string = ''; 32 id: string = '';
49 33
@@ -57,17 +41,17 @@ export default class Recipe {
57 41
58 serviceURL: string = ''; 42 serviceURL: string = '';
59 43
60 hasDirectMessages: boolean = DEFAULT_RECIPE_SETTINGS.hasDirectMessages; 44 hasDirectMessages: boolean = DEFAULT_SERVICE_SETTINGS.hasDirectMessages;
61 45
62 hasIndirectMessages: boolean = DEFAULT_RECIPE_SETTINGS.hasIndirectMessages; 46 hasIndirectMessages: boolean = DEFAULT_SERVICE_SETTINGS.hasIndirectMessages;
63 47
64 hasNotificationSound: boolean = DEFAULT_RECIPE_SETTINGS.hasNotificationSound; 48 hasNotificationSound: boolean = DEFAULT_SERVICE_SETTINGS.hasNotificationSound;
65 49
66 hasTeamId: boolean = DEFAULT_RECIPE_SETTINGS.hasTeamId; 50 hasTeamId: boolean = DEFAULT_SERVICE_SETTINGS.hasTeamId;
67 51
68 hasCustomUrl: boolean = DEFAULT_RECIPE_SETTINGS.hasCustomUrl; 52 hasCustomUrl: boolean = DEFAULT_SERVICE_SETTINGS.hasCustomUrl;
69 53
70 hasHostedOption: boolean = DEFAULT_RECIPE_SETTINGS.hasHostedOption; 54 hasHostedOption: boolean = DEFAULT_SERVICE_SETTINGS.hasHostedOption;
71 55
72 urlInputPrefix: string = ''; 56 urlInputPrefix: string = '';
73 57
@@ -75,12 +59,13 @@ export default class Recipe {
75 59
76 message: string = ''; 60 message: string = '';
77 61
78 allowFavoritesDelineationInUnreadCount: boolean = DEFAULT_RECIPE_SETTINGS.allowFavoritesDelineationInUnreadCount; 62 allowFavoritesDelineationInUnreadCount: boolean =
63 DEFAULT_SERVICE_SETTINGS.allowFavoritesDelineationInUnreadCount;
79 64
80 disablewebsecurity: boolean = DEFAULT_RECIPE_SETTINGS.disablewebsecurity; 65 disablewebsecurity: boolean = DEFAULT_SERVICE_SETTINGS.disablewebsecurity;
81 66
82 // TODO: Is this even used? 67 // TODO: Is this even used?
83 autoHibernate: boolean = DEFAULT_RECIPE_SETTINGS.autoHibernate; 68 autoHibernate: boolean = DEFAULT_SERVICE_SETTINGS.autoHibernate;
84 69
85 path: string = ''; 70 path: string = '';
86 71
@@ -98,7 +83,9 @@ export default class Recipe {
98 } 83 }
99 84
100 if (!semver.valid(data.version)) { 85 if (!semver.valid(data.version)) {
101 throw new Error(`Version ${data.version} of recipe '${data.name}' is not a valid semver version`); 86 throw new Error(
87 `Version ${data.version} of recipe '${data.name}' is not a valid semver version`,
88 );
102 } 89 }
103 90
104 // from the recipe 91 // from the recipe
@@ -106,19 +93,52 @@ export default class Recipe {
106 this.name = ifUndefinedString(data.name, this.name); 93 this.name = ifUndefinedString(data.name, this.name);
107 this.version = ifUndefinedString(data.version, this.version); 94 this.version = ifUndefinedString(data.version, this.version);
108 this.aliases = data.aliases || this.aliases; 95 this.aliases = data.aliases || this.aliases;
109 this.serviceURL = ifUndefinedString(data.config.serviceURL, this.serviceURL); 96 this.serviceURL = ifUndefinedString(
110 this.hasDirectMessages = ifUndefinedBoolean(data.config.hasDirectMessages, this.hasDirectMessages); 97 data.config.serviceURL,
111 this.hasIndirectMessages = ifUndefinedBoolean(data.config.hasIndirectMessages, this.hasIndirectMessages); 98 this.serviceURL,
112 this.hasNotificationSound = ifUndefinedBoolean(data.config.hasNotificationSound, this.hasNotificationSound); 99 );
100 this.hasDirectMessages = ifUndefinedBoolean(
101 data.config.hasDirectMessages,
102 this.hasDirectMessages,
103 );
104 this.hasIndirectMessages = ifUndefinedBoolean(
105 data.config.hasIndirectMessages,
106 this.hasIndirectMessages,
107 );
108 this.hasNotificationSound = ifUndefinedBoolean(
109 data.config.hasNotificationSound,
110 this.hasNotificationSound,
111 );
113 this.hasTeamId = ifUndefinedBoolean(data.config.hasTeamId, this.hasTeamId); 112 this.hasTeamId = ifUndefinedBoolean(data.config.hasTeamId, this.hasTeamId);
114 this.hasCustomUrl = ifUndefinedBoolean(data.config.hasCustomUrl, this.hasCustomUrl); 113 this.hasCustomUrl = ifUndefinedBoolean(
115 this.hasHostedOption = ifUndefinedBoolean(data.config.hasHostedOption, this.hasHostedOption); 114 data.config.hasCustomUrl,
116 this.urlInputPrefix = ifUndefinedString(data.config.urlInputPrefix, this.urlInputPrefix); 115 this.hasCustomUrl,
117 this.urlInputSuffix = ifUndefinedString(data.config.urlInputSuffix, this.urlInputSuffix); 116 );
118 this.disablewebsecurity = ifUndefinedBoolean(data.config.disablewebsecurity, this.disablewebsecurity); 117 this.hasHostedOption = ifUndefinedBoolean(
119 this.autoHibernate = ifUndefinedBoolean(data.config.autoHibernate, this.autoHibernate); 118 data.config.hasHostedOption,
119 this.hasHostedOption,
120 );
121 this.urlInputPrefix = ifUndefinedString(
122 data.config.urlInputPrefix,
123 this.urlInputPrefix,
124 );
125 this.urlInputSuffix = ifUndefinedString(
126 data.config.urlInputSuffix,
127 this.urlInputSuffix,
128 );
129 this.disablewebsecurity = ifUndefinedBoolean(
130 data.config.disablewebsecurity,
131 this.disablewebsecurity,
132 );
133 this.autoHibernate = ifUndefinedBoolean(
134 data.config.autoHibernate,
135 this.autoHibernate,
136 );
120 this.message = ifUndefinedString(data.config.message, this.message); 137 this.message = ifUndefinedString(data.config.message, this.message);
121 this.allowFavoritesDelineationInUnreadCount = ifUndefinedBoolean(data.config.allowFavoritesDelineationInUnreadCount, this.allowFavoritesDelineationInUnreadCount); 138 this.allowFavoritesDelineationInUnreadCount = ifUndefinedBoolean(
139 data.config.allowFavoritesDelineationInUnreadCount,
140 this.allowFavoritesDelineationInUnreadCount,
141 );
122 142
123 // computed 143 // computed
124 this.path = data.path; 144 this.path = data.path;
diff --git a/src/models/Service.js b/src/models/Service.js
index 75dfde027..12109fb0a 100644
--- a/src/models/Service.js
+++ b/src/models/Service.js
@@ -81,6 +81,8 @@ export default class Service {
81 81
82 @observable isHibernationEnabled = false; 82 @observable isHibernationEnabled = false;
83 83
84 @observable isWakeUpEnabled = true;
85
84 @observable isHibernationRequested = false; 86 @observable isHibernationRequested = false;
85 87
86 @observable onlyShowFavoritesInUnreadCount = false; 88 @observable onlyShowFavoritesInUnreadCount = false;
@@ -163,6 +165,10 @@ export default class Service {
163 data.isHibernationEnabled, 165 data.isHibernationEnabled,
164 this.isHibernationEnabled, 166 this.isHibernationEnabled,
165 ); 167 );
168 this.isWakeUpEnabled = ifUndefinedBoolean(
169 data.isWakeUpEnabled,
170 this.isWakeUpEnabled,
171 );
166 172
167 // Check if "Hibernate on Startup" is enabled and hibernate all services except active one 173 // Check if "Hibernate on Startup" is enabled and hibernate all services except active one
168 const { hibernateOnStartup } = window.ferdi.stores.settings.app; 174 const { hibernateOnStartup } = window.ferdi.stores.settings.app;
diff --git a/src/models/User.ts b/src/models/User.ts
index a04d46d3c..571f1f847 100644
--- a/src/models/User.ts
+++ b/src/models/User.ts
@@ -59,7 +59,8 @@ export default class User {
59 this.beta = data.beta || this.beta; 59 this.beta = data.beta || this.beta;
60 this.locale = data.locale || this.locale; 60 this.locale = data.locale || this.locale;
61 61
62 this.isSubscriptionOwner = data.isSubscriptionOwner || this.isSubscriptionOwner; 62 this.isSubscriptionOwner =
63 data.isSubscriptionOwner || this.isSubscriptionOwner;
63 64
64 this.team = data.team || this.team; 65 this.team = data.team || this.team;
65 } 66 }
diff --git a/src/models/UserAgent.js b/src/models/UserAgent.js
index 33bf9d072..02ff97db1 100644
--- a/src/models/UserAgent.js
+++ b/src/models/UserAgent.js
@@ -63,8 +63,12 @@ export default class UserAgent {
63 } 63 }
64 64
65 @computed get userAgent() { 65 @computed get userAgent() {
66 return this.serviceUserAgentPref 66 return (
67 || (this.chromelessUserAgent ? this.userAgentWithoutChromeVersion : this.defaultUserAgent); 67 this.serviceUserAgentPref ||
68 (this.chromelessUserAgent
69 ? this.userAgentWithoutChromeVersion
70 : this.defaultUserAgent)
71 );
68 } 72 }
69 73
70 @action setWebviewReference(webview) { 74 @action setWebviewReference(webview) {
diff --git a/src/routes.js b/src/routes.js
index 502ea86f5..9891e5d43 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -1,4 +1,4 @@
1import React, { Component } from 'react'; 1import { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { inject, observer } from 'mobx-react'; 3import { inject, observer } from 'mobx-react';
4import { Router, Route, IndexRedirect } from 'react-router'; 4import { Router, Route, IndexRedirect } from 'react-router';
diff --git a/src/stores.types.ts b/src/stores.types.ts
new file mode 100644
index 000000000..e5978a392
--- /dev/null
+++ b/src/stores.types.ts
@@ -0,0 +1,367 @@
1export interface FerdiStores {
2 app: AppStore;
3 communityRecipes: CommunityRecipesStore;
4 features: FeaturesStore;
5 globalError: GlobalErrorStore;
6 recipePreviews: RecipePreviewsStore;
7 recipes: RecipeStore;
8 requests: RequestsStore;
9 router: RouterStore;
10 services: ServicesStore;
11 settings: SettingsStore;
12 todos: TodosStore;
13 ui: UIStore;
14 user: UserStore;
15 workspaces: WorkspacesStore;
16}
17
18interface Stores {
19 app: AppStore;
20 communityRecipes: CommunityRecipesStore;
21 features: FeaturesStore;
22 globalError: GlobalErrorStore;
23 recipePreviews: RecipePreviewsStore;
24 recipes: RecipeStore;
25 requests: RequestsStore;
26 router: RouterStore;
27 services: ServicesStore;
28 settings: SettingsStore;
29 todos: TodosStore;
30 ui: UIStore;
31 user: UserStore;
32 workspaces: WorkspacesStore;
33}
34
35interface Actions {
36 app: AppStore;
37 recipePreviews: RecipePreviewsStore;
38 recipes: RecipeStore;
39 requests: RequestsStore;
40 services: ServicesStore;
41 settings: SettingsStore;
42 todos: TodosStore;
43 ui: UIStore;
44 user: UserStore;
45 workspaces: WorkspacesStore;
46}
47
48interface Api {
49 app: AppStore;
50 features: FeaturesStore;
51 local: {};
52 recipePreviews: RecipePreviewsStore;
53 recipes: RecipeStore;
54 services: ServicesStore;
55 user: UserStore;
56}
57
58interface AppStore {
59 actions: Actions;
60 accentColor: string;
61 api: Api;
62 authRequestFailed: () => void;
63 autoLaunchOnStart: () => void;
64 clearAppCacheRequest: () => void;
65 dictionaries: [];
66 fetchDataInterval: 4;
67 getAppCacheSizeRequest: () => void;
68 healthCheckRequest: () => void;
69 isClearingAllCache: () => void;
70 isFocused: () => void;
71 isFullScreen: () => void;
72 isOnline: () => void;
73 isSystemDarkModeEnabled: () => void;
74 isSystemMuteOverridden: () => void;
75 locale: () => void;
76 stores: Stores;
77 timeOfflineStart: () => void;
78 timeSuspensionStart: () => void;
79 updateStatus: () => void;
80 updateStatusTypes: {
81 CHECKING: 'CHECKING';
82 AVAILABLE: 'AVAILABLE';
83 NOT_AVAILABLE: 'NOT_AVAILABLE';
84 DOWNLOADED: 'DOWNLOADED';
85 FAILED: 'FAILED';
86 };
87 _reactions: any[];
88 _status: () => void;
89 actionStatus: () => void;
90 cacheSize: () => void;
91 debugInfo: () => void;
92}
93
94interface CommunityRecipesStore {
95 actions: Actions;
96 stores: Stores;
97 _actions: [];
98 _reactions: [];
99 communityRecipes: () => void;
100}
101
102interface FeaturesStore {
103 actions: Actions;
104 api: Api;
105 defaultFeaturesRequest: () => void;
106 features: () => void;
107 featuresRequest: () => void;
108 stores: Stores;
109 _reactions: any[];
110 _status: () => void;
111 _updateFeatures: () => void;
112 actionStatus: () => void;
113 anonymousFeatures: () => void;
114}
115
116interface GlobalErrorStore {
117 actions: Actions;
118 api: Api;
119 error: () => void;
120 messages: () => void;
121 response: () => void;
122 stores: Stores;
123 _handleRequests: () => void;
124 _reactions: [];
125 _status: () => void;
126 actionStatus: () => void;
127}
128
129interface RecipePreviewsStore {
130 actions: Actions;
131 allRecipePreviewsRequest: () => void;
132 api: Api;
133 searchRecipePreviewsRequest: () => void;
134 stores: Stores;
135 _reactions: [];
136 _status: () => void;
137 actionStatus: () => void;
138 all: () => void;
139 dev: () => void;
140 searchResults: () => void;
141}
142
143interface RecipeStore {
144 actions: Actions;
145 allRecipesRequest: () => void;
146 api: Api;
147 getRecipeUpdatesRequest: () => void;
148 installRecipeRequest: () => void;
149 stores: Stores;
150 _reactions: any[];
151 _status: () => void;
152 actionStatus: () => void;
153 active: () => void;
154 all: () => void;
155 recipeIdForServices: () => void;
156}
157
158interface RequestsStore {
159 actions: Actions;
160 api: Api;
161 localServerPort: () => void;
162 retries: number;
163 retryDelay: number;
164 servicesRequest: () => void;
165 showRequiredRequestsError: () => void;
166 stores: Stores;
167 userInfoRequest: () => void;
168 _reactions: any[];
169 _status: () => void;
170 actionStatus: () => void;
171 areRequiredRequestsLoading: () => void;
172 areRequiredRequestsSuccessful: () => void;
173}
174
175interface RouterStore {
176 go: () => void;
177 goBack: () => void;
178 goForward: () => void;
179 history: () => void;
180 location: () => void;
181 push: () => void;
182 replace: () => void;
183}
184
185export interface ServicesStore {
186 actions: Actions;
187 api: Api;
188 clearCacheRequest: () => void;
189 createServiceRequest: () => void;
190 deleteServiceRequest: () => void;
191 filterNeedle: () => void;
192 lastUsedServices: () => void;
193 reorderServicesRequest: () => void;
194 serviceMaintenanceTick: () => void;
195 stores: Stores;
196 updateServiceRequest: () => void;
197 _reactions: any[];
198 _status: () => void;
199 actionStatus: () => void;
200 active: () => void;
201 activeSettings: () => void;
202 all: () => void;
203 allDisplayed: () => void;
204 allDisplayedUnordered: () => void;
205 enabled: () => void;
206 filtered: () => void;
207 isTodosServiceActive: () => void;
208 isTodosServiceAdded: () => void;
209}
210
211interface SettingsStore {
212 actions: Actions;
213 api: Api;
214 fileSystemSettingsTypes: any[];
215 startup: boolean;
216 stores: Stores;
217 updateAppSettingsRequest: () => void;
218 _fileSystemSettingsCache: () => void;
219 _reactions: [];
220 _status: () => void;
221 actionStatus: () => void;
222 all: () => void;
223 app: AppStore;
224 migration: () => void;
225 proxy: () => void;
226 service: ServicesStore;
227 stats: () => void;
228}
229
230interface TodosStore {
231 actions: Actions;
232 api: Api;
233 isFeatureActive: () => void;
234 isFeatureEnabled: () => void;
235 isInitialized: true;
236 stores: Stores;
237 userAgentModel: () => void;
238 webview: () => void;
239 _actions: any[];
240 _allReactions: any[];
241 _firstLaunchReaction: () => void;
242 _goToService: () => void;
243 _handleNewWindowEvent: () => void;
244 _onTodosClientInitialized: () => void;
245 _openDevTools: () => void;
246 _reactions: any[];
247 _reload: () => void;
248 _routeCheckReaction: () => void;
249 _setFeatureEnabledReaction: () => void;
250 _updateSettings: () => void;
251 _updateTodosConfig: () => void;
252 isFeatureEnabledByUser: () => void;
253 isTodoUrlValid: () => void;
254 isTodosPanelForceHidden: () => void;
255 isTodosPanelVisible: () => void;
256 isUsingPredefinedTodoServer: () => void;
257 settings: () => void;
258 todoRecipeId: () => void;
259 todoUrl: () => void;
260 userAgent: () => void;
261 width: () => void;
262 _handleClientMessage: () => void;
263 _handleHostMessage: () => void;
264 _resize: () => void;
265 _setTodosWebview: () => void;
266 _toggleTodosFeatureVisibility: () => void;
267 _toggleTodosPanel: () => void;
268}
269
270interface UIStore {
271 actions: Actions;
272 api: Api;
273 isOsDarkThemeActive: () => void;
274 showServicesUpdatedInfoBar: () => void;
275 stores: Stores;
276 _reactions: [];
277 _status: () => void;
278 actionStatus: () => void;
279 isDarkThemeActive: () => void;
280 isSplitModeActive: () => void;
281 showMessageBadgesEvenWhenMuted: () => void;
282 theme: () => void;
283}
284
285interface UserStore {
286 BASE_ROUTE: '/auth';
287 CHANGE_SERVER_ROUTE: '/auth/server';
288 IMPORT_ROUTE: '/auth/signup/import';
289 INVITE_ROUTE: '/auth/signup/invite';
290 LOGIN_ROUTE: '/auth/login';
291 LOGOUT_ROUTE: '/auth/logout';
292 PASSWORD_ROUTE: '/auth/password';
293 SETUP_ROUTE: '/auth/signup/setup';
294 SIGNUP_ROUTE: '/auth/signup';
295 WELCOME_ROUTE: '/auth/welcome';
296 accountType: () => void;
297 actionStatus: () => void;
298 actions: Actions;
299 api: Api;
300 authToken: () => void;
301 deleteAccountRequest: () => void;
302 fetchUserInfoInterval: null;
303 getLegacyServicesRequest: () => void;
304 getUserInfoRequest: () => void;
305 hasCompletedSignup: () => void;
306 id: () => void;
307 inviteRequest: () => void;
308 isImportLegacyServicesCompleted: () => void;
309 isImportLegacyServicesExecuting: () => void;
310 isLoggingOut: () => void;
311 loginRequest: () => void;
312 logoutReason: () => void;
313 logoutReasonTypes: { SERVER: 'SERVER' };
314 passwordRequest: () => void;
315 signupRequest: () => void;
316 stores: Stores;
317 updateUserInfoRequest: () => void;
318 userData: () => void;
319 _reactions: any[];
320 _requireAuthenticatedUser: () => void;
321 _status: () => void;
322 changeServerRoute: () => void;
323 data: () => void;
324 importRoute: () => void;
325 inviteRoute: () => void;
326 isLoggedIn: () => void;
327 isTokenExpired: () => void;
328 legacyServices: () => void;
329 loginRoute: () => void;
330 logoutRoute: () => void;
331 passwordRoute: () => void;
332 setupRoute: () => void;
333 signupRoute: () => void;
334 team: () => void;
335}
336
337export interface WorkspacesStore {
338 activeWorkspace: () => void;
339 delete: ({ workspace }) => void;
340 update: ({ workspace }) => void;
341 create: ({ workspace }) => void;
342 edit: ({ workspace }) => void;
343 saving: boolean;
344 filterServicesByActiveWorkspace: () => void;
345 isFeatureActive: () => void;
346 isFeatureEnabled: () => void;
347 isSettingsRouteActive: () => void;
348 isSwitchingWorkspace: () => void;
349 isWorkspaceDrawerOpen: () => void;
350 nextWorkspace: () => void;
351 stores: Stores;
352 workspaceBeingEdited: () => void;
353 _actions: any[];
354 _activateLastUsedWorkspaceReaction: () => void;
355 _allActions: any[];
356 _allReactions: any[];
357 _cleanupInvalidServiceReferences: () => void;
358 _getWorkspaceById: () => void;
359 _openDrawerWithSettingsReaction: () => void;
360 _reactions: any[];
361 _setActiveServiceOnWorkspaceSwitchReaction: () => void;
362 _setFeatureEnabledReaction: () => void;
363 _setWorkspaceBeingEditedReaction: () => void;
364 _toggleKeepAllWorkspacesLoadedSetting: () => void;
365 _updateSettings: () => void;
366 _wasDrawerOpenBeforeSettingsRoute: null;
367}
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
index 3d9d2b551..81cef3775 100644
--- a/src/stores/AppStore.js
+++ b/src/stores/AppStore.js
@@ -17,16 +17,8 @@ import { readJsonSync } from 'fs-extra';
17import Store from './lib/Store'; 17import Store from './lib/Store';
18import Request from './lib/Request'; 18import Request from './lib/Request';
19import { CHECK_INTERVAL, DEFAULT_APP_SETTINGS } from '../config'; 19import { CHECK_INTERVAL, DEFAULT_APP_SETTINGS } from '../config';
20import { 20import { isMac, electronVersion, osRelease } from '../environment';
21 isMac, 21import { ferdiVersion, userDataPath, ferdiLocale } from '../environment-remote';
22 electronVersion,
23 osRelease,
24} from '../environment';
25import {
26 ferdiVersion,
27 userDataPath,
28 ferdiLocale,
29} from '../environment-remote';
30import locales from '../i18n/translations'; 22import locales from '../i18n/translations';
31import { getLocale } from '../helpers/i18n-helpers'; 23import { getLocale } from '../helpers/i18n-helpers';
32 24
@@ -41,8 +33,6 @@ const debug = require('debug')('Ferdi:AppStore');
41 33
42const mainWindow = getCurrentWindow(); 34const mainWindow = getCurrentWindow();
43 35
44const defaultLocale = DEFAULT_APP_SETTINGS.locale;
45
46const executablePath = isMac ? remoteProcess.execPath : process.execPath; 36const executablePath = isMac ? remoteProcess.execPath : process.execPath;
47const autoLauncher = new AutoLaunch({ 37const autoLauncher = new AutoLaunch({
48 name: 'Ferdi', 38 name: 'Ferdi',
@@ -80,9 +70,9 @@ export default class AppStore extends Store {
80 70
81 @observable timeOfflineStart; 71 @observable timeOfflineStart;
82 72
83 @observable updateStatus = null; 73 @observable updateStatus = '';
84 74
85 @observable locale = defaultLocale; 75 @observable locale = ferdiLocale;
86 76
87 @observable isSystemMuteOverridden = false; 77 @observable isSystemMuteOverridden = false;
88 78
@@ -163,9 +153,6 @@ export default class AppStore extends Store {
163 this.stores.features.featuresRequest.invalidate({ 153 this.stores.features.featuresRequest.invalidate({
164 immediately: true, 154 immediately: true,
165 }); 155 });
166 this.stores.news.latestNewsRequest.invalidate({
167 immediately: true,
168 });
169 }, ms('60m')); 156 }, ms('60m'));
170 157
171 // Check for updates once every 4 hours 158 // Check for updates once every 4 hours
@@ -408,7 +395,7 @@ export default class AppStore extends Store {
408 } 395 }
409 396
410 @action _resetUpdateStatus() { 397 @action _resetUpdateStatus() {
411 this.updateStatus = null; 398 this.updateStatus = '';
412 } 399 }
413 400
414 @action _healthCheck() { 401 @action _healthCheck() {
@@ -480,23 +467,13 @@ export default class AppStore extends Store {
480 } 467 }
481 468
482 _setLocale() { 469 _setLocale() {
483 let locale; 470 if (this.stores.user.isLoggedIn && this.stores.user.data.locale) {
484 if (this.stores.user.isLoggedIn) { 471 this.locale = this.stores.user.data.locale;
485 locale = this.stores.user.data.locale; 472 } else if (!this.locale) {
486 }
487
488 if (
489 locale &&
490 Object.prototype.hasOwnProperty.call(locales, locale) &&
491 locale !== this.locale
492 ) {
493 this.locale = locale;
494 } else if (!locale) {
495 this.locale = this._getDefaultLocale(); 473 this.locale = this._getDefaultLocale();
496 } 474 }
497 475
498 moment.locale(this.locale); 476 moment.locale(this.locale);
499
500 debug(`Set locale to "${this.locale}"`); 477 debug(`Set locale to "${this.locale}"`);
501 } 478 }
502 479
@@ -504,7 +481,6 @@ export default class AppStore extends Store {
504 return getLocale({ 481 return getLocale({
505 locale: ferdiLocale, 482 locale: ferdiLocale,
506 locales, 483 locales,
507 defaultLocale,
508 fallbackLocale: DEFAULT_APP_SETTINGS.fallbackLocale, 484 fallbackLocale: DEFAULT_APP_SETTINGS.fallbackLocale,
509 }); 485 });
510 } 486 }
@@ -523,10 +499,12 @@ export default class AppStore extends Store {
523 _handleFullScreen() { 499 _handleFullScreen() {
524 const body = document.querySelector('body'); 500 const body = document.querySelector('body');
525 501
526 if (this.isFullScreen) { 502 if (body) {
527 body.classList.add('isFullScreen'); 503 if (this.isFullScreen) {
528 } else { 504 body.classList.add('isFullScreen');
529 body.classList.remove('isFullScreen'); 505 } else {
506 body.classList.remove('isFullScreen');
507 }
530 } 508 }
531 } 509 }
532 510
diff --git a/src/stores/NewsStore.js b/src/stores/NewsStore.js
deleted file mode 100644
index 66a17cb29..000000000
--- a/src/stores/NewsStore.js
+++ /dev/null
@@ -1,55 +0,0 @@
1import { computed, observable } from 'mobx';
2import { remove } from 'lodash';
3
4import Store from './lib/Store';
5import CachedRequest from './lib/CachedRequest';
6import Request from './lib/Request';
7import { CHECK_INTERVAL } from '../config';
8
9export default class NewsStore extends Store {
10 @observable latestNewsRequest = new CachedRequest(this.api.news, 'latest');
11
12 @observable hideNewsRequest = new Request(this.api.news, 'hide');
13
14 constructor(...args) {
15 super(...args);
16
17 // Register action handlers
18 this.actions.news.hide.listen(this._hide.bind(this));
19 this.actions.user.logout.listen(this._resetNewsRequest.bind(this));
20 }
21
22 setup() {
23 // Check for news updates every couple of hours
24 setInterval(() => {
25 if (this.latestNewsRequest.wasExecuted && this.stores.user.isLoggedIn) {
26 this.latestNewsRequest.invalidate({ immediately: true });
27 }
28 }, CHECK_INTERVAL);
29 }
30
31 @computed get latest() {
32 return this.latestNewsRequest.execute().result || [];
33 }
34
35 // Actions
36 _hide({ newsId }) {
37 this.hideNewsRequest.execute(newsId);
38
39 this.latestNewsRequest.invalidate().patch((result) => {
40 // TODO: check if we can use mobx.array remove
41 remove(result, (n) => n.id === newsId);
42 });
43 }
44
45 /**
46 * Reset the news request when current user logs out so that when another user
47 * logs in again without an app restart, the request will be fetched again and
48 * the news will be shown to the user.
49 *
50 * @private
51 */
52 _resetNewsRequest() {
53 this.latestNewsRequest.reset();
54 }
55}
diff --git a/src/stores/RecipePreviewsStore.js b/src/stores/RecipePreviewsStore.js
index f4e39306c..e01e8fc6f 100644
--- a/src/stores/RecipePreviewsStore.js
+++ b/src/stores/RecipePreviewsStore.js
@@ -5,9 +5,15 @@ import CachedRequest from './lib/CachedRequest';
5import Request from './lib/Request'; 5import Request from './lib/Request';
6 6
7export default class RecipePreviewsStore extends Store { 7export default class RecipePreviewsStore extends Store {
8 @observable allRecipePreviewsRequest = new CachedRequest(this.api.recipePreviews, 'all'); 8 @observable allRecipePreviewsRequest = new CachedRequest(
9 this.api.recipePreviews,
10 'all',
11 );
9 12
10 @observable searchRecipePreviewsRequest = new Request(this.api.recipePreviews, 'search'); 13 @observable searchRecipePreviewsRequest = new Request(
14 this.api.recipePreviews,
15 'search',
16 );
11 17
12 constructor(...args) { 18 constructor(...args) {
13 super(...args); 19 super(...args);
@@ -25,7 +31,7 @@ export default class RecipePreviewsStore extends Store {
25 } 31 }
26 32
27 @computed get dev() { 33 @computed get dev() {
28 return this.stores.recipes.all.filter((r) => r.local); 34 return this.stores.recipes.all.filter(r => r.local);
29 } 35 }
30 36
31 // Actions 37 // Actions
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index ce6675866..4f7ad7442 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -15,7 +15,7 @@ import {
15 getDevRecipeDirectory, 15 getDevRecipeDirectory,
16} from '../helpers/recipe-helpers'; 16} from '../helpers/recipe-helpers';
17import { workspaceStore } from '../features/workspaces'; 17import { workspaceStore } from '../features/workspaces';
18import { KEEP_WS_LOADED_USID } from '../config'; 18import { DEFAULT_SERVICE_SETTINGS, KEEP_WS_LOADED_USID } from '../config';
19import { SPELLCHECKER_LOCALES } from '../i18n/languages'; 19import { SPELLCHECKER_LOCALES } from '../i18n/languages';
20import { ferdiVersion } from '../environment-remote'; 20import { ferdiVersion } from '../environment-remote';
21 21
@@ -224,6 +224,7 @@ export default class ServicesStore extends Store {
224 } 224 }
225 225
226 if ( 226 if (
227 service.isWakeUpEnabled &&
227 service.lastHibernated && 228 service.lastHibernated &&
228 Number(this.stores.settings.all.app.wakeUpStrategy) > 0 && 229 Number(this.stores.settings.all.app.wakeUpStrategy) > 0 &&
229 Date.now() - service.lastHibernated > 230 Date.now() - service.lastHibernated >
@@ -394,16 +395,15 @@ export default class ServicesStore extends Store {
394 } 395 }
395 396
396 // set default values for serviceData 397 // set default values for serviceData
397 // eslint-disable-next-line prefer-object-spread
398 // TODO: How is this different from the defaults of the recipe in 'src/models/Recipe' file?
399 serviceData = { 398 serviceData = {
400 isEnabled: true, 399 isEnabled: DEFAULT_SERVICE_SETTINGS.isEnabled,
401 isHibernationEnabled: false, 400 isHibernationEnabled: DEFAULT_SERVICE_SETTINGS.isHibernationEnabled,
402 isNotificationEnabled: true, 401 isWakeUpEnabled: DEFAULT_SERVICE_SETTINGS.isWakeUpEnabled,
403 isBadgeEnabled: true, 402 isNotificationEnabled: DEFAULT_SERVICE_SETTINGS.isNotificationEnabled,
404 isMuted: false, 403 isBadgeEnabled: DEFAULT_SERVICE_SETTINGS.isBadgeEnabled,
405 customIcon: false, 404 isMuted: DEFAULT_SERVICE_SETTINGS.isMuted,
406 isDarkModeEnabled: false, 405 customIcon: DEFAULT_SERVICE_SETTINGS.customIcon,
406 isDarkModeEnabled: DEFAULT_SERVICE_SETTINGS.isDarkModeEnabled,
407 spellcheckerLanguage: 407 spellcheckerLanguage:
408 SPELLCHECKER_LOCALES[this.stores.settings.app.spellcheckerLanguage], 408 SPELLCHECKER_LOCALES[this.stores.settings.app.spellcheckerLanguage],
409 userAgentPref: '', 409 userAgentPref: '',
@@ -888,6 +888,7 @@ export default class ServicesStore extends Store {
888 return this.actions.todos.reload(); 888 return this.actions.todos.reload();
889 } 889 }
890 890
891 if (!service.webview) return;
891 return service.webview.loadURL(service.url); 892 return service.webview.loadURL(service.url);
892 } 893 }
893 894
@@ -995,7 +996,7 @@ export default class ServicesStore extends Store {
995 const service = this.one(serviceId); 996 const service = this.one(serviceId);
996 if (service.isTodosService) { 997 if (service.isTodosService) {
997 this.actions.todos.openDevTools(); 998 this.actions.todos.openDevTools();
998 } else { 999 } else if (service.webview) {
999 service.webview.openDevTools(); 1000 service.webview.openDevTools();
1000 } 1001 }
1001 } 1002 }
@@ -1147,7 +1148,7 @@ export default class ServicesStore extends Store {
1147 const { isAttached } = service; 1148 const { isAttached } = service;
1148 const isMuted = isAppMuted || service.isMuted; 1149 const isMuted = isAppMuted || service.isMuted;
1149 1150
1150 if (isAttached) { 1151 if (isAttached && service.webview) {
1151 service.webview.audioMuted = isMuted; 1152 service.webview.audioMuted = isMuted;
1152 } 1153 }
1153 } 1154 }
diff --git a/src/stores/SettingsStore.js b/src/stores/SettingsStore.js
index ec80fee7c..ac9356404 100644
--- a/src/stores/SettingsStore.js
+++ b/src/stores/SettingsStore.js
@@ -2,7 +2,11 @@ import { ipcRenderer } from 'electron';
2import { getCurrentWindow } from '@electron/remote'; 2import { getCurrentWindow } from '@electron/remote';
3import { action, computed, observable, reaction } from 'mobx'; 3import { action, computed, observable, reaction } from 'mobx';
4import localStorage from 'mobx-localstorage'; 4import localStorage from 'mobx-localstorage';
5import { DEFAULT_APP_SETTINGS, FILE_SYSTEM_SETTINGS_TYPES, LOCAL_SERVER } from '../config'; 5import {
6 DEFAULT_APP_SETTINGS,
7 FILE_SYSTEM_SETTINGS_TYPES,
8 LOCAL_SERVER,
9} from '../config';
6import { hash } from '../helpers/password-helpers'; 10import { hash } from '../helpers/password-helpers';
7import Request from './lib/Request'; 11import Request from './lib/Request';
8import Store from './lib/Store'; 12import Store from './lib/Store';
diff --git a/src/stores/UIStore.ts b/src/stores/UIStore.ts
index 6ab63c2ee..6a9597006 100644
--- a/src/stores/UIStore.ts
+++ b/src/stores/UIStore.ts
@@ -1,7 +1,7 @@
1import { action, observable, computed, reaction } from 'mobx'; 1import { action, observable, computed, reaction } from 'mobx';
2import { theme, ThemeType } from '@meetfranz/theme';
3import { nativeTheme, systemPreferences } from '@electron/remote'; 2import { nativeTheme, systemPreferences } from '@electron/remote';
4 3
4import { theme, ThemeType } from '../themes';
5import Store from './lib/Store'; 5import Store from './lib/Store';
6import { isMac, isWindows } from '../environment'; 6import { isMac, isWindows } from '../environment';
7 7
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js
index 9f222d2d3..9a5d8cb30 100644
--- a/src/stores/UserStore.js
+++ b/src/stores/UserStore.js
@@ -273,13 +273,11 @@ export default class UserStore extends Store {
273 273
274 // Install recipes 274 // Install recipes
275 for (const recipe of recipes) { 275 for (const recipe of recipes) {
276 // eslint-disable-line no-unused-vars
277 // eslint-disable-next-line no-await-in-loop 276 // eslint-disable-next-line no-await-in-loop
278 await this.stores.recipes._install({ recipeId: recipe }); 277 await this.stores.recipes._install({ recipeId: recipe });
279 } 278 }
280 279
281 for (const service of services) { 280 for (const service of services) {
282 // eslint-disable-line no-unused-vars
283 this.actions.service.createFromLegacyService({ 281 this.actions.service.createFromLegacyService({
284 data: service, 282 data: service,
285 }); 283 });
diff --git a/src/stores/index.ts b/src/stores/index.ts
index 1760ddfa2..6ad898d85 100644
--- a/src/stores/index.ts
+++ b/src/stores/index.ts
@@ -6,7 +6,6 @@ import ServicesStore from './ServicesStore';
6import RecipesStore from './RecipesStore'; 6import RecipesStore from './RecipesStore';
7import RecipePreviewsStore from './RecipePreviewsStore'; 7import RecipePreviewsStore from './RecipePreviewsStore';
8import UIStore from './UIStore'; 8import UIStore from './UIStore';
9import NewsStore from './NewsStore';
10import RequestStore from './RequestStore'; 9import RequestStore from './RequestStore';
11import GlobalErrorStore from './GlobalErrorStore'; 10import GlobalErrorStore from './GlobalErrorStore';
12import { workspaceStore } from '../features/workspaces'; 11import { workspaceStore } from '../features/workspaces';
@@ -25,7 +24,6 @@ export default (api, actions, router) => {
25 recipes: new RecipesStore(stores, api, actions), 24 recipes: new RecipesStore(stores, api, actions),
26 recipePreviews: new RecipePreviewsStore(stores, api, actions), 25 recipePreviews: new RecipePreviewsStore(stores, api, actions),
27 ui: new UIStore(stores, api, actions), 26 ui: new UIStore(stores, api, actions),
28 news: new NewsStore(stores, api, actions),
29 requests: new RequestStore(stores, api, actions), 27 requests: new RequestStore(stores, api, actions),
30 globalError: new GlobalErrorStore(stores, api, actions), 28 globalError: new GlobalErrorStore(stores, api, actions),
31 workspaces: workspaceStore, 29 workspaces: workspaceStore,
diff --git a/src/stores/lib/Reaction.js b/src/stores/lib/Reaction.ts
index 7e1bc685e..0ca24a6fa 100644
--- a/src/stores/lib/Reaction.js
+++ b/src/stores/lib/Reaction.ts
@@ -20,12 +20,10 @@ export default class Reaction {
20 20
21 stop() { 21 stop() {
22 if (this.isRunning) { 22 if (this.isRunning) {
23 this.dispose(); 23 this.dispose?.();
24 this.isRunning = false; 24 this.isRunning = false;
25 } 25 }
26 } 26 }
27} 27}
28 28
29export const createReactions = (reactions) => ( 29export const createReactions = reactions => reactions.map(r => new Reaction(r));
30 reactions.map((r) => new Reaction(r))
31);
diff --git a/src/stores/lib/Request.js b/src/stores/lib/Request.js
index 39f32729a..65871ea17 100644
--- a/src/stores/lib/Request.js
+++ b/src/stores/lib/Request.js
@@ -38,42 +38,56 @@ export default class Request {
38 if (this._isWaitingForResponse) return this; 38 if (this._isWaitingForResponse) return this;
39 39
40 if (!this._api[this._method]) { 40 if (!this._api[this._method]) {
41 throw new Error(`Missing method <${this._method}> on api object:`, this._api); 41 throw new Error(
42 `Missing method <${this._method}> on api object:`,
43 this._api,
44 );
42 } 45 }
43 46
44 // This timeout is necessary to avoid warnings from mobx 47 // This timeout is necessary to avoid warnings from mobx
45 // regarding triggering actions as side-effect of getters 48 // regarding triggering actions as side-effect of getters
46 setTimeout(action(() => { 49 setTimeout(
47 this.isExecuting = true; 50 action(() => {
48 }), 0); 51 this.isExecuting = true;
52 }),
53 0,
54 );
49 55
50 // Issue api call & save it as promise that is handled to update the results of the operation 56 // Issue api call & save it as promise that is handled to update the results of the operation
51 this._promise = new Promise((resolve, reject) => { 57 this._promise = new Promise((resolve, reject) => {
52 this._api[this._method](...callArgs) 58 this._api[this._method](...callArgs)
53 .then((result) => { 59 .then(result => {
54 setTimeout(action(() => { 60 setTimeout(
55 this.result = result; 61 action(() => {
56 if (this._currentApiCall) this._currentApiCall.result = result; 62 this.result = result;
57 this.isExecuting = false; 63 if (this._currentApiCall) this._currentApiCall.result = result;
58 this.isError = false; 64 this.isExecuting = false;
59 this.wasExecuted = true; 65 this.isError = false;
60 this._isWaitingForResponse = false; 66 this.wasExecuted = true;
61 this._triggerHooks(); 67 this._isWaitingForResponse = false;
62 resolve(result); 68 this._triggerHooks();
63 }), 1); 69 resolve(result);
70 }),
71 1,
72 );
64 return result; 73 return result;
65 }) 74 })
66 .catch(action((error) => { 75 .catch(
67 setTimeout(action(() => { 76 action(error => {
68 this.error = error; 77 setTimeout(
69 this.isExecuting = false; 78 action(() => {
70 this.isError = true; 79 this.error = error;
71 this.wasExecuted = true; 80 this.isExecuting = false;
72 this._isWaitingForResponse = false; 81 this.isError = true;
73 this._triggerHooks(); 82 this.wasExecuted = true;
74 reject(error); 83 this._isWaitingForResponse = false;
75 }), 1); 84 this._triggerHooks();
76 })); 85 reject(error);
86 }),
87 1,
88 );
89 }),
90 );
77 }); 91 });
78 92
79 this._isWaitingForResponse = true; 93 this._isWaitingForResponse = true;
@@ -89,7 +103,11 @@ export default class Request {
89 retry = () => this.reload(); 103 retry = () => this.reload();
90 104
91 isExecutingWithArgs(...args) { 105 isExecutingWithArgs(...args) {
92 return this.isExecuting && this._currentApiCall && isEqual(this._currentApiCall.args, args); 106 return (
107 this.isExecuting &&
108 this._currentApiCall &&
109 isEqual(this._currentApiCall.args, args)
110 );
93 } 111 }
94 112
95 @computed get isExecutingFirstTime() { 113 @computed get isExecutingFirstTime() {
@@ -97,12 +115,18 @@ export default class Request {
97 } 115 }
98 116
99 then(...args) { 117 then(...args) {
100 if (!this._promise) throw new Error('You have to call Request::execute before you can access it as promise'); 118 if (!this._promise)
119 throw new Error(
120 'You have to call Request::execute before you can access it as promise',
121 );
101 return this._promise.then(...args); 122 return this._promise.then(...args);
102 } 123 }
103 124
104 catch(...args) { 125 catch(...args) {
105 if (!this._promise) throw new Error('You have to call Request::execute before you can access it as promise'); 126 if (!this._promise)
127 throw new Error(
128 'You have to call Request::execute before you can access it as promise',
129 );
106 return this._promise.catch(...args); 130 return this._promise.catch(...args);
107 } 131 }
108 132
diff --git a/src/stores/lib/Store.js b/src/stores/lib/Store.js
index b39070ce8..a867c3a46 100644
--- a/src/stores/lib/Store.js
+++ b/src/stores/lib/Store.js
@@ -28,7 +28,8 @@ export default class Store {
28 } 28 }
29 29
30 registerReactions(reactions) { 30 registerReactions(reactions) {
31 for (const reaction of reactions) this._reactions.push(new Reaction(reaction)); 31 for (const reaction of reactions)
32 this._reactions.push(new Reaction(reaction));
32 } 33 }
33 34
34 setup() {} 35 setup() {}
diff --git a/src/themes/IStyleTypes.ts b/src/themes/IStyleTypes.ts
new file mode 100644
index 000000000..48f52daf2
--- /dev/null
+++ b/src/themes/IStyleTypes.ts
@@ -0,0 +1,7 @@
1export interface IStyleTypes {
2 [index: string]: {
3 accent: string;
4 contrast: string;
5 border?: string;
6 };
7}
diff --git a/src/themes/dark/index.ts b/src/themes/dark/index.ts
new file mode 100644
index 000000000..aa132c743
--- /dev/null
+++ b/src/themes/dark/index.ts
@@ -0,0 +1,171 @@
1import color from 'color';
2import { cloneDeep, merge } from 'lodash';
3
4import makeDefaultThemeConfig from '../default';
5import * as legacyStyles from '../legacy';
6
7export default (brandPrimary: string) => {
8 const defaultStyles = makeDefaultThemeConfig(brandPrimary);
9 let brandPrimaryColor = color(legacyStyles.themeBrandPrimary);
10 try {
11 brandPrimaryColor = color(defaultStyles.brandPrimary);
12 } catch {
13 // Ignore invalid color and fall back to default.
14 }
15
16 const colorBackground = legacyStyles.darkThemeGrayDarkest;
17 const colorText = legacyStyles.darkThemeTextColor;
18 const inputColor = legacyStyles.darkThemeGrayLightest;
19 const inputBackground = legacyStyles.themeGrayDark;
20 const inputBorder = `1px solid ${legacyStyles.darkThemeGrayLight}`;
21 const inputPrefixColor = color(legacyStyles.darkThemeGrayLighter)
22 .lighten(0.3)
23 .hex();
24 const buttonSecondaryTextColor = legacyStyles.darkThemeTextColor;
25 const selectColor = inputColor;
26 const drawerBg = color(colorBackground).lighten(0.3).hex();
27
28 const services = merge({}, defaultStyles.services, {
29 listItems: {
30 borderColor: legacyStyles.darkThemeGrayDarker,
31 hoverBgColor: legacyStyles.darkThemeGrayDarker,
32 disabled: {
33 color: legacyStyles.darkThemeGray,
34 },
35 },
36 });
37
38 return {
39 ...defaultStyles,
40
41 colorBackground,
42 colorContentBackground: legacyStyles.darkThemeGrayDarkest,
43 colorBackgroundSubscriptionContainer: legacyStyles.themeBrandInfo,
44
45 colorHeadline: legacyStyles.darkThemeTextColor,
46 colorText: legacyStyles.darkThemeTextColor,
47
48 defaultContentBorder: legacyStyles.themeGrayDark,
49
50 // Loader
51 colorFullscreenLoaderSpinner: '#FFF',
52 colorWebviewLoaderBackground: color(legacyStyles.darkThemeGrayDarkest)
53 .alpha(0.5)
54 .rgb()
55 .string(),
56
57 // Input
58 labelColor: legacyStyles.darkThemeTextColor,
59 inputColor,
60 inputBackground,
61 inputBorder,
62 inputPrefixColor,
63 inputPrefixBackground: legacyStyles.darkThemeGray,
64 inputDisabledOpacity: 0.5,
65 inputScorePasswordBackground: legacyStyles.darkThemeGrayDark,
66 inputModifierColor: color(legacyStyles.darkThemeGrayLighter)
67 .lighten(0.3)
68 .hex(),
69 inputPlaceholderColor: color(legacyStyles.darkThemeGrayLighter)
70 .darken(0.1)
71 .hex(),
72
73 // Toggle
74 toggleBackground: legacyStyles.darkThemeGray,
75 toggleButton: legacyStyles.darkThemeGrayLighter,
76
77 // Button
78 buttonPrimaryTextColor: legacyStyles.darkThemeTextColor,
79
80 buttonSecondaryBackground: legacyStyles.darkThemeGrayLighter,
81 buttonSecondaryTextColor,
82
83 buttonDangerTextColor: legacyStyles.darkThemeTextColor,
84
85 buttonWarningTextColor: legacyStyles.darkThemeTextColor,
86
87 buttonLoaderColor: {
88 primary: '#FFF',
89 secondary: buttonSecondaryTextColor,
90 success: '#FFF',
91 warning: '#FFF',
92 danger: '#FFF',
93 inverted: defaultStyles.brandPrimary,
94 },
95
96 // Select
97 selectBackground: inputBackground,
98 selectBorder: inputBorder,
99 selectColor,
100 selectToggleColor: inputPrefixColor,
101 selectPopupBackground: legacyStyles.darkThemeGrayLight,
102 selectOptionColor: '#FFF',
103 selectOptionBorder: `1px solid ${color(legacyStyles.darkThemeGrayLight)
104 .darken(0.2)
105 .hex()}`,
106 selectOptionItemHover: color(legacyStyles.darkThemeGrayLight)
107 .darken(0.2)
108 .hex(),
109 selectOptionItemHoverColor: selectColor,
110 selectSearchColor: inputBackground,
111
112 // Modal
113 colorModalOverlayBackground: color(legacyStyles.darkThemeBlack)
114 .alpha(0.9)
115 .rgb()
116 .string(),
117 colorModalBackground: legacyStyles.darkThemeGrayDark,
118
119 // Services
120 services,
121
122 // Service Icon
123 serviceIcon: merge({}, defaultStyles.serviceIcon, {
124 isCustom: {
125 border: `1px solid ${legacyStyles.darkThemeGrayDark}`,
126 },
127 }),
128
129 // Workspaces
130 workspaces: merge({}, defaultStyles.workspaces, {
131 settings: {
132 listItems: cloneDeep(services.listItems),
133 },
134 drawer: {
135 background: drawerBg,
136 addButton: {
137 color: legacyStyles.darkThemeGrayLighter,
138 hoverColor: legacyStyles.darkThemeGraySmoke,
139 },
140 listItem: {
141 border: color(drawerBg).lighten(0.2).hex(),
142 hoverBackground: color(drawerBg).lighten(0.2).hex(),
143 activeBackground: defaultStyles.brandPrimary,
144 name: {
145 color: colorText,
146 activeColor: 'white',
147 },
148 services: {
149 color: color(colorText).darken(0.5).hex(),
150 active: brandPrimaryColor.lighten(0.5).hex(),
151 },
152 },
153 },
154 }),
155
156 // Todos
157 todos: merge({}, defaultStyles.todos, {
158 todosLayer: {
159 borderLeftColor: legacyStyles.darkThemeGrayDarker,
160 },
161 toggleButton: {
162 background: defaultStyles.styleTypes.primary.accent,
163 textColor: defaultStyles.styleTypes.primary.contrast,
164 shadowColor: 'rgba(0, 0, 0, 0.2)',
165 },
166 dragIndicator: {
167 background: legacyStyles.themeGrayLight,
168 },
169 }),
170 };
171};
diff --git a/src/themes/default/index.ts b/src/themes/default/index.ts
new file mode 100644
index 000000000..80bcba766
--- /dev/null
+++ b/src/themes/default/index.ts
@@ -0,0 +1,251 @@
1import color from 'color';
2import { cloneDeep } from 'lodash';
3
4import * as legacyStyles from '../legacy';
5import type { IStyleTypes } from '../IStyleTypes';
6
7export default (brandPrimary: string) => {
8 const brandSuccess = '#5cb85c';
9 const brandInfo = '#5bc0de';
10 const brandWarning = '#FF9F00';
11 const brandDanger = '#d9534f';
12 const uiFontSize = 14;
13 const colorBackground = legacyStyles.themeGrayLighter;
14 const colorContentBackground = '#FFFFFF';
15 const colorText = legacyStyles.themeTextColor;
16 const inputColor = legacyStyles.themeGray;
17 const inputBackground = legacyStyles.themeGrayLightest;
18 const inputHeight = 40;
19 const inputBorder = `1px solid ${legacyStyles.themeGrayLighter}`;
20 const inputPrefixColor = legacyStyles.themeGrayLight;
21 const inputDisabledOpacity = 0.5;
22 const buttonSecondaryTextColor = legacyStyles.themeGray;
23 const selectColor = inputColor;
24 const drawerBg = color(colorBackground).lighten(0.1).hex();
25
26 const styleTypes: IStyleTypes = {
27 primary: {
28 accent: brandPrimary,
29 contrast: '#FFF',
30 },
31 secondary: {
32 accent: legacyStyles.themeGrayLighter,
33 contrast: legacyStyles.themeGray,
34 },
35 success: {
36 accent: brandSuccess,
37 contrast: '#FFF',
38 },
39 warning: {
40 accent: brandWarning,
41 contrast: '#FFF',
42 },
43 danger: {
44 accent: brandDanger,
45 contrast: '#FFF',
46 },
47 inverted: {
48 accent: 'none',
49 contrast: brandPrimary,
50 border: `1px solid ${brandPrimary}`,
51 },
52 };
53
54 const services = {
55 listItems: {
56 padding: 10,
57 height: 57,
58 borderColor: legacyStyles.themeGrayLightest,
59 hoverBgColor: legacyStyles.themeGrayLightest,
60 disabled: {
61 color: legacyStyles.themeGrayLight,
62 },
63 },
64 };
65
66 return {
67 brandPrimary,
68 brandSuccess,
69 brandInfo,
70 brandWarning,
71 brandDanger,
72
73 uiFontSize,
74
75 borderRadius: legacyStyles.themeBorderRadius,
76 borderRadiusSmall: legacyStyles.themeBorderRadiusSmall,
77
78 colorBackground,
79
80 colorContentBackground,
81 colorHeadline: legacyStyles.themeGrayDark,
82
83 colorText,
84
85 defaultContentBorder: color(legacyStyles.themeGrayLighter)
86 .darken(0.1)
87 .rgb()
88 .string(),
89
90 // Subscription Container Component
91 colorSubscriptionContainerBackground: 'none',
92 colorSubscriptionContainerBorder: `1px solid ${brandPrimary}`,
93 colorSubscriptionContainerTitle: brandPrimary,
94 colorSubscriptionContainerActionButtonBackground: brandPrimary,
95 colorSubscriptionContainerActionButtonColor: '#FFF',
96
97 // Loader
98 colorAppLoaderSpinner: '#FFF',
99 colorFullscreenLoaderSpinner: legacyStyles.themeGrayDark,
100 colorWebviewLoaderBackground: color(legacyStyles.themeGrayLighter)
101 .alpha(0.8)
102 .rgb()
103 .string(),
104
105 // Input
106 labelColor: legacyStyles.themeGrayLight,
107 inputColor,
108 inputHeight,
109 inputBackground,
110 inputBorder,
111 inputModifierColor: legacyStyles.themeGrayLight,
112 inputPlaceholderColor: color(legacyStyles.themeGrayLight)
113 .lighten(0.3)
114 .hex(),
115 inputPrefixColor,
116 inputPrefixBackground: legacyStyles.themeGrayLighter,
117 inputDisabledOpacity,
118 inputScorePasswordBackground: legacyStyles.themeGrayLighter,
119
120 // Toggle
121 toggleBackground: legacyStyles.themeGrayLighter,
122 toggleButton: legacyStyles.themeGrayLight,
123 toggleButtonActive: brandPrimary,
124 toggleWidth: 40,
125 toggleHeight: 14,
126
127 // Style Types
128 styleTypes,
129
130 // Button
131 buttonPrimaryBackground: brandPrimary,
132 buttonPrimaryTextColor: '#FFF',
133
134 buttonSecondaryBackground: legacyStyles.themeGrayLighter,
135 buttonSecondaryTextColor,
136
137 buttonSuccessBackground: brandSuccess,
138 buttonSuccessTextColor: '#FFF',
139
140 buttonDangerBackground: brandDanger,
141 buttonDangerTextColor: '#FFF',
142
143 buttonWarningBackground: brandWarning,
144 buttonWarningTextColor: '#FFF',
145
146 buttonInvertedBackground: 'none',
147 buttonInvertedTextColor: brandPrimary,
148 buttonInvertedBorder: `1px solid ${brandPrimary}`,
149
150 buttonHeight: inputHeight,
151
152 buttonLoaderColor: {
153 primary: '#FFF',
154 secondary: buttonSecondaryTextColor,
155 success: '#FFF',
156 warning: '#FFF',
157 danger: '#FFF',
158 inverted: brandPrimary,
159 },
160
161 // Select
162 selectBackground: inputBackground,
163 selectBorder: inputBorder,
164 selectHeight: inputHeight,
165 selectColor,
166 selectToggleColor: inputPrefixColor,
167 selectPopupBackground: '#FFF',
168 selectOptionColor: inputColor,
169 selectOptionBorder: `1px solid ${legacyStyles.themeGrayLightest}`,
170 selectOptionItemHover: legacyStyles.themeGrayLighter,
171 selectOptionItemHoverColor: selectColor,
172 selectOptionItemActive: brandPrimary,
173 selectOptionItemActiveColor: '#FFF',
174 selectSearchBackground: legacyStyles.themeGrayLighter,
175 selectSearchColor: inputColor,
176 selectDisabledOpacity: inputDisabledOpacity,
177
178 // Badge
179 badgeFontSize: uiFontSize - 2,
180 badgeBorderRadius: 50,
181
182 // Modal
183 colorModalOverlayBackground: color('#000').alpha(0.8).rgb().string(),
184 colorModalBackground: colorContentBackground,
185
186 // Services
187 services,
188
189 // Service Icon
190 serviceIcon: {
191 width: 35,
192 isCustom: {
193 border: `1px solid ${legacyStyles.themeGrayLighter}`,
194 borderRadius: legacyStyles.themeBorderRadius,
195 width: 37,
196 },
197 },
198
199 // Workspaces
200 workspaces: {
201 settings: {
202 listItems: cloneDeep(services.listItems),
203 },
204 drawer: {
205 width: 300,
206 padding: 20,
207 background: drawerBg,
208 buttons: {
209 color: color(legacyStyles.themeGrayLight).lighten(0.1).hex(),
210 hoverColor: legacyStyles.themeGrayLight,
211 },
212 listItem: {
213 hoverBackground: color(drawerBg).darken(0.01).hex(),
214 activeBackground: legacyStyles.themeGrayLightest,
215 border: color(drawerBg).darken(0.05).hex(),
216 name: {
217 color: colorText,
218 activeColor: colorText,
219 },
220 services: {
221 color: color(colorText).lighten(1.5).hex(),
222 active: color(colorText).lighten(1.5).hex(),
223 },
224 },
225 },
226 switchingIndicator: {
227 spinnerColor: 'white',
228 },
229 },
230
231 // Todos
232 todos: {
233 todosLayer: {
234 borderLeftColor: color(legacyStyles.themeGrayLighter).darken(0.1).hex(),
235 },
236 toggleButton: {
237 background: styleTypes.primary.accent,
238 textColor: styleTypes.primary.contrast,
239 shadowColor: 'rgba(0, 0, 0, 0.2)',
240 },
241 dragIndicator: {
242 background: legacyStyles.themeGrayLight,
243 },
244 resizeHandler: {
245 backgroundHover: styleTypes.primary.accent,
246 },
247 },
248
249 legacyStyles,
250 };
251};
diff --git a/src/themes/index.ts b/src/themes/index.ts
new file mode 100644
index 000000000..27be4d04b
--- /dev/null
+++ b/src/themes/index.ts
@@ -0,0 +1,21 @@
1import makeDarkThemeConfig from './dark';
2import makeDefaultThemeConfig from './default';
3import { themeBrandPrimary } from './legacy';
4
5export enum ThemeType {
6 default = 'default',
7 dark = 'dark',
8}
9
10export function theme(
11 themeId: ThemeType,
12 brandColor: string = themeBrandPrimary,
13) {
14 return themeId === ThemeType.dark
15 ? makeDarkThemeConfig(brandColor)
16 : makeDefaultThemeConfig(brandColor);
17}
18
19const defaultThemeConfigWithDefaultAccentColor = makeDefaultThemeConfig(themeBrandPrimary);
20
21export type Theme = typeof defaultThemeConfigWithDefaultAccentColor;
diff --git a/src/themes/legacy/index.ts b/src/themes/legacy/index.ts
new file mode 100644
index 000000000..c6105a4e2
--- /dev/null
+++ b/src/themes/legacy/index.ts
@@ -0,0 +1,40 @@
1import { DEFAULT_ACCENT_COLOR } from '../../config';
2
3/* legacy config, injected into sass */
4export const themeBrandPrimary = DEFAULT_ACCENT_COLOR;
5export const themeBrandSuccess = '#5cb85c';
6export const themeBrandInfo = '#5bc0de';
7export const themeBrandWarning = '#FF9F00';
8export const themeBrandDanger = '#d9534f';
9
10export const themeGrayDark = '#373a3c';
11export const themeGray = '#55595c';
12export const themeGrayLight = '#818a91';
13export const themeGrayLighter = '#eceeef';
14export const themeGrayLightest = '#f7f7f9';
15
16export const themeBorderRadius = '6px';
17export const themeBorderRadiusSmall = '3px';
18
19export const themeSidebarWidth = '68px';
20
21export const themeTextColor = themeGrayDark;
22
23export const themeTransitionTime = '.5s';
24
25export const themeInsetShadow = 'inset 0 2px 5px rgba(0, 0, 0, .03)';
26
27export const darkThemeBlack = '#1A1A1A';
28
29export const darkThemeGrayDarkest = '#1E1E1E';
30export const darkThemeGrayDarker = '#2D2F31';
31export const darkThemeGrayDark = '#383A3B';
32
33export const darkThemeGray = '#47494B';
34
35export const darkThemeGrayLight = '#515355';
36export const darkThemeGrayLighter = '#8a8b8b';
37export const darkThemeGrayLightest = '#FFFFFF';
38
39export const darkThemeGraySmoke = '#CED0D1';
40export const darkThemeTextColor = '#FFFFFF';
diff --git a/src/webview/badge.ts b/src/webview/badge.ts
index 8e8b66c0c..fb696723d 100644
--- a/src/webview/badge.ts
+++ b/src/webview/badge.ts
@@ -3,7 +3,7 @@ import { ipcRenderer } from 'electron';
3const debug = require('debug')('Ferdi:Plugin:BadgeHandler'); 3const debug = require('debug')('Ferdi:Plugin:BadgeHandler');
4 4
5export class BadgeHandler { 5export class BadgeHandler {
6 countCache: { direct: number; indirect: number; }; 6 countCache: { direct: number; indirect: number };
7 7
8 constructor() { 8 constructor() {
9 this.countCache = { 9 this.countCache = {
@@ -26,14 +26,19 @@ export class BadgeHandler {
26 return Math.max(adjustedNumber, 0); 26 return Math.max(adjustedNumber, 0);
27 } 27 }
28 28
29 setBadge(direct: string | number | undefined | null, indirect: string | number | undefined | null) { 29 setBadge(
30 direct: string | number | undefined | null,
31 indirect: string | number | undefined | null,
32 ) {
30 const count = { 33 const count = {
31 direct: this.safeParseInt(direct), 34 direct: this.safeParseInt(direct),
32 indirect: this.safeParseInt(indirect), 35 indirect: this.safeParseInt(indirect),
33 }; 36 };
34 37
35 if (this.countCache.direct.toString() === count.direct.toString() 38 if (
36 && this.countCache.indirect.toString() === count.indirect.toString()) { 39 this.countCache.direct.toString() === count.direct.toString() &&
40 this.countCache.indirect.toString() === count.indirect.toString()
41 ) {
37 return; 42 return;
38 } 43 }
39 44
diff --git a/src/webview/contextMenu.js b/src/webview/contextMenu.js
deleted file mode 100644
index 567a2d470..000000000
--- a/src/webview/contextMenu.js
+++ /dev/null
@@ -1,20 +0,0 @@
1import { getCurrentWebContents } from '@electron/remote';
2import ContextMenuBuilder from './contextMenuBuilder';
3
4const webContents = getCurrentWebContents();
5
6export default async function setupContextMenu(isSpellcheckEnabled, getDefaultSpellcheckerLanguage, getSpellcheckerLanguage, getSearchEngine, getClipboardNotifications) {
7 const contextMenuBuilder = new ContextMenuBuilder(
8 webContents,
9 );
10
11 webContents.on('context-menu', (e, props) => {
12 // TODO?: e.preventDefault();
13 contextMenuBuilder.showPopupMenu(
14 { ...props, searchEngine: getSearchEngine(), clipboardNotifications: getClipboardNotifications() },
15 isSpellcheckEnabled(),
16 getDefaultSpellcheckerLanguage(),
17 getSpellcheckerLanguage(),
18 );
19 });
20}
diff --git a/src/webview/contextMenu.ts b/src/webview/contextMenu.ts
new file mode 100644
index 000000000..72f927ef4
--- /dev/null
+++ b/src/webview/contextMenu.ts
@@ -0,0 +1,29 @@
1import { getCurrentWebContents } from '@electron/remote';
2import { ContextMenuBuilder } from './contextMenuBuilder';
3
4const webContents = getCurrentWebContents();
5
6export default async function setupContextMenu(
7 isSpellcheckEnabled: () => void,
8 getDefaultSpellcheckerLanguage: () => void,
9 getSpellcheckerLanguage: () => void,
10 getSearchEngine: () => void,
11 getClipboardNotifications: () => void,
12) {
13 const contextMenuBuilder = new ContextMenuBuilder(webContents);
14
15 webContents.on('context-menu', (_e, props) => {
16 // TODO?: e.preventDefault();
17 contextMenuBuilder.showPopupMenu(
18 {
19 ...props,
20 searchEngine: getSearchEngine(),
21 clipboardNotifications: getClipboardNotifications(),
22 },
23 // @ts-expect-error Expected 1 arguments, but got 4.
24 isSpellcheckEnabled(),
25 getDefaultSpellcheckerLanguage(),
26 getSpellcheckerLanguage(),
27 );
28 });
29}
diff --git a/src/webview/contextMenuBuilder.js b/src/webview/contextMenuBuilder.ts
index 938eade1e..7b8c6cb2d 100644
--- a/src/webview/contextMenuBuilder.js
+++ b/src/webview/contextMenuBuilder.ts
@@ -6,7 +6,7 @@
6 * 6 *
7 * Source: https://github.com/electron-userland/electron-spellchecker/blob/master/src/context-menu-builder.js 7 * Source: https://github.com/electron-userland/electron-spellchecker/blob/master/src/context-menu-builder.js
8 */ 8 */
9// eslint-disable-next-line no-unused-vars 9
10import { clipboard, ipcRenderer, nativeImage, WebContents } from 'electron'; 10import { clipboard, ipcRenderer, nativeImage, WebContents } from 'electron';
11import { Menu, MenuItem } from '@electron/remote'; 11import { Menu, MenuItem } from '@electron/remote';
12import { cmdOrCtrlShortcutKey, isMac } from '../environment'; 12import { cmdOrCtrlShortcutKey, isMac } from '../environment';
@@ -14,17 +14,38 @@ import { cmdOrCtrlShortcutKey, isMac } from '../environment';
14import { SEARCH_ENGINE_NAMES, SEARCH_ENGINE_URLS } from '../config'; 14import { SEARCH_ENGINE_NAMES, SEARCH_ENGINE_URLS } from '../config';
15import { openExternalUrl } from '../helpers/url-helpers'; 15import { openExternalUrl } from '../helpers/url-helpers';
16 16
17const { URL } = require('url'); 17function matchesWord(string: string) {
18
19function matchesWord(string) {
20 const regex = 18 const regex =
21 /[A-Za-z\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]+/g; 19 /[A-Za-z\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]+/g;
22 20
23 return string.match(regex); 21 return string.match(regex);
24} 22}
25 23
24interface ContextMenuStringTable {
25 lookUpDefinition: ({ word }: { word: string }) => string;
26 cut: () => string;
27 copy: () => string;
28 paste: () => string;
29 pasteAndMatchStyle: () => string;
30 searchWith: ({ searchEngine }: { searchEngine: string }) => string;
31 openLinkUrl: () => string;
32 openLinkInFerdiUrl: () => string;
33 openInBrowser: () => string;
34 copyLinkUrl: () => string;
35 copyImageUrl: () => string;
36 copyImage: () => string;
37 downloadImage: () => string;
38 addToDictionary: () => string;
39 goBack: () => string;
40 goForward: () => string;
41 copyPageUrl: () => string;
42 goToHomePage: () => string;
43 copyMail: () => string;
44 inspectElement: () => string;
45}
46
26// TODO: Need to externalize for i18n 47// TODO: Need to externalize for i18n
27const contextMenuStringTable = { 48const contextMenuStringTable: ContextMenuStringTable = {
28 lookUpDefinition: ({ word }) => `Look Up "${word}"`, 49 lookUpDefinition: ({ word }) => `Look Up "${word}"`,
29 cut: () => 'Cut', 50 cut: () => 'Cut',
30 copy: () => 'Copy', 51 copy: () => 'Copy',
@@ -54,16 +75,31 @@ const contextMenuStringTable = {
54 * which we use to generate the menu. We also use the spell-check information to 75 * which we use to generate the menu. We also use the spell-check information to
55 * generate suggestions. 76 * generate suggestions.
56 */ 77 */
57module.exports = class ContextMenuBuilder { 78export class ContextMenuBuilder {
79 debugMode: boolean;
80
81 processMenu: (
82 menu: Electron.CrossProcessExports.Menu,
83 ) => Electron.CrossProcessExports.Menu;
84
85 menu: Electron.CrossProcessExports.Menu | null;
86
87 stringTable: ContextMenuStringTable;
88
89 getWebContents: () => Electron.WebContents;
90
58 /** 91 /**
59 * Creates an instance of ContextMenuBuilder 92 * Creates an instance of ContextMenuBuilder
60 * 93 *
61 * @param {WebContents} webContents Current webContents 94 * @param webContents Current webContents
62 * @param {Boolean} debugMode If true, display the "Inspect Element" menu item. 95 * @param debugMode If true, display the "Inspect Element" menu item.
63 * @param {function} processMenu If passed, this method will be passed the menu to change 96 * @param processMenu If passed, this method will be passed the menu to change it prior to display. Signature: (menu, info) => menu
64 * it prior to display. Signature: (menu, info) => menu
65 */ 97 */
66 constructor(webContents, debugMode = false, processMenu = m => m) { 98 constructor(
99 webContents: WebContents,
100 debugMode: boolean = false,
101 processMenu = (menu: Electron.CrossProcessExports.Menu) => menu,
102 ) {
67 this.debugMode = debugMode; 103 this.debugMode = debugMode;
68 this.processMenu = processMenu; 104 this.processMenu = processMenu;
69 this.menu = null; 105 this.menu = null;
@@ -77,12 +113,11 @@ module.exports = class ContextMenuBuilder {
77 * allows to change string in runtime. All formatters are simply typeof () => string, except 113 * allows to change string in runtime. All formatters are simply typeof () => string, except
78 * lookUpDefinition provides word, ({word}) => string. 114 * lookUpDefinition provides word, ({word}) => string.
79 * 115 *
80 * @param {Object} stringTable The object contains string foramtter function for context menu. 116 * @param stringTable The object contains string formatter function for context menu.
81 * It is allowed to specify only certain menu string as necessary, which will makes other string 117 * It is allowed to specify only certain menu string as necessary, which will makes other string
82 * fall backs to default. 118 * fall backs to default.
83 *
84 */ 119 */
85 setAlternateStringFormatter(stringTable) { 120 setAlternateStringFormatter(stringTable: ContextMenuStringTable) {
86 this.stringTable = Object.assign(this.stringTable, stringTable); 121 this.stringTable = Object.assign(this.stringTable, stringTable);
87 } 122 }
88 123
@@ -90,12 +125,9 @@ module.exports = class ContextMenuBuilder {
90 * Shows a popup menu given the information returned from the context-menu 125 * Shows a popup menu given the information returned from the context-menu
91 * event. This is probably the only method you need to call in this class. 126 * event. This is probably the only method you need to call in this class.
92 * 127 *
93 * @param {Object} contextInfo The object returned from the 'context-menu' 128 * @param contextInfo The object returned from the 'context-menu' Electron event.
94 * Electron event.
95 *
96 * @return {Promise} Completion
97 */ 129 */
98 async showPopupMenu(contextInfo) { 130 async showPopupMenu(contextInfo: Electron.ContextMenuParams): Promise<void> {
99 const menu = await this.buildMenuForElement(contextInfo); 131 const menu = await this.buildMenuForElement(contextInfo);
100 if (!menu) return; 132 if (!menu) return;
101 menu.popup(); 133 menu.popup();
@@ -105,10 +137,10 @@ module.exports = class ContextMenuBuilder {
105 * Builds a context menu specific to the given info that _would_ be shown 137 * Builds a context menu specific to the given info that _would_ be shown
106 * immediately by {{showPopupMenu}}. Use this to add your own menu items to 138 * immediately by {{showPopupMenu}}. Use this to add your own menu items to
107 * the list but use most of the default behavior. 139 * the list but use most of the default behavior.
108 *
109 * @return {Promise<Menu>} The newly created `Menu`
110 */ 140 */
111 async buildMenuForElement(info) { 141 async buildMenuForElement(
142 info: Electron.ContextMenuParams,
143 ): Promise<Electron.CrossProcessExports.Menu> {
112 if (info.linkURL && info.linkURL.length > 0) { 144 if (info.linkURL && info.linkURL.length > 0) {
113 return this.buildMenuForLink(info); 145 return this.buildMenuForLink(info);
114 } 146 }
@@ -132,7 +164,9 @@ module.exports = class ContextMenuBuilder {
132 * 164 *
133 * @return {Menu} The `Menu` 165 * @return {Menu} The `Menu`
134 */ 166 */
135 buildMenuForTextInput(menuInfo) { 167 buildMenuForTextInput(
168 menuInfo: Electron.ContextMenuParams,
169 ): Electron.CrossProcessExports.Menu {
136 const menu = new Menu(); 170 const menu = new Menu();
137 171
138 this.addSpellingItems(menu, menuInfo); 172 this.addSpellingItems(menu, menuInfo);
@@ -143,8 +177,10 @@ module.exports = class ContextMenuBuilder {
143 this.addPaste(menu, menuInfo); 177 this.addPaste(menu, menuInfo);
144 this.addPastePlain(menu, menuInfo); 178 this.addPastePlain(menu, menuInfo);
145 this.addInspectElement(menu, menuInfo); 179 this.addInspectElement(menu, menuInfo);
180 // @ts-expect-error Expected 1 arguments, but got 2.
146 this.processMenu(menu, menuInfo); 181 this.processMenu(menu, menuInfo);
147 182
183 // @ts-expect-error Expected 2 arguments, but got 1.
148 this.copyPageUrl(menu); 184 this.copyPageUrl(menu);
149 this.goToHomePage(menu, menuInfo); 185 this.goToHomePage(menu, menuInfo);
150 this.openInBrowser(menu, menuInfo); 186 this.openInBrowser(menu, menuInfo);
@@ -157,7 +193,9 @@ module.exports = class ContextMenuBuilder {
157 * 193 *
158 * @return {Menu} The `Menu` 194 * @return {Menu} The `Menu`
159 */ 195 */
160 buildMenuForLink(menuInfo) { 196 buildMenuForLink(
197 menuInfo: Electron.ContextMenuParams,
198 ): Electron.CrossProcessExports.Menu {
161 const menu = new Menu(); 199 const menu = new Menu();
162 const isEmailAddress = menuInfo.linkURL.startsWith('mailto:'); 200 const isEmailAddress = menuInfo.linkURL.startsWith('mailto:');
163 201
@@ -170,6 +208,7 @@ module.exports = class ContextMenuBuilder {
170 const url = isEmailAddress ? menuInfo.linkText : menuInfo.linkURL; 208 const url = isEmailAddress ? menuInfo.linkText : menuInfo.linkURL;
171 clipboard.writeText(url); 209 clipboard.writeText(url);
172 this._sendNotificationOnClipboardEvent( 210 this._sendNotificationOnClipboardEvent(
211 // @ts-expect-error Property 'clipboardNotifications' does not exist on type 'ContextMenuParams'.
173 menuInfo.clipboardNotifications, 212 menuInfo.clipboardNotifications,
174 () => `Link URL copied: ${url}`, 213 () => `Link URL copied: ${url}`,
175 ); 214 );
@@ -200,11 +239,13 @@ module.exports = class ContextMenuBuilder {
200 } 239 }
201 240
202 this.addInspectElement(menu, menuInfo); 241 this.addInspectElement(menu, menuInfo);
242 // @ts-expect-error Expected 1 arguments, but got 2.
203 this.processMenu(menu, menuInfo); 243 this.processMenu(menu, menuInfo);
204 244
205 this.addSeparator(menu); 245 this.addSeparator(menu);
206 this.goBack(menu); 246 this.goBack(menu);
207 this.goForward(menu); 247 this.goForward(menu);
248 // @ts-expect-error Expected 2 arguments, but got 1.
208 this.copyPageUrl(menu); 249 this.copyPageUrl(menu);
209 this.goToHomePage(menu, menuInfo); 250 this.goToHomePage(menu, menuInfo);
210 this.openInBrowser(menu, menuInfo); 251 this.openInBrowser(menu, menuInfo);
@@ -214,15 +255,16 @@ module.exports = class ContextMenuBuilder {
214 255
215 /** 256 /**
216 * Builds a menu applicable to a text field. 257 * Builds a menu applicable to a text field.
217 *
218 * @return {Menu} The `Menu`
219 */ 258 */
220 buildMenuForText(menuInfo) { 259 buildMenuForText(
260 menuInfo: Electron.ContextMenuParams,
261 ): Electron.CrossProcessExports.Menu {
221 const menu = new Menu(); 262 const menu = new Menu();
222 263
223 this.addSearchItems(menu, menuInfo); 264 this.addSearchItems(menu, menuInfo);
224 this.addCopy(menu, menuInfo); 265 this.addCopy(menu, menuInfo);
225 this.addInspectElement(menu, menuInfo); 266 this.addInspectElement(menu, menuInfo);
267 // @ts-expect-error Expected 1 arguments, but got 2.
226 this.processMenu(menu, menuInfo); 268 this.processMenu(menu, menuInfo);
227 269
228 this.addSeparator(menu); 270 this.addSeparator(menu);
@@ -240,13 +282,16 @@ module.exports = class ContextMenuBuilder {
240 * 282 *
241 * @return {Menu} The `Menu` 283 * @return {Menu} The `Menu`
242 */ 284 */
243 buildMenuForImage(menuInfo) { 285 buildMenuForImage(
286 menuInfo: Electron.ContextMenuParams,
287 ): Electron.CrossProcessExports.Menu {
244 const menu = new Menu(); 288 const menu = new Menu();
245 289
246 if (this.isSrcUrlValid(menuInfo)) { 290 if (this.isSrcUrlValid(menuInfo)) {
247 this.addImageItems(menu, menuInfo); 291 this.addImageItems(menu, menuInfo);
248 } 292 }
249 this.addInspectElement(menu, menuInfo); 293 this.addInspectElement(menu, menuInfo);
294 // @ts-expect-error Expected 1 arguments, but got 2.
250 this.processMenu(menu, menuInfo); 295 this.processMenu(menu, menuInfo);
251 296
252 return menu; 297 return menu;
@@ -256,14 +301,17 @@ module.exports = class ContextMenuBuilder {
256 * Checks if the current text selection contains a single misspelled word and 301 * Checks if the current text selection contains a single misspelled word and
257 * if so, adds suggested spellings as individual menu items. 302 * if so, adds suggested spellings as individual menu items.
258 */ 303 */
259 addSpellingItems(menu, menuInfo) { 304 addSpellingItems(
305 menu: Electron.CrossProcessExports.Menu,
306 menuInfo: Electron.ContextMenuParams,
307 ) {
260 const webContents = this.getWebContents(); 308 const webContents = this.getWebContents();
261 // Add each spelling suggestion 309 // Add each spelling suggestion
262 for (const suggestion of menuInfo.dictionarySuggestions) { 310 for (const suggestion of menuInfo.dictionarySuggestions) {
263 menu.append( 311 menu.append(
264 new MenuItem({ 312 new MenuItem({
265 label: suggestion, 313 label: suggestion,
266 // eslint-disable-next-line no-loop-func 314
267 click: () => webContents.replaceMisspelling(suggestion), 315 click: () => webContents.replaceMisspelling(suggestion),
268 }), 316 }),
269 ); 317 );
@@ -288,7 +336,10 @@ module.exports = class ContextMenuBuilder {
288 /** 336 /**
289 * Adds search-related menu items. 337 * Adds search-related menu items.
290 */ 338 */
291 addSearchItems(menu, menuInfo) { 339 addSearchItems(
340 menu: Electron.CrossProcessExports.Menu,
341 menuInfo: Electron.ContextMenuParams,
342 ) {
292 if (!menuInfo.selectionText || menuInfo.selectionText.length === 0) { 343 if (!menuInfo.selectionText || menuInfo.selectionText.length === 0) {
293 return menu; 344 return menu;
294 } 345 }
@@ -313,9 +364,11 @@ module.exports = class ContextMenuBuilder {
313 364
314 const search = new MenuItem({ 365 const search = new MenuItem({
315 label: this.stringTable.searchWith({ 366 label: this.stringTable.searchWith({
367 // @ts-expect-error Property 'searchEngine' does not exist on type 'ContextMenuParams'.
316 searchEngine: SEARCH_ENGINE_NAMES[menuInfo.searchEngine], 368 searchEngine: SEARCH_ENGINE_NAMES[menuInfo.searchEngine],
317 }), 369 }),
318 click: () => { 370 click: () => {
371 // @ts-expect-error Property 'searchEngine' does not exist on type 'ContextMenuParams'.
319 const url = SEARCH_ENGINE_URLS[menuInfo.searchEngine]({ 372 const url = SEARCH_ENGINE_URLS[menuInfo.searchEngine]({
320 searchTerm: encodeURIComponent(menuInfo.selectionText), 373 searchTerm: encodeURIComponent(menuInfo.selectionText),
321 }); 374 });
@@ -329,22 +382,28 @@ module.exports = class ContextMenuBuilder {
329 return menu; 382 return menu;
330 } 383 }
331 384
332 isSrcUrlValid(menuInfo) { 385 isSrcUrlValid(menuInfo: Electron.ContextMenuParams) {
333 return menuInfo.srcURL && menuInfo.srcURL.length > 0; 386 return menuInfo.srcURL && menuInfo.srcURL.length > 0;
334 } 387 }
335 388
336 /** 389 /**
337 * Adds "Copy Image" and "Copy Image URL" items when `src` is valid. 390 * Adds "Copy Image" and "Copy Image URL" items when `src` is valid.
338 */ 391 */
339 addImageItems(menu, menuInfo) { 392 addImageItems(
393 menu: Electron.CrossProcessExports.Menu,
394 menuInfo: Electron.ContextMenuParams,
395 ) {
340 const copyImage = new MenuItem({ 396 const copyImage = new MenuItem({
341 label: this.stringTable.copyImage(), 397 label: this.stringTable.copyImage(),
342 click: () => { 398 click: () => {
343 const result = this.convertImageToBase64(menuInfo.srcURL, dataURL => 399 const result = this.convertImageToBase64(
344 clipboard.writeImage(nativeImage.createFromDataURL(dataURL)), 400 menuInfo.srcURL,
401 (dataURL: string) =>
402 clipboard.writeImage(nativeImage.createFromDataURL(dataURL)),
345 ); 403 );
346 404
347 this._sendNotificationOnClipboardEvent( 405 this._sendNotificationOnClipboardEvent(
406 // @ts-expect-error Property 'clipboardNotifications' does not exist on type 'ContextMenuParams'.
348 menuInfo.clipboardNotifications, 407 menuInfo.clipboardNotifications,
349 () => `Image copied from URL: ${menuInfo.srcURL}`, 408 () => `Image copied from URL: ${menuInfo.srcURL}`,
350 ); 409 );
@@ -359,6 +418,7 @@ module.exports = class ContextMenuBuilder {
359 click: () => { 418 click: () => {
360 const result = clipboard.writeText(menuInfo.srcURL); 419 const result = clipboard.writeText(menuInfo.srcURL);
361 this._sendNotificationOnClipboardEvent( 420 this._sendNotificationOnClipboardEvent(
421 // @ts-expect-error Property 'clipboardNotifications' does not exist on type 'ContextMenuParams'.
362 menuInfo.clipboardNotifications, 422 menuInfo.clipboardNotifications,
363 () => `Image URL copied: ${menuInfo.srcURL}`, 423 () => `Image URL copied: ${menuInfo.srcURL}`,
364 ); 424 );
@@ -374,7 +434,7 @@ module.exports = class ContextMenuBuilder {
374 label: this.stringTable.downloadImage(), 434 label: this.stringTable.downloadImage(),
375 click: () => { 435 click: () => {
376 const urlWithoutBlob = menuInfo.srcURL.slice(5); 436 const urlWithoutBlob = menuInfo.srcURL.slice(5);
377 this.convertImageToBase64(menuInfo.srcURL, dataURL => { 437 this.convertImageToBase64(menuInfo.srcURL, (dataURL: any) => {
378 const url = new window.URL(urlWithoutBlob); 438 const url = new window.URL(urlWithoutBlob);
379 const fileName = url.pathname.slice(1); 439 const fileName = url.pathname.slice(1);
380 ipcRenderer.send('download-file', { 440 ipcRenderer.send('download-file', {
@@ -386,6 +446,7 @@ module.exports = class ContextMenuBuilder {
386 }); 446 });
387 }); 447 });
388 this._sendNotificationOnClipboardEvent( 448 this._sendNotificationOnClipboardEvent(
449 // @ts-expect-error Property 'clipboardNotifications' does not exist on type 'ContextMenuParams'.
389 menuInfo.clipboardNotifications, 450 menuInfo.clipboardNotifications,
390 () => `Image downloaded: ${urlWithoutBlob}`, 451 () => `Image downloaded: ${urlWithoutBlob}`,
391 ); 452 );
@@ -401,7 +462,10 @@ module.exports = class ContextMenuBuilder {
401 /** 462 /**
402 * Adds the Cut menu item 463 * Adds the Cut menu item
403 */ 464 */
404 addCut(menu, menuInfo) { 465 addCut(
466 menu: Electron.CrossProcessExports.Menu,
467 menuInfo: Electron.ContextMenuParams,
468 ) {
405 const webContents = this.getWebContents(); 469 const webContents = this.getWebContents();
406 menu.append( 470 menu.append(
407 new MenuItem({ 471 new MenuItem({
@@ -418,7 +482,10 @@ module.exports = class ContextMenuBuilder {
418 /** 482 /**
419 * Adds the Copy menu item. 483 * Adds the Copy menu item.
420 */ 484 */
421 addCopy(menu, menuInfo) { 485 addCopy(
486 menu: Electron.CrossProcessExports.Menu,
487 menuInfo: Electron.ContextMenuParams,
488 ) {
422 const webContents = this.getWebContents(); 489 const webContents = this.getWebContents();
423 menu.append( 490 menu.append(
424 new MenuItem({ 491 new MenuItem({
@@ -435,7 +502,10 @@ module.exports = class ContextMenuBuilder {
435 /** 502 /**
436 * Adds the Paste menu item. 503 * Adds the Paste menu item.
437 */ 504 */
438 addPaste(menu, menuInfo) { 505 addPaste(
506 menu: Electron.CrossProcessExports.Menu,
507 menuInfo: Electron.ContextMenuParams,
508 ) {
439 const webContents = this.getWebContents(); 509 const webContents = this.getWebContents();
440 menu.append( 510 menu.append(
441 new MenuItem({ 511 new MenuItem({
@@ -449,7 +519,10 @@ module.exports = class ContextMenuBuilder {
449 return menu; 519 return menu;
450 } 520 }
451 521
452 addPastePlain(menu, menuInfo) { 522 addPastePlain(
523 menu: Electron.CrossProcessExports.Menu,
524 menuInfo: Electron.ContextMenuParams,
525 ) {
453 if ( 526 if (
454 menuInfo.editFlags.canPaste && 527 menuInfo.editFlags.canPaste &&
455 !menuInfo.linkText && 528 !menuInfo.linkText &&
@@ -469,7 +542,7 @@ module.exports = class ContextMenuBuilder {
469 /** 542 /**
470 * Adds a separator item. 543 * Adds a separator item.
471 */ 544 */
472 addSeparator(menu) { 545 addSeparator(menu: Electron.CrossProcessExports.Menu) {
473 menu.append(new MenuItem({ type: 'separator' })); 546 menu.append(new MenuItem({ type: 'separator' }));
474 return menu; 547 return menu;
475 } 548 }
@@ -477,7 +550,11 @@ module.exports = class ContextMenuBuilder {
477 /** 550 /**
478 * Adds the "Inspect Element" menu item. 551 * Adds the "Inspect Element" menu item.
479 */ 552 */
480 addInspectElement(menu, menuInfo, needsSeparator = true) { 553 addInspectElement(
554 menu: Electron.CrossProcessExports.Menu,
555 menuInfo: Electron.ContextMenuParams,
556 needsSeparator = true,
557 ) {
481 const webContents = this.getWebContents(); 558 const webContents = this.getWebContents();
482 if (!this.debugMode) return menu; 559 if (!this.debugMode) return menu;
483 if (needsSeparator) this.addSeparator(menu); 560 if (needsSeparator) this.addSeparator(menu);
@@ -498,21 +575,30 @@ module.exports = class ContextMenuBuilder {
498 * @param {Function} callback A callback that will be invoked with the result 575 * @param {Function} callback A callback that will be invoked with the result
499 * @param {String} outputFormat The image format to use, defaults to 'image/png' 576 * @param {String} outputFormat The image format to use, defaults to 'image/png'
500 */ 577 */
501 convertImageToBase64(url, callback, outputFormat = 'image/png') { 578 convertImageToBase64(
502 let canvas = document.createElement('canvas'); 579 url: string,
580 callback: {
581 (dataURL: any): void;
582 (dataURL: any): void;
583 (arg0: string): void;
584 },
585 outputFormat: string = 'image/png',
586 ) {
587 let canvas: HTMLCanvasElement | null = document.createElement('canvas');
503 const ctx = canvas.getContext('2d'); 588 const ctx = canvas.getContext('2d');
504 // eslint-disable-next-line no-undef
505 const img = new Image(); 589 const img = new Image();
506 img.crossOrigin = 'Anonymous'; 590 img.crossOrigin = 'Anonymous';
507 591
508 img.addEventListener('load', () => { 592 img.addEventListener('load', () => {
509 canvas.height = img.height; 593 if (canvas) {
510 canvas.width = img.width; 594 canvas.height = img.height;
511 ctx?.drawImage(img, 0, 0); 595 canvas.width = img.width;
512 596 ctx?.drawImage(img, 0, 0);
513 const dataURL = canvas.toDataURL(outputFormat); 597
514 canvas = null; 598 const dataURL = canvas.toDataURL(outputFormat);
515 callback(dataURL); 599 canvas = null;
600 callback(dataURL);
601 }
516 }); 602 });
517 603
518 img.src = url; 604 img.src = url;
@@ -521,7 +607,7 @@ module.exports = class ContextMenuBuilder {
521 /** 607 /**
522 * Adds the 'go back' menu item 608 * Adds the 'go back' menu item
523 */ 609 */
524 goBack(menu) { 610 goBack(menu: Electron.CrossProcessExports.Menu) {
525 const webContents = this.getWebContents(); 611 const webContents = this.getWebContents();
526 menu.append( 612 menu.append(
527 new MenuItem({ 613 new MenuItem({
@@ -538,7 +624,7 @@ module.exports = class ContextMenuBuilder {
538 /** 624 /**
539 * Adds the 'go forward' menu item 625 * Adds the 'go forward' menu item
540 */ 626 */
541 goForward(menu) { 627 goForward(menu: Electron.CrossProcessExports.Menu) {
542 const webContents = this.getWebContents(); 628 const webContents = this.getWebContents();
543 menu.append( 629 menu.append(
544 new MenuItem({ 630 new MenuItem({
@@ -555,7 +641,10 @@ module.exports = class ContextMenuBuilder {
555 /** 641 /**
556 * Adds the 'copy page url' menu item. 642 * Adds the 'copy page url' menu item.
557 */ 643 */
558 copyPageUrl(menu, menuInfo) { 644 copyPageUrl(
645 menu: Electron.CrossProcessExports.Menu,
646 menuInfo: Electron.ContextMenuParams,
647 ) {
559 menu.append( 648 menu.append(
560 new MenuItem({ 649 new MenuItem({
561 label: this.stringTable.copyPageUrl(), 650 label: this.stringTable.copyPageUrl(),
@@ -563,7 +652,8 @@ module.exports = class ContextMenuBuilder {
563 click: () => { 652 click: () => {
564 clipboard.writeText(window.location.href); 653 clipboard.writeText(window.location.href);
565 this._sendNotificationOnClipboardEvent( 654 this._sendNotificationOnClipboardEvent(
566 menuInfo.clipboardNotifications, 655 // @ts-expect-error Property 'clipboardNotifications' does not exist on type 'ContextMenuParams'.
656 menuInfo?.clipboardNotifications,
567 () => `Page URL copied: ${window.location.href}`, 657 () => `Page URL copied: ${window.location.href}`,
568 ); 658 );
569 }, 659 },
@@ -576,8 +666,11 @@ module.exports = class ContextMenuBuilder {
576 /** 666 /**
577 * Adds the 'go to home' menu item. 667 * Adds the 'go to home' menu item.
578 */ 668 */
579 goToHomePage(menu, menuInfo) { 669 goToHomePage(
580 const baseURL = new URL(menuInfo.pageURL); 670 menu: Electron.CrossProcessExports.Menu,
671 menuInfo: Electron.ContextMenuParams,
672 ) {
673 const baseURL = new window.URL(menuInfo.pageURL);
581 menu.append( 674 menu.append(
582 new MenuItem({ 675 new MenuItem({
583 label: this.stringTable.goToHomePage(), 676 label: this.stringTable.goToHomePage(),
@@ -596,7 +689,10 @@ module.exports = class ContextMenuBuilder {
596 /** 689 /**
597 * Adds the 'open in browser' menu item. 690 * Adds the 'open in browser' menu item.
598 */ 691 */
599 openInBrowser(menu, menuInfo) { 692 openInBrowser(
693 menu: Electron.CrossProcessExports.Menu,
694 menuInfo: Electron.ContextMenuParams,
695 ) {
600 menu.append( 696 menu.append(
601 new MenuItem({ 697 new MenuItem({
602 label: this.stringTable.openInBrowser(), 698 label: this.stringTable.openInBrowser(),
@@ -610,7 +706,10 @@ module.exports = class ContextMenuBuilder {
610 return menu; 706 return menu;
611 } 707 }
612 708
613 _sendNotificationOnClipboardEvent(isDisabled, notificationText) { 709 _sendNotificationOnClipboardEvent(
710 isDisabled: boolean,
711 notificationText: () => string,
712 ) {
614 if (isDisabled) { 713 if (isDisabled) {
615 return; 714 return;
616 } 715 }
@@ -619,4 +718,4 @@ module.exports = class ContextMenuBuilder {
619 body: notificationText(), 718 body: notificationText(),
620 }); 719 });
621 } 720 }
622}; 721}
diff --git a/src/webview/darkmode/custom.js b/src/webview/darkmode/custom.ts
index f767f5755..f767f5755 100644
--- a/src/webview/darkmode/custom.js
+++ b/src/webview/darkmode/custom.ts
diff --git a/src/webview/darkmode/ignore.js b/src/webview/darkmode/ignore.js
deleted file mode 100644
index 110df364f..000000000
--- a/src/webview/darkmode/ignore.js
+++ /dev/null
@@ -1,3 +0,0 @@
1export default [
2 'discordapp.com',
3];
diff --git a/src/webview/darkmode/ignore.ts b/src/webview/darkmode/ignore.ts
new file mode 100644
index 000000000..daa25d10c
--- /dev/null
+++ b/src/webview/darkmode/ignore.ts
@@ -0,0 +1 @@
export default ['discordapp.com'];
diff --git a/src/webview/find.js b/src/webview/find.ts
index 040811d68..0665d9670 100644
--- a/src/webview/find.js
+++ b/src/webview/find.ts
@@ -3,11 +3,15 @@ import { FindInPage as ElectronFindInPage } from 'electron-find';
3 3
4// Shim to expose webContents functionality to electron-find without @electron/remote 4// Shim to expose webContents functionality to electron-find without @electron/remote
5const webContentsShim = { 5const webContentsShim = {
6 findInPage: (text, options = {}) => ipcRenderer.sendSync('find-in-page', text, options), 6 findInPage: (text: string, options = {}) =>
7 stopFindInPage: (action) => { 7 ipcRenderer.sendSync('find-in-page', text, options),
8 stopFindInPage: (action: any) => {
8 ipcRenderer.sendSync('stop-find-in-page', action); 9 ipcRenderer.sendSync('stop-find-in-page', action);
9 }, 10 },
10 on: (eventName, listener) => { 11 on: (
12 eventName: string,
13 listener: (arg0: { sender: undefined }, arg1: any) => void,
14 ): void => {
11 if (eventName === 'found-in-page') { 15 if (eventName === 'found-in-page') {
12 ipcRenderer.on('found-in-page', (_, result) => { 16 ipcRenderer.on('found-in-page', (_, result) => {
13 listener({ sender: this }, result); 17 listener({ sender: this }, result);
diff --git a/src/webview/notifications.js b/src/webview/notifications.ts
index 22960d818..73124b9a9 100644
--- a/src/webview/notifications.js
+++ b/src/webview/notifications.ts
@@ -5,9 +5,10 @@ import { v1 as uuidV1 } from 'uuid';
5const debug = require('debug')('Ferdi:Notifications'); 5const debug = require('debug')('Ferdi:Notifications');
6 6
7export class NotificationsHandler { 7export class NotificationsHandler {
8 onNotify = data => data; 8 onNotify = (data: { title: string; options: any; notificationId: string }) =>
9 data;
9 10
10 displayNotification(title, options) { 11 displayNotification(title: string, options: any) {
11 return new Promise(resolve => { 12 return new Promise(resolve => {
12 debug('New notification', title, options); 13 debug('New notification', title, options);
13 14
@@ -23,7 +24,7 @@ export class NotificationsHandler {
23 ); 24 );
24 25
25 ipcRenderer.once(`notification-onclick:${notificationId}`, () => { 26 ipcRenderer.once(`notification-onclick:${notificationId}`, () => {
26 resolve(); 27 resolve(true);
27 }); 28 });
28 }); 29 });
29 } 30 }
diff --git a/src/webview/screenshare.js b/src/webview/screenshare.ts
index e7e43c04e..91a1623bb 100644
--- a/src/webview/screenshare.js
+++ b/src/webview/screenshare.ts
@@ -9,8 +9,8 @@ export async function getDisplayMediaSelector() {
9 return `<div class="desktop-capturer-selection__scroller"> 9 return `<div class="desktop-capturer-selection__scroller">
10 <ul class="desktop-capturer-selection__list"> 10 <ul class="desktop-capturer-selection__list">
11 ${sources 11 ${sources
12 .map( 12 .map(
13 ({ id, name, thumbnail }) => ` 13 ({ id, name, thumbnail }) => `
14 <li class="desktop-capturer-selection__item"> 14 <li class="desktop-capturer-selection__item">
15 <button class="desktop-capturer-selection__btn" data-id="${id}" title="${name}"> 15 <button class="desktop-capturer-selection__btn" data-id="${id}" title="${name}">
16 <img class="desktop-capturer-selection__thumbnail" src="${thumbnail.toDataURL()}" /> 16 <img class="desktop-capturer-selection__thumbnail" src="${thumbnail.toDataURL()}" />
@@ -18,8 +18,8 @@ export async function getDisplayMediaSelector() {
18 </button> 18 </button>
19 </li> 19 </li>
20 `, 20 `,
21 ) 21 )
22 .join('')} 22 .join('')}
23 <li class="desktop-capturer-selection__item"> 23 <li class="desktop-capturer-selection__item">
24 <button class="desktop-capturer-selection__btn" data-id="${CANCEL_ID}" title="Cancel"> 24 <button class="desktop-capturer-selection__btn" data-id="${CANCEL_ID}" title="Cancel">
25 <span class="desktop-capturer-selection__name desktop-capturer-selection__name--cancel">Cancel</span> 25 <span class="desktop-capturer-selection__name desktop-capturer-selection__name--cancel">Cancel</span>
diff --git a/src/webview/spellchecker.ts b/src/webview/spellchecker.ts
index 468a1b4ae..ee70589d5 100644
--- a/src/webview/spellchecker.ts
+++ b/src/webview/spellchecker.ts
@@ -5,7 +5,11 @@ import { isMac } from '../environment';
5const debug = require('debug')('Ferdi:spellchecker'); 5const debug = require('debug')('Ferdi:spellchecker');
6 6
7export function getSpellcheckerLocaleByFuzzyIdentifier(identifier: string) { 7export function getSpellcheckerLocaleByFuzzyIdentifier(identifier: string) {
8 const locales = Object.keys(SPELLCHECKER_LOCALES).filter((key) => key.toLocaleLowerCase() === identifier.toLowerCase() || key.split('-')[0] === identifier.toLowerCase()); 8 const locales = Object.keys(SPELLCHECKER_LOCALES).filter(
9 key =>
10 key.toLocaleLowerCase() === identifier.toLowerCase() ||
11 key.split('-')[0] === identifier.toLowerCase(),
12 );
9 13
10 return locales.length > 0 ? locales[0] : null; 14 return locales.length > 0 ? locales[0] : null;
11} 15}