diff options
author | Kristóf Marussy <kristof@marussy.com> | 2022-03-29 18:19:39 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2022-05-16 00:54:56 +0200 |
commit | a35d560f8d1ca414e3ba387b50731ca099e2da47 (patch) | |
tree | 5a8a90345c04eb0b69ea3fca8185641e26905b5d | |
parent | feat: New window banner (diff) | |
download | sophie-a35d560f8d1ca414e3ba387b50731ca099e2da47.tar.gz sophie-a35d560f8d1ca414e3ba387b50731ca099e2da47.tar.zst sophie-a35d560f8d1ca414e3ba387b50731ca099e2da47.zip |
feat: Add custom menubar
The menu is populated reactive from the store with no caching.
This doesn't seem to cause any performance problems so far.
Currently the menu is electron-specific.
In the future, we'll need a more runtime-independent way to build the
menu.
Signed-off-by: Kristóf Marussy <kristof@marussy.com>
-rw-r--r-- | packages/main/src/index.ts | 8 | ||||
-rw-r--r-- | packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts | 4 | ||||
-rw-r--r-- | packages/main/src/infrastructure/electron/impl/electronShell.ts | 5 | ||||
-rw-r--r-- | packages/main/src/infrastructure/electron/impl/setApplicationMenu.ts | 165 | ||||
-rw-r--r-- | packages/main/src/infrastructure/electron/types.ts | 2 | ||||
-rw-r--r-- | packages/main/src/initReactions.ts | 3 | ||||
-rw-r--r-- | packages/main/src/stores/GlobalSettings.ts | 43 | ||||
-rw-r--r-- | packages/main/src/stores/MainEnv.ts | 2 | ||||
-rw-r--r-- | packages/main/src/stores/MainStore.ts | 9 | ||||
-rw-r--r-- | packages/main/src/stores/Service.ts | 3 | ||||
-rw-r--r-- | packages/main/src/stores/SharedStore.ts | 53 |
11 files changed, 276 insertions, 21 deletions
diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts index 869c555..29a8cca 100644 --- a/packages/main/src/index.ts +++ b/packages/main/src/index.ts | |||
@@ -19,7 +19,7 @@ | |||
19 | * SPDX-License-Identifier: AGPL-3.0-only | 19 | * SPDX-License-Identifier: AGPL-3.0-only |
20 | */ | 20 | */ |
21 | 21 | ||
22 | import { arch } from 'node:os'; | 22 | import os from 'node:os'; |
23 | 23 | ||
24 | import { app } from 'electron'; | 24 | import { app } from 'electron'; |
25 | import { ensureDirSync } from 'fs-extra'; | 25 | import { ensureDirSync } from 'fs-extra'; |
@@ -68,7 +68,7 @@ app.setAboutPanelOptions({ | |||
68 | `Chrome: ${process.versions.chrome}`, | 68 | `Chrome: ${process.versions.chrome}`, |
69 | `Node.js: ${process.versions.node}`, | 69 | `Node.js: ${process.versions.node}`, |
70 | `Platform: ${osName()}`, | 70 | `Platform: ${osName()}`, |
71 | `Arch: ${arch()}`, | 71 | `Arch: ${os.arch()}`, |
72 | `Build date: ${new Date( | 72 | `Build date: ${new Date( |
73 | Number(import.meta.env.BUILD_DATE), | 73 | Number(import.meta.env.BUILD_DATE), |
74 | ).toLocaleString()}`, | 74 | ).toLocaleString()}`, |
@@ -90,7 +90,9 @@ app.on('window-all-closed', () => { | |||
90 | } | 90 | } |
91 | }); | 91 | }); |
92 | 92 | ||
93 | initReactions(store, isDevelopment) | 93 | const isMac = os.platform() === 'darwin'; |
94 | |||
95 | initReactions(store, isDevelopment, isMac) | ||
94 | // eslint-disable-next-line promise/always-return -- `then` instead of top-level await. | 96 | // eslint-disable-next-line promise/always-return -- `then` instead of top-level await. |
95 | .then((disposeCompositionRoot) => { | 97 | .then((disposeCompositionRoot) => { |
96 | app.on('will-quit', disposeCompositionRoot); | 98 | app.on('will-quit', disposeCompositionRoot); |
diff --git a/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts b/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts index 089e63a..5062da0 100644 --- a/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts +++ b/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts | |||
@@ -178,6 +178,10 @@ export default class ElectronServiceView implements ServiceView { | |||
178 | this.browserView.webContents.stop(); | 178 | this.browserView.webContents.stop(); |
179 | } | 179 | } |
180 | 180 | ||
181 | toggleDeveloperTools(): void { | ||
182 | this.browserView.webContents.toggleDevTools(); | ||
183 | } | ||
184 | |||
181 | setBounds(bounds: BrowserViewBounds): void { | 185 | setBounds(bounds: BrowserViewBounds): void { |
182 | this.browserView.setBounds(bounds); | 186 | this.browserView.setBounds(bounds); |
183 | } | 187 | } |
diff --git a/packages/main/src/infrastructure/electron/impl/electronShell.ts b/packages/main/src/infrastructure/electron/impl/electronShell.ts index 0e8c0c1..be1cbe3 100644 --- a/packages/main/src/infrastructure/electron/impl/electronShell.ts +++ b/packages/main/src/infrastructure/electron/impl/electronShell.ts | |||
@@ -18,7 +18,7 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { shell } from 'electron'; | 21 | import { app, shell } from 'electron'; |
22 | import { getLogger } from 'loglevel'; | 22 | import { getLogger } from 'loglevel'; |
23 | 23 | ||
24 | import type MainEnv from '../../../stores/MainEnv'; | 24 | import type MainEnv from '../../../stores/MainEnv'; |
@@ -31,6 +31,9 @@ const electronShell: MainEnv = { | |||
31 | log.error('Failed to open', url, 'in external program', error); | 31 | log.error('Failed to open', url, 'in external program', error); |
32 | }); | 32 | }); |
33 | }, | 33 | }, |
34 | openAboutDialog(): void { | ||
35 | app.showAboutPanel(); | ||
36 | }, | ||
34 | }; | 37 | }; |
35 | 38 | ||
36 | export default electronShell; | 39 | export default electronShell; |
diff --git a/packages/main/src/infrastructure/electron/impl/setApplicationMenu.ts b/packages/main/src/infrastructure/electron/impl/setApplicationMenu.ts new file mode 100644 index 0000000..5166719 --- /dev/null +++ b/packages/main/src/infrastructure/electron/impl/setApplicationMenu.ts | |||
@@ -0,0 +1,165 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import { Menu, MenuItemConstructorOptions } from 'electron'; | ||
22 | import { autorun } from 'mobx'; | ||
23 | import { addDisposer } from 'mobx-state-tree'; | ||
24 | |||
25 | import type MainStore from '../../../stores/MainStore'; | ||
26 | |||
27 | export default function setApplicationMenu( | ||
28 | store: MainStore, | ||
29 | devMode: boolean, | ||
30 | isMac: boolean, | ||
31 | ): void { | ||
32 | const dispose = autorun(() => { | ||
33 | const { settings, shared, visibleService } = store; | ||
34 | const { showLocationBar, selectedService } = settings; | ||
35 | const { canSwitchServices, services } = shared; | ||
36 | |||
37 | const template: MenuItemConstructorOptions[] = [ | ||
38 | ...(isMac ? ([{ role: 'appMenu' }] as MenuItemConstructorOptions[]) : []), | ||
39 | { role: 'fileMenu' }, | ||
40 | { role: 'editMenu' }, | ||
41 | { | ||
42 | role: 'viewMenu', | ||
43 | submenu: [ | ||
44 | { | ||
45 | label: 'Show Location Bar', | ||
46 | accelerator: 'CommandOrControl+Shift+L', | ||
47 | type: 'checkbox', | ||
48 | checked: showLocationBar, | ||
49 | click() { | ||
50 | settings.toggleLocationBar(); | ||
51 | }, | ||
52 | }, | ||
53 | { type: 'separator' }, | ||
54 | { | ||
55 | label: 'Reload', | ||
56 | accelerator: 'CommandOrControl+R', | ||
57 | enabled: selectedService !== undefined, | ||
58 | click() { | ||
59 | selectedService?.reload(false); | ||
60 | }, | ||
61 | }, | ||
62 | { | ||
63 | label: 'Force Reload', | ||
64 | accelerator: 'CommandOrControl+Shift+R', | ||
65 | enabled: selectedService !== undefined, | ||
66 | click() { | ||
67 | selectedService?.reload(true); | ||
68 | }, | ||
69 | }, | ||
70 | { | ||
71 | label: 'Toggle Developer Tools', | ||
72 | accelerator: 'CommandOrControl+Shift+I', | ||
73 | enabled: visibleService !== undefined, | ||
74 | click() { | ||
75 | visibleService?.toggleDeveloperTools(); | ||
76 | }, | ||
77 | }, | ||
78 | { type: 'separator' }, | ||
79 | ...(devMode | ||
80 | ? ([ | ||
81 | { | ||
82 | role: 'forceReload', | ||
83 | label: 'Reload Sophie', | ||
84 | accelerator: 'CommandOrControl+Shift+Alt+R', | ||
85 | }, | ||
86 | { | ||
87 | role: 'toggleDevTools', | ||
88 | label: 'Toggle Sophie Developer Tools', | ||
89 | accelerator: 'CommandOrControl+Shift+Alt+I', | ||
90 | }, | ||
91 | { type: 'separator' }, | ||
92 | ] as MenuItemConstructorOptions[]) | ||
93 | : []), | ||
94 | { role: 'togglefullscreen' }, | ||
95 | ], | ||
96 | }, | ||
97 | { | ||
98 | label: 'Services', | ||
99 | submenu: [ | ||
100 | { | ||
101 | label: 'Next Service', | ||
102 | accelerator: 'CommandOrControl+Tab', | ||
103 | enabled: canSwitchServices, | ||
104 | click() { | ||
105 | shared.activateNextService(); | ||
106 | }, | ||
107 | }, | ||
108 | { | ||
109 | label: 'Previous Service', | ||
110 | accelerator: 'CommandOrControl+Shift+Tab', | ||
111 | enabled: canSwitchServices, | ||
112 | click() { | ||
113 | shared.activatePreviousService(); | ||
114 | }, | ||
115 | }, | ||
116 | ...(services.length > 0 | ||
117 | ? ([ | ||
118 | { type: 'separator' }, | ||
119 | ...services.map( | ||
120 | (service, index): MenuItemConstructorOptions => ({ | ||
121 | label: service.config.name, | ||
122 | type: 'radio', | ||
123 | ...(index < 9 | ||
124 | ? { | ||
125 | accelerator: `CommandOrControl+${index + 1}`, | ||
126 | } | ||
127 | : {}), | ||
128 | checked: selectedService === service, | ||
129 | click() { | ||
130 | settings.setSelectedService(service); | ||
131 | }, | ||
132 | }), | ||
133 | ), | ||
134 | ] as MenuItemConstructorOptions[]) | ||
135 | : []), | ||
136 | ], | ||
137 | }, | ||
138 | { role: 'windowMenu' }, | ||
139 | { | ||
140 | role: 'help', | ||
141 | submenu: [ | ||
142 | { | ||
143 | label: 'Gitlab', | ||
144 | click() { | ||
145 | store.openWebpageInBrowser(); | ||
146 | }, | ||
147 | }, | ||
148 | ...(isMac | ||
149 | ? [] | ||
150 | : ([ | ||
151 | { | ||
152 | role: 'about', | ||
153 | click() { | ||
154 | store.openAboutDialog(); | ||
155 | }, | ||
156 | }, | ||
157 | ] as MenuItemConstructorOptions[])), | ||
158 | ], | ||
159 | }, | ||
160 | ]; | ||
161 | const menu = Menu.buildFromTemplate(template); | ||
162 | Menu.setApplicationMenu(menu); | ||
163 | }); | ||
164 | addDisposer(store, dispose); | ||
165 | } | ||
diff --git a/packages/main/src/infrastructure/electron/types.ts b/packages/main/src/infrastructure/electron/types.ts index 7b04a6b..4716f0b 100644 --- a/packages/main/src/infrastructure/electron/types.ts +++ b/packages/main/src/infrastructure/electron/types.ts | |||
@@ -65,6 +65,8 @@ export interface ServiceView { | |||
65 | 65 | ||
66 | stop(): void; | 66 | stop(): void; |
67 | 67 | ||
68 | toggleDeveloperTools(): void; | ||
69 | |||
68 | setBounds(bounds: BrowserViewBounds): void; | 70 | setBounds(bounds: BrowserViewBounds): void; |
69 | 71 | ||
70 | dispose(): void; | 72 | dispose(): void; |
diff --git a/packages/main/src/initReactions.ts b/packages/main/src/initReactions.ts index b6a8502..9c49fc5 100644 --- a/packages/main/src/initReactions.ts +++ b/packages/main/src/initReactions.ts | |||
@@ -25,6 +25,7 @@ import UserAgents from './infrastructure/electron/UserAgents'; | |||
25 | import ElectronViewFactory from './infrastructure/electron/impl/ElectronViewFactory'; | 25 | import ElectronViewFactory from './infrastructure/electron/impl/ElectronViewFactory'; |
26 | import { installDevToolsExtensions } from './infrastructure/electron/impl/devTools'; | 26 | import { installDevToolsExtensions } from './infrastructure/electron/impl/devTools'; |
27 | import hardenSession from './infrastructure/electron/impl/hardenSession'; | 27 | import hardenSession from './infrastructure/electron/impl/hardenSession'; |
28 | import setApplicationMenu from './infrastructure/electron/impl/setApplicationMenu'; | ||
28 | import getDistResources from './infrastructure/resources/impl/getDistResources'; | 29 | import getDistResources from './infrastructure/resources/impl/getDistResources'; |
29 | import loadServices from './reactions/loadServices'; | 30 | import loadServices from './reactions/loadServices'; |
30 | import synchronizeConfig from './reactions/synchronizeConfig'; | 31 | import synchronizeConfig from './reactions/synchronizeConfig'; |
@@ -35,6 +36,7 @@ import type Disposer from './utils/Disposer'; | |||
35 | export default async function initReactions( | 36 | export default async function initReactions( |
36 | store: MainStore, | 37 | store: MainStore, |
37 | devMode: boolean, | 38 | devMode: boolean, |
39 | isMac: boolean, | ||
38 | ): Promise<Disposer> { | 40 | ): Promise<Disposer> { |
39 | const configRepository = new ConfigFile(app.getPath('userData')); | 41 | const configRepository = new ConfigFile(app.getPath('userData')); |
40 | const disposeConfigController = await synchronizeConfig( | 42 | const disposeConfigController = await synchronizeConfig( |
@@ -50,6 +52,7 @@ export default async function initReactions( | |||
50 | } | 52 | } |
51 | const userAgents = new UserAgents(app.userAgentFallback); | 53 | const userAgents = new UserAgents(app.userAgentFallback); |
52 | app.userAgentFallback = userAgents.fallbackUserAgent(devMode); | 54 | app.userAgentFallback = userAgents.fallbackUserAgent(devMode); |
55 | setApplicationMenu(store, devMode, isMac); | ||
53 | const viewFactory = new ElectronViewFactory(userAgents, resources, devMode); | 56 | const viewFactory = new ElectronViewFactory(userAgents, resources, devMode); |
54 | const [mainWindow] = await Promise.all([ | 57 | const [mainWindow] = await Promise.all([ |
55 | viewFactory.createMainWindow(store), | 58 | viewFactory.createMainWindow(store), |
diff --git a/packages/main/src/stores/GlobalSettings.ts b/packages/main/src/stores/GlobalSettings.ts index 31b7e12..4e6aa13 100644 --- a/packages/main/src/stores/GlobalSettings.ts +++ b/packages/main/src/stores/GlobalSettings.ts | |||
@@ -27,23 +27,32 @@ import Service from './Service'; | |||
27 | 27 | ||
28 | const log = getLogger('sharedStore'); | 28 | const log = getLogger('sharedStore'); |
29 | 29 | ||
30 | const GlobalSettings = defineGlobalSettingsModel(Service).actions((self) => ({ | 30 | const GlobalSettings = defineGlobalSettingsModel(Service) |
31 | setThemeSource(mode: ThemeSource): void { | 31 | .actions((self) => ({ |
32 | self.themeSource = mode; | 32 | setThemeSource(mode: ThemeSource): void { |
33 | }, | 33 | self.themeSource = mode; |
34 | setShowLocationBar(showLocationBar: boolean): void { | 34 | }, |
35 | self.showLocationBar = showLocationBar; | 35 | setShowLocationBar(showLocationBar: boolean): void { |
36 | }, | 36 | self.showLocationBar = showLocationBar; |
37 | setSelectedServiceId(serviceId: string): void { | 37 | }, |
38 | const serviceInstance = resolveIdentifier(Service, self, serviceId); | 38 | setSelectedService(service: Service): void { |
39 | if (serviceInstance === undefined) { | 39 | self.selectedService = service; |
40 | log.warn('Trying to select unknown service', serviceId); | 40 | }, |
41 | return; | 41 | })) |
42 | } | 42 | .actions((self) => ({ |
43 | self.selectedService = serviceInstance; | 43 | toggleLocationBar(): void { |
44 | log.debug('Selected service', serviceId); | 44 | self.setShowLocationBar(!self.showLocationBar); |
45 | }, | 45 | }, |
46 | })); | 46 | setSelectedServiceId(serviceId: string): void { |
47 | const serviceInstance = resolveIdentifier(Service, self, serviceId); | ||
48 | if (serviceInstance === undefined) { | ||
49 | log.warn('Trying to select unknown service', serviceId); | ||
50 | return; | ||
51 | } | ||
52 | self.setSelectedService(serviceInstance); | ||
53 | log.debug('Selected service', serviceId); | ||
54 | }, | ||
55 | })); | ||
47 | 56 | ||
48 | /* | 57 | /* |
49 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | 58 | eslint-disable-next-line @typescript-eslint/no-redeclare -- |
diff --git a/packages/main/src/stores/MainEnv.ts b/packages/main/src/stores/MainEnv.ts index 94c34f3..8923322 100644 --- a/packages/main/src/stores/MainEnv.ts +++ b/packages/main/src/stores/MainEnv.ts | |||
@@ -26,4 +26,6 @@ export function getEnv(model: IAnyStateTreeNode): MainEnv { | |||
26 | 26 | ||
27 | export default interface MainEnv { | 27 | export default interface MainEnv { |
28 | openURLInExternalBrowser(url: string): void; | 28 | openURLInExternalBrowser(url: string): void; |
29 | |||
30 | openAboutDialog(): void; | ||
29 | } | 31 | } |
diff --git a/packages/main/src/stores/MainStore.ts b/packages/main/src/stores/MainStore.ts index ed0f391..86fadcb 100644 --- a/packages/main/src/stores/MainStore.ts +++ b/packages/main/src/stores/MainStore.ts | |||
@@ -25,6 +25,7 @@ import type { MainWindow } from '../infrastructure/electron/types'; | |||
25 | import { getLogger } from '../utils/log'; | 25 | import { getLogger } from '../utils/log'; |
26 | 26 | ||
27 | import GlobalSettings from './GlobalSettings'; | 27 | import GlobalSettings from './GlobalSettings'; |
28 | import { getEnv } from './MainEnv'; | ||
28 | import Profile from './Profile'; | 29 | import Profile from './Profile'; |
29 | import Service from './Service'; | 30 | import Service from './Service'; |
30 | import SharedStore from './SharedStore'; | 31 | import SharedStore from './SharedStore'; |
@@ -112,6 +113,14 @@ const MainStore = types | |||
112 | setMainWindow(mainWindow: MainWindow | undefined): void { | 113 | setMainWindow(mainWindow: MainWindow | undefined): void { |
113 | self.mainWindow = mainWindow; | 114 | self.mainWindow = mainWindow; |
114 | }, | 115 | }, |
116 | openWebpageInBrowser() { | ||
117 | getEnv(self).openURLInExternalBrowser( | ||
118 | 'https://gitlab.com/say-hi-to-sophie/shophie', | ||
119 | ); | ||
120 | }, | ||
121 | openAboutDialog() { | ||
122 | getEnv(self).openAboutDialog(); | ||
123 | }, | ||
115 | beforeDestroy(): void { | 124 | beforeDestroy(): void { |
116 | self.mainWindow?.dispose(); | 125 | self.mainWindow?.dispose(); |
117 | }, | 126 | }, |
diff --git a/packages/main/src/stores/Service.ts b/packages/main/src/stores/Service.ts index 0a35114..d8f3166 100644 --- a/packages/main/src/stores/Service.ts +++ b/packages/main/src/stores/Service.ts | |||
@@ -126,6 +126,9 @@ const Service = defineServiceModel(ServiceSettings) | |||
126 | dismissAllPopups(): void { | 126 | dismissAllPopups(): void { |
127 | self.popups.splice(0); | 127 | self.popups.splice(0); |
128 | }, | 128 | }, |
129 | toggleDeveloperTools(): void { | ||
130 | self.serviceView?.toggleDeveloperTools(); | ||
131 | }, | ||
129 | })) | 132 | })) |
130 | .actions((self) => { | 133 | .actions((self) => { |
131 | function setState(state: ServiceStateSnapshotIn): void { | 134 | function setState(state: ServiceStateSnapshotIn): void { |
diff --git a/packages/main/src/stores/SharedStore.ts b/packages/main/src/stores/SharedStore.ts index d72c532..67d58d6 100644 --- a/packages/main/src/stores/SharedStore.ts +++ b/packages/main/src/stores/SharedStore.ts | |||
@@ -21,12 +21,16 @@ | |||
21 | import { defineSharedStoreModel } from '@sophie/shared'; | 21 | import { defineSharedStoreModel } from '@sophie/shared'; |
22 | import { getSnapshot, Instance } from 'mobx-state-tree'; | 22 | import { getSnapshot, Instance } from 'mobx-state-tree'; |
23 | 23 | ||
24 | import { getLogger } from '../utils/log'; | ||
25 | |||
24 | import GlobalSettings from './GlobalSettings'; | 26 | import GlobalSettings from './GlobalSettings'; |
25 | import Profile from './Profile'; | 27 | import Profile from './Profile'; |
26 | import Service from './Service'; | 28 | import Service from './Service'; |
27 | import type Config from './config/Config'; | 29 | import type Config from './config/Config'; |
28 | import loadConfig from './config/loadConfig'; | 30 | import loadConfig from './config/loadConfig'; |
29 | 31 | ||
32 | const log = getLogger('SharedStore'); | ||
33 | |||
30 | function getConfigs<T>(models: { config: T }[]): T[] | undefined { | 34 | function getConfigs<T>(models: { config: T }[]): T[] | undefined { |
31 | return models.length === 0 ? undefined : models.map((model) => model.config); | 35 | return models.length === 0 ? undefined : models.map((model) => model.config); |
32 | } | 36 | } |
@@ -42,6 +46,19 @@ const SharedStore = defineSharedStoreModel(GlobalSettings, Profile, Service) | |||
42 | services: getConfigs(services), | 46 | services: getConfigs(services), |
43 | }; | 47 | }; |
44 | }, | 48 | }, |
49 | get canSwitchServices(): boolean { | ||
50 | return self.services.length >= 2; | ||
51 | }, | ||
52 | get selectedServiceIndex(): number { | ||
53 | const { | ||
54 | services, | ||
55 | settings: { selectedService }, | ||
56 | } = self; | ||
57 | if (selectedService === undefined) { | ||
58 | return -1; | ||
59 | } | ||
60 | return services.indexOf(selectedService); | ||
61 | }, | ||
45 | })) | 62 | })) |
46 | .actions((self) => ({ | 63 | .actions((self) => ({ |
47 | loadConfig(config: Config): void { | 64 | loadConfig(config: Config): void { |
@@ -50,6 +67,42 @@ const SharedStore = defineSharedStoreModel(GlobalSettings, Profile, Service) | |||
50 | setShouldUseDarkColors(shouldUseDarkColors: boolean): void { | 67 | setShouldUseDarkColors(shouldUseDarkColors: boolean): void { |
51 | self.shouldUseDarkColors = shouldUseDarkColors; | 68 | self.shouldUseDarkColors = shouldUseDarkColors; |
52 | }, | 69 | }, |
70 | activateServiceByOffset(offset: number): void { | ||
71 | if (offset === 0) { | ||
72 | return; | ||
73 | } | ||
74 | const { selectedServiceIndex: index, services, settings } = self; | ||
75 | if (index < 0) { | ||
76 | log.warn('No selected service to offset'); | ||
77 | return; | ||
78 | } | ||
79 | const { length } = services; | ||
80 | const indexWithOffset = (index + offset) % length; | ||
81 | // Make sure that `newIndex` is positive even for large negative `offset`. | ||
82 | const newIndex = | ||
83 | indexWithOffset < 0 ? indexWithOffset + length : indexWithOffset; | ||
84 | const newService = services.at(newIndex); | ||
85 | if (newService === undefined) { | ||
86 | log.error( | ||
87 | 'Could not advance selected service index from', | ||
88 | index, | ||
89 | 'by', | ||
90 | offset, | ||
91 | 'to', | ||
92 | newIndex, | ||
93 | ); | ||
94 | return; | ||
95 | } | ||
96 | settings.setSelectedService(newService); | ||
97 | }, | ||
98 | })) | ||
99 | .actions((self) => ({ | ||
100 | activateNextService(): void { | ||
101 | self.activateServiceByOffset(1); | ||
102 | }, | ||
103 | activatePreviousService(): void { | ||
104 | self.activateServiceByOffset(-1); | ||
105 | }, | ||
53 | })); | 106 | })); |
54 | 107 | ||
55 | /* | 108 | /* |