diff options
author | Kristóf Marussy <kristof@marussy.com> | 2022-01-27 01:02:02 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2022-02-08 21:43:17 +0100 |
commit | da610a51f49807d1409f36b98e06e89447a4202b (patch) | |
tree | a4f7911f45ca52626ef02f370c3f260d33f8272e | |
parent | refactor: Coding conventions (diff) | |
download | sophie-da610a51f49807d1409f36b98e06e89447a4202b.tar.gz sophie-da610a51f49807d1409f36b98e06e89447a4202b.tar.zst sophie-da610a51f49807d1409f36b98e06e89447a4202b.zip |
refactor: Extract config handling
Move the handling of the contents of the config file out of the stores
and into dedicated files to simplify the code of the stores.
Signed-off-by: Kristóf Marussy <kristof@marussy.com>
-rw-r--r-- | packages/main/src/infrastructure/config/ConfigRepository.ts | 2 | ||||
-rw-r--r-- | packages/main/src/infrastructure/config/impl/ConfigFile.ts (renamed from packages/main/src/infrastructure/config/ConfigFile.ts) | 11 | ||||
-rw-r--r-- | packages/main/src/initReactions.ts | 2 | ||||
-rw-r--r-- | packages/main/src/reactions/synchronizeConfig.ts | 2 | ||||
-rw-r--r-- | packages/main/src/stores/Profile.ts | 30 | ||||
-rw-r--r-- | packages/main/src/stores/ProfileSettings.ts | 30 | ||||
-rw-r--r-- | packages/main/src/stores/Service.ts | 35 | ||||
-rw-r--r-- | packages/main/src/stores/SharedStore.ts | 91 | ||||
-rw-r--r-- | packages/main/src/stores/__tests__/SharedStore.spec.ts | 7 | ||||
-rw-r--r-- | packages/main/src/stores/config/Config.ts | 30 | ||||
-rw-r--r-- | packages/main/src/stores/config/ProfileConfig.ts (renamed from packages/main/src/utils/generateId.ts) | 10 | ||||
-rw-r--r-- | packages/main/src/stores/config/ServiceConfig.ts | 29 | ||||
-rw-r--r-- | packages/main/src/stores/config/loadConfig.ts | 146 |
13 files changed, 268 insertions, 157 deletions
diff --git a/packages/main/src/infrastructure/config/ConfigRepository.ts b/packages/main/src/infrastructure/config/ConfigRepository.ts index 0ce7fc1..e00f5a0 100644 --- a/packages/main/src/infrastructure/config/ConfigRepository.ts +++ b/packages/main/src/infrastructure/config/ConfigRepository.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 type { Config } from '../../stores/SharedStore'; | 21 | import type Config from '../../stores/config/Config'; |
22 | import type Disposer from '../../utils/Disposer'; | 22 | import type Disposer from '../../utils/Disposer'; |
23 | 23 | ||
24 | export type ReadConfigResult = | 24 | export type ReadConfigResult = |
diff --git a/packages/main/src/infrastructure/config/ConfigFile.ts b/packages/main/src/infrastructure/config/impl/ConfigFile.ts index 193a20d..90ee187 100644 --- a/packages/main/src/infrastructure/config/ConfigFile.ts +++ b/packages/main/src/infrastructure/config/impl/ConfigFile.ts | |||
@@ -25,12 +25,11 @@ import path from 'node:path'; | |||
25 | import JSON5 from 'json5'; | 25 | import JSON5 from 'json5'; |
26 | import { throttle } from 'lodash-es'; | 26 | import { throttle } from 'lodash-es'; |
27 | 27 | ||
28 | import type { Config } from '../../stores/SharedStore'; | 28 | import type Config from '../../../stores/config/Config'; |
29 | import type Disposer from '../../utils/Disposer'; | 29 | import type Disposer from '../../../utils/Disposer'; |
30 | import { getLogger } from '../../utils/log'; | 30 | import { getLogger } from '../../../utils/log'; |
31 | 31 | import type ConfigRepository from '../ConfigRepository'; | |
32 | import type ConfigRepository from './ConfigRepository'; | 32 | import type ReadConfigResult from '../ReadConfigResult'; |
33 | import type ReadConfigResult from './ReadConfigResult'; | ||
34 | 33 | ||
35 | const log = getLogger('ConfigFile'); | 34 | const log = getLogger('ConfigFile'); |
36 | 35 | ||
diff --git a/packages/main/src/initReactions.ts b/packages/main/src/initReactions.ts index 50e561d..87ad425 100644 --- a/packages/main/src/initReactions.ts +++ b/packages/main/src/initReactions.ts | |||
@@ -20,7 +20,7 @@ | |||
20 | 20 | ||
21 | import { app } from 'electron'; | 21 | import { app } from 'electron'; |
22 | 22 | ||
23 | import ConfigFile from './infrastructure/config/ConfigFile'; | 23 | import ConfigFile from './infrastructure/config/impl/ConfigFile'; |
24 | import synchronizeConfig from './reactions/synchronizeConfig'; | 24 | import synchronizeConfig from './reactions/synchronizeConfig'; |
25 | import synchronizeNativeTheme from './reactions/synchronizeNativeTheme'; | 25 | import synchronizeNativeTheme from './reactions/synchronizeNativeTheme'; |
26 | import type MainStore from './stores/MainStore'; | 26 | import type MainStore from './stores/MainStore'; |
diff --git a/packages/main/src/reactions/synchronizeConfig.ts b/packages/main/src/reactions/synchronizeConfig.ts index 480cc1a..4a9c24b 100644 --- a/packages/main/src/reactions/synchronizeConfig.ts +++ b/packages/main/src/reactions/synchronizeConfig.ts | |||
@@ -25,7 +25,7 @@ import ms from 'ms'; | |||
25 | 25 | ||
26 | import type ConfigRepository from '../infrastructure/config/ConfigRepository'; | 26 | import type ConfigRepository from '../infrastructure/config/ConfigRepository'; |
27 | import type SharedStore from '../stores/SharedStore'; | 27 | import type SharedStore from '../stores/SharedStore'; |
28 | import type { Config } from '../stores/SharedStore'; | 28 | import type Config from '../stores/config/Config'; |
29 | import type Disposer from '../utils/Disposer'; | 29 | import type Disposer from '../utils/Disposer'; |
30 | import { getLogger } from '../utils/log'; | 30 | import { getLogger } from '../utils/log'; |
31 | 31 | ||
diff --git a/packages/main/src/stores/Profile.ts b/packages/main/src/stores/Profile.ts index ec2a64b..0fd486e 100644 --- a/packages/main/src/stores/Profile.ts +++ b/packages/main/src/stores/Profile.ts | |||
@@ -18,19 +18,17 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { | 21 | import { Profile as ProfileBase } from '@sophie/shared'; |
22 | Profile as ProfileBase, | ||
23 | ProfileSettingsSnapshotIn, | ||
24 | } from '@sophie/shared'; | ||
25 | import { getSnapshot, Instance } from 'mobx-state-tree'; | 22 | import { getSnapshot, Instance } from 'mobx-state-tree'; |
26 | 23 | ||
27 | import generateId from '../utils/generateId'; | 24 | import overrideProps from '../utils/overrideProps'; |
28 | 25 | ||
29 | export interface ProfileConfig extends ProfileSettingsSnapshotIn { | 26 | import ProfileSettings from './ProfileSettings'; |
30 | id?: string | undefined; | 27 | import type ProfileConfig from './config/ProfileConfig'; |
31 | } | ||
32 | 28 | ||
33 | const Profile = ProfileBase.views((self) => ({ | 29 | const Profile = overrideProps(ProfileBase, { |
30 | settings: ProfileSettings, | ||
31 | }).views((self) => ({ | ||
34 | get config(): ProfileConfig { | 32 | get config(): ProfileConfig { |
35 | const { id, settings } = self; | 33 | const { id, settings } = self; |
36 | return { ...getSnapshot(settings), id }; | 34 | return { ...getSnapshot(settings), id }; |
@@ -44,17 +42,3 @@ const Profile = ProfileBase.views((self) => ({ | |||
44 | interface Profile extends Instance<typeof Profile> {} | 42 | interface Profile extends Instance<typeof Profile> {} |
45 | 43 | ||
46 | export default Profile; | 44 | export default Profile; |
47 | |||
48 | export type ProfileSettingsSnapshotWithId = [string, ProfileSettingsSnapshotIn]; | ||
49 | |||
50 | export function addMissingProfileIds( | ||
51 | profileConfigs: ProfileConfig[] | undefined, | ||
52 | ): ProfileSettingsSnapshotWithId[] { | ||
53 | return (profileConfigs ?? []).map((profileConfig) => { | ||
54 | const { id, ...settings } = profileConfig; | ||
55 | return [ | ||
56 | typeof id === 'undefined' ? generateId(settings.name) : id, | ||
57 | settings, | ||
58 | ]; | ||
59 | }); | ||
60 | } | ||
diff --git a/packages/main/src/stores/ProfileSettings.ts b/packages/main/src/stores/ProfileSettings.ts new file mode 100644 index 0000000..eed51e3 --- /dev/null +++ b/packages/main/src/stores/ProfileSettings.ts | |||
@@ -0,0 +1,30 @@ | |||
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 { ProfileSettings } from '@sophie/shared'; | ||
22 | |||
23 | // TODO Export a modified ProfileSettings once we need to add actions to it. | ||
24 | // eslint-disable-next-line unicorn/prefer-export-from -- Can't export from default. | ||
25 | export default ProfileSettings; | ||
26 | |||
27 | export type { | ||
28 | ProfileSettingsSnapshotIn, | ||
29 | ProfileSettingsSnapshotOut, | ||
30 | } from '@sophie/shared'; | ||
diff --git a/packages/main/src/stores/Service.ts b/packages/main/src/stores/Service.ts index e70caa6..fea0bdf 100644 --- a/packages/main/src/stores/Service.ts +++ b/packages/main/src/stores/Service.ts | |||
@@ -20,20 +20,12 @@ | |||
20 | 20 | ||
21 | import type { UnreadCount } from '@sophie/service-shared'; | 21 | import type { UnreadCount } from '@sophie/service-shared'; |
22 | import { Service as ServiceBase } from '@sophie/shared'; | 22 | import { Service as ServiceBase } from '@sophie/shared'; |
23 | import { Instance, getSnapshot, ReferenceIdentifier } from 'mobx-state-tree'; | 23 | import { Instance, getSnapshot } from 'mobx-state-tree'; |
24 | 24 | ||
25 | import generateId from '../utils/generateId'; | ||
26 | import overrideProps from '../utils/overrideProps'; | 25 | import overrideProps from '../utils/overrideProps'; |
27 | 26 | ||
28 | import { ProfileSettingsSnapshotWithId } from './Profile'; | 27 | import ServiceSettings from './ServiceSettings'; |
29 | import ServiceSettings, { ServiceSettingsSnapshotIn } from './ServiceSettings'; | 28 | import type ServiceConfig from './config/ServiceConfig'; |
30 | |||
31 | export interface ServiceConfig | ||
32 | extends Omit<ServiceSettingsSnapshotIn, 'profile'> { | ||
33 | id?: string | undefined; | ||
34 | |||
35 | profile?: ReferenceIdentifier | undefined; | ||
36 | } | ||
37 | 29 | ||
38 | const Service = overrideProps(ServiceBase, { | 30 | const Service = overrideProps(ServiceBase, { |
39 | settings: ServiceSettings, | 31 | settings: ServiceSettings, |
@@ -90,24 +82,3 @@ const Service = overrideProps(ServiceBase, { | |||
90 | interface Service extends Instance<typeof Service> {} | 82 | interface Service extends Instance<typeof Service> {} |
91 | 83 | ||
92 | export default Service; | 84 | export default Service; |
93 | |||
94 | export type ServiceSettingsSnapshotWithId = [string, ServiceSettingsSnapshotIn]; | ||
95 | |||
96 | export function addMissingServiceIdsAndProfiles( | ||
97 | serviceConfigs: ServiceConfig[] | undefined, | ||
98 | profiles: ProfileSettingsSnapshotWithId[], | ||
99 | ): ServiceSettingsSnapshotWithId[] { | ||
100 | return (serviceConfigs ?? []).map((serviceConfig) => { | ||
101 | const { id, ...settings } = serviceConfig; | ||
102 | const { name } = settings; | ||
103 | let { profile: profileId } = settings; | ||
104 | if (profileId === undefined) { | ||
105 | profileId = generateId(name); | ||
106 | profiles.push([profileId, { name }]); | ||
107 | } | ||
108 | return [ | ||
109 | id === undefined ? generateId(name) : id, | ||
110 | { ...settings, profile: profileId }, | ||
111 | ]; | ||
112 | }); | ||
113 | } | ||
diff --git a/packages/main/src/stores/SharedStore.ts b/packages/main/src/stores/SharedStore.ts index c34af75..b9983f6 100644 --- a/packages/main/src/stores/SharedStore.ts +++ b/packages/main/src/stores/SharedStore.ts | |||
@@ -19,71 +19,23 @@ | |||
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { SharedStore as SharedStoreBase } from '@sophie/shared'; | 21 | import { SharedStore as SharedStoreBase } from '@sophie/shared'; |
22 | import { | 22 | import { getSnapshot, Instance, types } from 'mobx-state-tree'; |
23 | applySnapshot, | ||
24 | getSnapshot, | ||
25 | IMSTArray, | ||
26 | IMSTMap, | ||
27 | Instance, | ||
28 | IReferenceType, | ||
29 | IStateTreeNode, | ||
30 | IType, | ||
31 | types, | ||
32 | } from 'mobx-state-tree'; | ||
33 | 23 | ||
34 | import { getLogger } from '../utils/log'; | 24 | import { getLogger } from '../utils/log'; |
35 | import overrideProps from '../utils/overrideProps'; | 25 | import overrideProps from '../utils/overrideProps'; |
36 | 26 | ||
37 | import GlobalSettings, { GlobalSettingsSnapshotIn } from './GlobalSettings'; | 27 | import GlobalSettings from './GlobalSettings'; |
38 | import Profile, { addMissingProfileIds, ProfileConfig } from './Profile'; | 28 | import Profile from './Profile'; |
39 | import Service, { | 29 | import Service from './Service'; |
40 | addMissingServiceIdsAndProfiles, | 30 | import type Config from './config/Config'; |
41 | ServiceConfig, | 31 | import loadConfig from './config/loadConfig'; |
42 | } from './Service'; | ||
43 | 32 | ||
44 | const log = getLogger('sharedStore'); | 33 | const log = getLogger('sharedStore'); |
45 | 34 | ||
46 | export interface Config extends GlobalSettingsSnapshotIn { | ||
47 | profiles?: ProfileConfig[] | undefined; | ||
48 | |||
49 | services?: ServiceConfig[] | undefined; | ||
50 | } | ||
51 | |||
52 | function getConfigs<T>(models: { config: T }[]): T[] | undefined { | 35 | function getConfigs<T>(models: { config: T }[]): T[] | undefined { |
53 | return models.length === 0 ? undefined : models.map((model) => model.config); | 36 | return models.length === 0 ? undefined : models.map((model) => model.config); |
54 | } | 37 | } |
55 | 38 | ||
56 | function applySettings< | ||
57 | C, | ||
58 | D extends IType< | ||
59 | { id: string; settings: C }, | ||
60 | unknown, | ||
61 | { settings: IStateTreeNode<IType<C, unknown, unknown>> } | ||
62 | >, | ||
63 | >( | ||
64 | current: IMSTArray<IReferenceType<D>>, | ||
65 | currentById: IMSTMap<D>, | ||
66 | toApply: [string, C][], | ||
67 | ): void { | ||
68 | const toApplyById = new Map(toApply); | ||
69 | const toDelete = new Set(currentById.keys()); | ||
70 | toApplyById.forEach((settingsSnapshot, id) => { | ||
71 | const model = currentById.get(id); | ||
72 | if (model === undefined) { | ||
73 | currentById.set(id, { | ||
74 | id, | ||
75 | settings: settingsSnapshot, | ||
76 | }); | ||
77 | } else { | ||
78 | toDelete.delete(id); | ||
79 | applySnapshot(model.settings, settingsSnapshot); | ||
80 | } | ||
81 | }); | ||
82 | current.clear(); | ||
83 | toDelete.forEach((id) => currentById.delete(id)); | ||
84 | current.push(...toApply.map(([id]) => id)); | ||
85 | } | ||
86 | |||
87 | const SharedStore = overrideProps(SharedStoreBase, { | 39 | const SharedStore = overrideProps(SharedStoreBase, { |
88 | settings: types.optional(GlobalSettings, {}), | 40 | settings: types.optional(GlobalSettings, {}), |
89 | profilesById: types.map(Profile), | 41 | profilesById: types.map(Profile), |
@@ -105,36 +57,7 @@ const SharedStore = overrideProps(SharedStoreBase, { | |||
105 | })) | 57 | })) |
106 | .actions((self) => ({ | 58 | .actions((self) => ({ |
107 | loadConfig(config: Config): void { | 59 | loadConfig(config: Config): void { |
108 | const { | 60 | loadConfig(self, config); |
109 | profiles, | ||
110 | profilesById, | ||
111 | selectedService, | ||
112 | services, | ||
113 | servicesById, | ||
114 | settings, | ||
115 | } = self; | ||
116 | const { id: selectedServiceId } = selectedService ?? { id: undefined }; | ||
117 | const { | ||
118 | profiles: profilesConfig, | ||
119 | services: servicesConfig, | ||
120 | ...settingsToApply | ||
121 | } = config; | ||
122 | const profilesToApply = addMissingProfileIds(profilesConfig); | ||
123 | const servicesToApply = addMissingServiceIdsAndProfiles( | ||
124 | servicesConfig, | ||
125 | profilesToApply, | ||
126 | ); | ||
127 | applySettings(profiles, profilesById, profilesToApply); | ||
128 | applySettings(services, servicesById, servicesToApply); | ||
129 | applySnapshot(settings, settingsToApply); | ||
130 | let newSelectedService; | ||
131 | if (selectedServiceId !== undefined) { | ||
132 | newSelectedService = servicesById.get(selectedServiceId); | ||
133 | } | ||
134 | if (newSelectedService === undefined && services.length > 0) { | ||
135 | [newSelectedService] = services; | ||
136 | } | ||
137 | self.selectedService = newSelectedService; | ||
138 | }, | 61 | }, |
139 | setShouldUseDarkColors(shouldUseDarkColors: boolean): void { | 62 | setShouldUseDarkColors(shouldUseDarkColors: boolean): void { |
140 | self.shouldUseDarkColors = shouldUseDarkColors; | 63 | self.shouldUseDarkColors = shouldUseDarkColors; |
diff --git a/packages/main/src/stores/__tests__/SharedStore.spec.ts b/packages/main/src/stores/__tests__/SharedStore.spec.ts index dfd59a1..268ce3f 100644 --- a/packages/main/src/stores/__tests__/SharedStore.spec.ts +++ b/packages/main/src/stores/__tests__/SharedStore.spec.ts | |||
@@ -18,9 +18,10 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import type { ProfileConfig } from '../Profile'; | 21 | import SharedStore from '../SharedStore'; |
22 | import type { ServiceConfig } from '../Service'; | 22 | import type Config from '../config/Config'; |
23 | import SharedStore, { Config } from '../SharedStore'; | 23 | import type ProfileConfig from '../config/ProfileConfig'; |
24 | import type ServiceConfig from '../config/ServiceConfig'; | ||
24 | 25 | ||
25 | const profileProps: ProfileConfig = { | 26 | const profileProps: ProfileConfig = { |
26 | name: 'Test profile', | 27 | name: 'Test profile', |
diff --git a/packages/main/src/stores/config/Config.ts b/packages/main/src/stores/config/Config.ts new file mode 100644 index 0000000..c38e3c5 --- /dev/null +++ b/packages/main/src/stores/config/Config.ts | |||
@@ -0,0 +1,30 @@ | |||
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 type { GlobalSettingsSnapshotIn } from '../GlobalSettings'; | ||
22 | |||
23 | import type ProfileConfig from './ProfileConfig'; | ||
24 | import type ServiceConfig from './ServiceConfig'; | ||
25 | |||
26 | export default interface Config extends GlobalSettingsSnapshotIn { | ||
27 | profiles?: ProfileConfig[] | undefined; | ||
28 | |||
29 | services?: ServiceConfig[] | undefined; | ||
30 | } | ||
diff --git a/packages/main/src/utils/generateId.ts b/packages/main/src/stores/config/ProfileConfig.ts index 8a87e5a..ce276d4 100644 --- a/packages/main/src/utils/generateId.ts +++ b/packages/main/src/stores/config/ProfileConfig.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | /* | 1 | /* |
2 | * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com> | 2 | * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com> |
3 | * | 3 | * |
4 | * This file is part of Sophie. | 4 | * This file is part of Sophie. |
5 | * | 5 | * |
@@ -18,10 +18,8 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { nanoid } from 'nanoid'; | 21 | import { ProfileSettingsSnapshotIn } from '@sophie/shared'; |
22 | import slug from 'slug'; | ||
23 | 22 | ||
24 | export default function generateId(name?: string | undefined) { | 23 | export default interface ProfileConfig extends ProfileSettingsSnapshotIn { |
25 | const nameSlug = typeof name === 'undefined' ? '' : slug(name); | 24 | id?: string | undefined; |
26 | return `${nameSlug}_${nanoid()}`; | ||
27 | } | 25 | } |
diff --git a/packages/main/src/stores/config/ServiceConfig.ts b/packages/main/src/stores/config/ServiceConfig.ts new file mode 100644 index 0000000..40ea4c9 --- /dev/null +++ b/packages/main/src/stores/config/ServiceConfig.ts | |||
@@ -0,0 +1,29 @@ | |||
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 { ServiceSettingsSnapshotIn } from '@sophie/shared'; | ||
22 | import { ReferenceIdentifier } from 'mobx-state-tree'; | ||
23 | |||
24 | export default interface ServiceConfig | ||
25 | extends Omit<ServiceSettingsSnapshotIn, 'profile'> { | ||
26 | id?: string | undefined; | ||
27 | |||
28 | profile?: ReferenceIdentifier | undefined; | ||
29 | } | ||
diff --git a/packages/main/src/stores/config/loadConfig.ts b/packages/main/src/stores/config/loadConfig.ts new file mode 100644 index 0000000..770d675 --- /dev/null +++ b/packages/main/src/stores/config/loadConfig.ts | |||
@@ -0,0 +1,146 @@ | |||
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 { | ||
22 | applySnapshot, | ||
23 | IMSTArray, | ||
24 | IMSTMap, | ||
25 | IReferenceType, | ||
26 | IStateTreeNode, | ||
27 | IType, | ||
28 | } from 'mobx-state-tree'; | ||
29 | import { nanoid } from 'nanoid'; | ||
30 | import slug from 'slug'; | ||
31 | |||
32 | import GlobalSettings from '../GlobalSettings'; | ||
33 | import type Profile from '../Profile'; | ||
34 | import type { ProfileSettingsSnapshotIn } from '../ProfileSettings'; | ||
35 | import type Service from '../Service'; | ||
36 | import type { ServiceSettingsSnapshotIn } from '../ServiceSettings'; | ||
37 | |||
38 | import type Config from './Config'; | ||
39 | import type ProfileConfig from './ProfileConfig'; | ||
40 | import type ServiceConfig from './ServiceConfig'; | ||
41 | |||
42 | function generateId(name?: string | undefined): string { | ||
43 | const nameSlug = typeof name === 'undefined' ? '' : slug(name); | ||
44 | return `${nameSlug}_${nanoid()}`; | ||
45 | } | ||
46 | |||
47 | function addMissingProfileIds( | ||
48 | profileConfigs: ProfileConfig[] | undefined, | ||
49 | ): [string, ProfileSettingsSnapshotIn][] { | ||
50 | return (profileConfigs ?? []).map((profileConfig) => { | ||
51 | const { id, ...settings } = profileConfig; | ||
52 | return [id === undefined ? generateId(settings.name) : id, settings]; | ||
53 | }); | ||
54 | } | ||
55 | |||
56 | function addMissingServiceIdsAndProfiles( | ||
57 | serviceConfigs: ServiceConfig[] | undefined, | ||
58 | profiles: [string, ProfileSettingsSnapshotIn][], | ||
59 | ): [string, ServiceSettingsSnapshotIn][] { | ||
60 | return (serviceConfigs ?? []).map((serviceConfig) => { | ||
61 | const { id, ...settings } = serviceConfig; | ||
62 | const { name } = settings; | ||
63 | let { profile: profileId } = settings; | ||
64 | if (profileId === undefined) { | ||
65 | profileId = generateId(name); | ||
66 | profiles.push([profileId, { name }]); | ||
67 | } | ||
68 | return [ | ||
69 | id === undefined ? generateId(name) : id, | ||
70 | { ...settings, profile: profileId }, | ||
71 | ]; | ||
72 | }); | ||
73 | } | ||
74 | |||
75 | type TypeWithSettings<C> = IType< | ||
76 | { id: string; settings: C }, | ||
77 | unknown, | ||
78 | { settings: IStateTreeNode<IType<C, unknown, unknown>> } | ||
79 | >; | ||
80 | |||
81 | function applySettings<C, D extends TypeWithSettings<C>>( | ||
82 | current: IMSTArray<IReferenceType<D>>, | ||
83 | currentById: IMSTMap<D>, | ||
84 | toApply: [string, C][], | ||
85 | ): void { | ||
86 | const toApplyById = new Map(toApply); | ||
87 | const toDelete = new Set(currentById.keys()); | ||
88 | toApplyById.forEach((settingsSnapshot, id) => { | ||
89 | const model = currentById.get(id); | ||
90 | if (model === undefined) { | ||
91 | currentById.set(id, { | ||
92 | id, | ||
93 | settings: settingsSnapshot, | ||
94 | }); | ||
95 | } else { | ||
96 | toDelete.delete(id); | ||
97 | applySnapshot(model.settings, settingsSnapshot); | ||
98 | } | ||
99 | }); | ||
100 | toDelete.forEach((id) => currentById.delete(id)); | ||
101 | current.clear(); | ||
102 | current.push(...toApply.map(([id]) => id)); | ||
103 | } | ||
104 | |||
105 | export default function loadConfig( | ||
106 | target: { | ||
107 | readonly profiles: IMSTArray<IReferenceType<typeof Profile>>; | ||
108 | readonly profilesById: IMSTMap<typeof Profile>; | ||
109 | selectedService: Service | undefined; | ||
110 | readonly services: IMSTArray<IReferenceType<typeof Service>>; | ||
111 | readonly servicesById: IMSTMap<typeof Service>; | ||
112 | readonly settings: GlobalSettings; | ||
113 | }, | ||
114 | config: Config, | ||
115 | ): void { | ||
116 | const { | ||
117 | profiles, | ||
118 | profilesById, | ||
119 | selectedService, | ||
120 | services, | ||
121 | servicesById, | ||
122 | settings, | ||
123 | } = target; | ||
124 | const { id: selectedServiceId } = selectedService ?? { id: undefined }; | ||
125 | const { | ||
126 | profiles: profilesConfig, | ||
127 | services: servicesConfig, | ||
128 | ...settingsToApply | ||
129 | } = config; | ||
130 | const profilesToApply = addMissingProfileIds(profilesConfig); | ||
131 | const servicesToApply = addMissingServiceIdsAndProfiles( | ||
132 | servicesConfig, | ||
133 | profilesToApply, | ||
134 | ); | ||
135 | applySettings(profiles, profilesById, profilesToApply); | ||
136 | applySettings(services, servicesById, servicesToApply); | ||
137 | applySnapshot(settings, settingsToApply); | ||
138 | let newSelectedService: Service | undefined; | ||
139 | if (selectedServiceId !== undefined) { | ||
140 | newSelectedService = servicesById.get(selectedServiceId); | ||
141 | } | ||
142 | if (newSelectedService === undefined && services.length > 0) { | ||
143 | [newSelectedService] = services; | ||
144 | } | ||
145 | target.selectedService = newSelectedService; | ||
146 | } | ||