diff options
author | 2022-02-08 21:40:40 +0100 | |
---|---|---|
committer | 2022-02-14 12:13:22 +0100 | |
commit | cc214bb1afd37068c2bbc93f33990ca93f9a900f (patch) | |
tree | 7707415d5c4e42b2542d3f482060d4935c892fe1 /packages/main/src/infrastructure | |
parent | feat: Unread message badges (diff) | |
download | sophie-cc214bb1afd37068c2bbc93f33990ca93f9a900f.tar.gz sophie-cc214bb1afd37068c2bbc93f33990ca93f9a900f.tar.zst sophie-cc214bb1afd37068c2bbc93f33990ca93f9a900f.zip |
feat: Load and switch services
Signed-off-by: Kristóf Marussy <kristof@marussy.com>
Diffstat (limited to 'packages/main/src/infrastructure')
8 files changed, 684 insertions, 23 deletions
diff --git a/packages/main/src/infrastructure/config/ReadConfigResult.ts b/packages/main/src/infrastructure/config/ReadConfigResult.ts deleted file mode 100644 index 3b3ee55..0000000 --- a/packages/main/src/infrastructure/config/ReadConfigResult.ts +++ /dev/null | |||
@@ -1,23 +0,0 @@ | |||
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 | type ReadConfigResult = { found: true; data: unknown } | { found: false }; | ||
22 | |||
23 | export default ReadConfigResult; | ||
diff --git a/packages/main/src/infrastructure/electron/RendererBridge.ts b/packages/main/src/infrastructure/electron/RendererBridge.ts new file mode 100644 index 0000000..8633b9d --- /dev/null +++ b/packages/main/src/infrastructure/electron/RendererBridge.ts | |||
@@ -0,0 +1,103 @@ | |||
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 { SharedStoreSnapshotOut } from '@sophie/shared'; | ||
22 | import { | ||
23 | addMiddleware, | ||
24 | getSnapshot, | ||
25 | IJsonPatch, | ||
26 | IMiddlewareEvent, | ||
27 | onPatch, | ||
28 | } from 'mobx-state-tree'; | ||
29 | |||
30 | import type MainStore from '../../stores/MainStore'; | ||
31 | import Disposer from '../../utils/Disposer'; | ||
32 | import { getLogger } from '../../utils/log'; | ||
33 | |||
34 | const log = getLogger('RendererBridge'); | ||
35 | |||
36 | export type PatchListener = (patch: IJsonPatch[]) => void; | ||
37 | |||
38 | export default class RendererBridge { | ||
39 | snapshot: SharedStoreSnapshotOut; | ||
40 | |||
41 | private readonly disposeOnPatch: Disposer; | ||
42 | |||
43 | private readonly disposeMiddleware: Disposer; | ||
44 | |||
45 | constructor(store: MainStore, listener: PatchListener) { | ||
46 | this.snapshot = getSnapshot(store.shared); | ||
47 | |||
48 | // The call for the currently pending action, if any. | ||
49 | let topLevelCall: IMiddlewareEvent | undefined; | ||
50 | // An array of accumulated patches if we're in an action, `undefined` otherwise. | ||
51 | let patches: IJsonPatch[] | undefined; | ||
52 | |||
53 | this.disposeOnPatch = onPatch(store.shared, (patch) => { | ||
54 | if (patches === undefined) { | ||
55 | // Update unprotected stores (outside an action) right away. | ||
56 | listener([patch]); | ||
57 | } else { | ||
58 | patches.push(patch); | ||
59 | } | ||
60 | }); | ||
61 | |||
62 | this.disposeMiddleware = addMiddleware(store, (call, next, abort) => { | ||
63 | if (call.parentActionEvent !== undefined) { | ||
64 | // We're already in an action, there's no need to enter one. | ||
65 | next(call); | ||
66 | return; | ||
67 | } | ||
68 | if (patches !== undefined) { | ||
69 | log.error( | ||
70 | 'Unexpected call', | ||
71 | call, | ||
72 | 'during dispatching another call', | ||
73 | topLevelCall, | ||
74 | 'with accumulated patches', | ||
75 | patches, | ||
76 | ); | ||
77 | abort(undefined); | ||
78 | return; | ||
79 | } | ||
80 | // Make shure that the saved snapshot is consistent with the patches we're going to send. | ||
81 | this.snapshot = getSnapshot(store.shared); | ||
82 | topLevelCall = call; | ||
83 | patches = []; | ||
84 | try { | ||
85 | next(call); | ||
86 | } finally { | ||
87 | try { | ||
88 | if (patches.length > 0) { | ||
89 | listener(patches); | ||
90 | } | ||
91 | } finally { | ||
92 | topLevelCall = undefined; | ||
93 | patches = undefined; | ||
94 | } | ||
95 | } | ||
96 | }); | ||
97 | } | ||
98 | |||
99 | dispose(): void { | ||
100 | this.disposeMiddleware(); | ||
101 | this.disposeOnPatch(); | ||
102 | } | ||
103 | } | ||
diff --git a/packages/main/src/infrastructure/electron/UserAgents.ts b/packages/main/src/infrastructure/electron/UserAgents.ts new file mode 100644 index 0000000..af7f049 --- /dev/null +++ b/packages/main/src/infrastructure/electron/UserAgents.ts | |||
@@ -0,0 +1,46 @@ | |||
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 | const CHROMELESS_USER_AGENT_REGEX = /^[^:]+:\/\/accounts\.google\.[^./]+\//; | ||
22 | |||
23 | export default class UserAgents { | ||
24 | private readonly default: string; | ||
25 | |||
26 | private readonly chromeless: string; | ||
27 | |||
28 | constructor(readonly mainWindowUserAgent: string) { | ||
29 | this.default = mainWindowUserAgent.replaceAll( | ||
30 | /\s(sophie|Electron)\/\S+/g, | ||
31 | '', | ||
32 | ); | ||
33 | this.chromeless = this.default.replace(/ Chrome\/\S+/, ''); | ||
34 | } | ||
35 | |||
36 | serviceUserAgent(url?: string | undefined): string { | ||
37 | return url !== undefined && CHROMELESS_USER_AGENT_REGEX.test(url) | ||
38 | ? this.chromeless | ||
39 | : this.default; | ||
40 | } | ||
41 | |||
42 | fallbackUserAgent(devMode: boolean): string { | ||
43 | // Removing the electron version breaks redux devtools, so we only do this in production. | ||
44 | return devMode ? this.mainWindowUserAgent : this.default; | ||
45 | } | ||
46 | } | ||
diff --git a/packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts b/packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts new file mode 100644 index 0000000..cff7957 --- /dev/null +++ b/packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts | |||
@@ -0,0 +1,169 @@ | |||
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 | Action, | ||
23 | MainToRendererIpcMessage, | ||
24 | RendererToMainIpcMessage, | ||
25 | } from '@sophie/shared'; | ||
26 | import { BrowserWindow, ipcMain, IpcMainEvent } from 'electron'; | ||
27 | import type { IJsonPatch } from 'mobx-state-tree'; | ||
28 | |||
29 | import type MainStore from '../../../stores/MainStore'; | ||
30 | import { getLogger } from '../../../utils/log'; | ||
31 | import RendererBridge from '../RendererBridge'; | ||
32 | import type { MainWindow, ServiceView } from '../types'; | ||
33 | |||
34 | import ElectronServiceView from './ElectronServiceView'; | ||
35 | import type ElectronViewFactory from './ElectronViewFactory'; | ||
36 | import { openDevToolsWhenReady } from './devTools'; | ||
37 | import lockWebContentsToFile from './lockWebContentsToFile'; | ||
38 | |||
39 | const log = getLogger('ElectronMainWindow'); | ||
40 | |||
41 | export default class ElectronMainWindow implements MainWindow { | ||
42 | private readonly browserWindow: BrowserWindow; | ||
43 | |||
44 | private readonly bridge: RendererBridge; | ||
45 | |||
46 | private readonly dispatchActionHandler = ( | ||
47 | event: IpcMainEvent, | ||
48 | rawAction: unknown, | ||
49 | ): void => { | ||
50 | const { id } = event.sender; | ||
51 | if (id !== this.browserWindow.webContents.id) { | ||
52 | log.warn( | ||
53 | 'Unexpected', | ||
54 | RendererToMainIpcMessage.DispatchAction, | ||
55 | 'from webContents', | ||
56 | id, | ||
57 | ); | ||
58 | return; | ||
59 | } | ||
60 | try { | ||
61 | const action = Action.parse(rawAction); | ||
62 | this.store.dispatch(action); | ||
63 | } catch (error) { | ||
64 | log.error('Error while dispatching renderer action', rawAction, error); | ||
65 | } | ||
66 | }; | ||
67 | |||
68 | constructor( | ||
69 | private readonly store: MainStore, | ||
70 | private readonly parent: ElectronViewFactory, | ||
71 | ) { | ||
72 | this.browserWindow = new BrowserWindow({ | ||
73 | show: false, | ||
74 | autoHideMenuBar: true, | ||
75 | darkTheme: store.shared.shouldUseDarkColors, | ||
76 | webPreferences: { | ||
77 | sandbox: true, | ||
78 | devTools: parent.devMode, | ||
79 | preload: parent.resources.getPath('preload', 'index.cjs'), | ||
80 | }, | ||
81 | }); | ||
82 | |||
83 | const { webContents } = this.browserWindow; | ||
84 | |||
85 | ipcMain.handle(RendererToMainIpcMessage.GetSharedStoreSnapshot, (event) => { | ||
86 | const { id } = event.sender; | ||
87 | if (id !== webContents.id) { | ||
88 | log.warn( | ||
89 | 'Unexpected', | ||
90 | RendererToMainIpcMessage.GetSharedStoreSnapshot, | ||
91 | 'from webContents', | ||
92 | id, | ||
93 | ); | ||
94 | throw new Error('Invalid IPC call'); | ||
95 | } | ||
96 | return this.bridge.snapshot; | ||
97 | }); | ||
98 | |||
99 | this.bridge = new RendererBridge(store, (patch) => { | ||
100 | webContents.send(MainToRendererIpcMessage.SharedStorePatch, patch); | ||
101 | }); | ||
102 | |||
103 | ipcMain.on( | ||
104 | RendererToMainIpcMessage.DispatchAction, | ||
105 | this.dispatchActionHandler, | ||
106 | ); | ||
107 | |||
108 | webContents.userAgent = parent.userAgents.mainWindowUserAgent; | ||
109 | |||
110 | this.browserWindow.on('ready-to-show', () => this.browserWindow.show()); | ||
111 | |||
112 | this.browserWindow.on('close', () => this.dispose()); | ||
113 | |||
114 | if (parent.devMode) { | ||
115 | openDevToolsWhenReady(this.browserWindow); | ||
116 | } | ||
117 | } | ||
118 | |||
119 | bringToForeground(): void { | ||
120 | if (!this.browserWindow.isVisible()) { | ||
121 | this.browserWindow.show(); | ||
122 | } | ||
123 | if (this.browserWindow.isMinimized()) { | ||
124 | this.browserWindow.restore(); | ||
125 | } | ||
126 | this.browserWindow.focus(); | ||
127 | } | ||
128 | |||
129 | setServiceView(serviceView: ServiceView | undefined) { | ||
130 | if (serviceView === undefined) { | ||
131 | // eslint-disable-next-line unicorn/no-null -- Electron API requires passing `null`. | ||
132 | this.browserWindow.setBrowserView(null); | ||
133 | return; | ||
134 | } | ||
135 | if (serviceView instanceof ElectronServiceView) { | ||
136 | this.browserWindow.setBrowserView(serviceView.browserView); | ||
137 | serviceView.browserView.setBackgroundColor('#fff'); | ||
138 | return; | ||
139 | } | ||
140 | throw new TypeError( | ||
141 | 'Unexpected ServiceView with no underlying BrowserView', | ||
142 | ); | ||
143 | } | ||
144 | |||
145 | dispose() { | ||
146 | this.bridge.dispose(); | ||
147 | this.browserWindow.destroy(); | ||
148 | ipcMain.removeHandler(RendererToMainIpcMessage.GetSharedStoreSnapshot); | ||
149 | ipcMain.removeListener( | ||
150 | RendererToMainIpcMessage.DispatchAction, | ||
151 | this.dispatchActionHandler, | ||
152 | ); | ||
153 | } | ||
154 | |||
155 | loadInterface(): Promise<void> { | ||
156 | return lockWebContentsToFile( | ||
157 | this.parent.resources, | ||
158 | 'index.html', | ||
159 | this.browserWindow.webContents, | ||
160 | ); | ||
161 | } | ||
162 | |||
163 | sendSharedStorePatch(patch: IJsonPatch[]): void { | ||
164 | this.browserWindow.webContents.send( | ||
165 | MainToRendererIpcMessage.SharedStorePatch, | ||
166 | patch, | ||
167 | ); | ||
168 | } | ||
169 | } | ||
diff --git a/packages/main/src/infrastructure/electron/impl/ElectronPartition.ts b/packages/main/src/infrastructure/electron/impl/ElectronPartition.ts new file mode 100644 index 0000000..e60ce21 --- /dev/null +++ b/packages/main/src/infrastructure/electron/impl/ElectronPartition.ts | |||
@@ -0,0 +1,59 @@ | |||
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 { Session, session } from 'electron'; | ||
22 | |||
23 | import type Profile from '../../../stores/Profile'; | ||
24 | import type { Partition } from '../types'; | ||
25 | |||
26 | import type ElectronViewFactory from './ElectronViewFactory'; | ||
27 | |||
28 | export default class ElectronPartition implements Partition { | ||
29 | readonly id: string; | ||
30 | |||
31 | readonly session: Session; | ||
32 | |||
33 | constructor(profile: Profile, parent: ElectronViewFactory) { | ||
34 | this.id = profile.id; | ||
35 | this.session = session.fromPartition(`persist:${profile.id}`); | ||
36 | this.session.setPermissionRequestHandler( | ||
37 | (_webContents, permission, callback) => { | ||
38 | // TODO Handle screen sharing. | ||
39 | callback(permission === 'notifications'); | ||
40 | }, | ||
41 | ); | ||
42 | this.session.setUserAgent(parent.userAgents.serviceUserAgent()); | ||
43 | this.session.webRequest.onBeforeSendHeaders( | ||
44 | ({ url, requestHeaders }, callback) => { | ||
45 | callback({ | ||
46 | requestHeaders: { | ||
47 | ...requestHeaders, | ||
48 | 'User-Agent': parent.userAgents.serviceUserAgent(url), | ||
49 | }, | ||
50 | }); | ||
51 | }, | ||
52 | ); | ||
53 | } | ||
54 | |||
55 | // eslint-disable-next-line class-methods-use-this -- Implementing interface method. | ||
56 | dispose(): void { | ||
57 | // No reactions to dispose yet. | ||
58 | } | ||
59 | } | ||
diff --git a/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts b/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts new file mode 100644 index 0000000..c4f7e4d --- /dev/null +++ b/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts | |||
@@ -0,0 +1,121 @@ | |||
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 { BrowserViewBounds } from '@sophie/shared'; | ||
22 | import { BrowserView } from 'electron'; | ||
23 | |||
24 | import type Service from '../../../stores/Service'; | ||
25 | import type Resources from '../../resources/Resources'; | ||
26 | import type { ServiceView } from '../types'; | ||
27 | |||
28 | import ElectronPartition from './ElectronPartition'; | ||
29 | import type ElectronViewFactory from './ElectronViewFactory'; | ||
30 | |||
31 | export default class ElectronServiceView implements ServiceView { | ||
32 | readonly id: string; | ||
33 | |||
34 | readonly partitionId: string; | ||
35 | |||
36 | readonly browserView: BrowserView; | ||
37 | |||
38 | constructor( | ||
39 | service: Service, | ||
40 | resources: Resources, | ||
41 | partition: ElectronPartition, | ||
42 | private readonly parent: ElectronViewFactory, | ||
43 | ) { | ||
44 | this.id = service.id; | ||
45 | this.partitionId = partition.id; | ||
46 | this.browserView = new BrowserView({ | ||
47 | webPreferences: { | ||
48 | sandbox: true, | ||
49 | nodeIntegrationInSubFrames: true, | ||
50 | preload: resources.getPath('service-preload', 'index.cjs'), | ||
51 | session: partition.session, | ||
52 | }, | ||
53 | }); | ||
54 | |||
55 | const { webContents } = this.browserView; | ||
56 | |||
57 | function setLocation(url: string) { | ||
58 | service.setLocation({ | ||
59 | url, | ||
60 | canGoBack: webContents.canGoBack(), | ||
61 | canGoForward: webContents.canGoForward(), | ||
62 | }); | ||
63 | } | ||
64 | |||
65 | webContents.on('did-navigate', (_event, url) => { | ||
66 | setLocation(url); | ||
67 | }); | ||
68 | |||
69 | webContents.on('did-navigate-in-page', (_event, url, isMainFrame) => { | ||
70 | if (isMainFrame) { | ||
71 | setLocation(url); | ||
72 | } | ||
73 | }); | ||
74 | |||
75 | webContents.on('page-title-updated', (_event, title) => { | ||
76 | service.setTitle(title); | ||
77 | }); | ||
78 | |||
79 | webContents.on('did-start-loading', () => { | ||
80 | service.startedLoading(); | ||
81 | }); | ||
82 | |||
83 | webContents.on('did-finish-load', () => { | ||
84 | service.finishedLoading(); | ||
85 | }); | ||
86 | |||
87 | webContents.on('render-process-gone', () => { | ||
88 | service.crashed(); | ||
89 | }); | ||
90 | } | ||
91 | |||
92 | get webContentsId(): number { | ||
93 | return this.browserView.webContents.id; | ||
94 | } | ||
95 | |||
96 | loadURL(url: string): Promise<void> { | ||
97 | return this.browserView.webContents.loadURL(url); | ||
98 | } | ||
99 | |||
100 | goBack(): void { | ||
101 | this.browserView.webContents.goBack(); | ||
102 | } | ||
103 | |||
104 | goForward(): void { | ||
105 | this.browserView.webContents.goForward(); | ||
106 | } | ||
107 | |||
108 | setBounds(bounds: BrowserViewBounds): void { | ||
109 | this.browserView.setBounds(bounds); | ||
110 | } | ||
111 | |||
112 | dispose(): void { | ||
113 | this.parent.unregisterServiceView(this.webContentsId); | ||
114 | setImmediate(() => { | ||
115 | // Undocumented electron API, see e.g., https://github.com/electron/electron/issues/29626 | ||
116 | ( | ||
117 | this.browserView.webContents as unknown as { destroy(): void } | ||
118 | ).destroy(); | ||
119 | }); | ||
120 | } | ||
121 | } | ||
diff --git a/packages/main/src/infrastructure/electron/impl/ElectronViewFactory.ts b/packages/main/src/infrastructure/electron/impl/ElectronViewFactory.ts new file mode 100644 index 0000000..f8b4a36 --- /dev/null +++ b/packages/main/src/infrastructure/electron/impl/ElectronViewFactory.ts | |||
@@ -0,0 +1,119 @@ | |||
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 { readFile } from 'node:fs/promises'; | ||
22 | |||
23 | import { ServiceToMainIpcMessage } from '@sophie/service-shared'; | ||
24 | import { ipcMain, WebSource } from 'electron'; | ||
25 | |||
26 | import type MainStore from '../../../stores/MainStore'; | ||
27 | import type Profile from '../../../stores/Profile'; | ||
28 | import type Service from '../../../stores/Service'; | ||
29 | import { getLogger } from '../../../utils/log'; | ||
30 | import type Resources from '../../resources/Resources'; | ||
31 | import type UserAgents from '../UserAgents'; | ||
32 | import type { MainWindow, Partition, ServiceView, ViewFactory } from '../types'; | ||
33 | |||
34 | import ElectronMainWindow from './ElectronMainWindow'; | ||
35 | import ElectronPartition from './ElectronPartition'; | ||
36 | import ElectronServiceView from './ElectronServiceView'; | ||
37 | |||
38 | const log = getLogger('ElectronViewFactory'); | ||
39 | |||
40 | export default class ElectronViewFactory implements ViewFactory { | ||
41 | private readonly webContentsIdToServiceView = new Map< | ||
42 | number, | ||
43 | ElectronServiceView | ||
44 | >(); | ||
45 | |||
46 | private serviceInjectSource: WebSource | undefined; | ||
47 | |||
48 | constructor( | ||
49 | readonly userAgents: UserAgents, | ||
50 | readonly resources: Resources, | ||
51 | readonly devMode: boolean, | ||
52 | ) { | ||
53 | ipcMain.handle(ServiceToMainIpcMessage.ApiExposedInMainWorld, (event) => { | ||
54 | if (!this.webContentsIdToServiceView.has(event.sender.id)) { | ||
55 | log.error( | ||
56 | 'Unexpected', | ||
57 | ServiceToMainIpcMessage.ApiExposedInMainWorld, | ||
58 | 'IPC message from webContents', | ||
59 | event.sender.id, | ||
60 | ); | ||
61 | throw new Error('Invalid IPC call'); | ||
62 | } | ||
63 | if (this.serviceInjectSource === undefined) { | ||
64 | log.error('Service inject source was not loaded'); | ||
65 | } | ||
66 | return this.serviceInjectSource; | ||
67 | }); | ||
68 | } | ||
69 | |||
70 | async createMainWindow(store: MainStore): Promise<MainWindow> { | ||
71 | const mainWindow = new ElectronMainWindow(store, this); | ||
72 | await mainWindow.loadInterface(); | ||
73 | return mainWindow; | ||
74 | } | ||
75 | |||
76 | createPartition(profile: Profile): Partition { | ||
77 | return new ElectronPartition(profile, this); | ||
78 | } | ||
79 | |||
80 | createServiceView(service: Service, partition: Partition): ServiceView { | ||
81 | if (partition instanceof ElectronPartition) { | ||
82 | const serviceView = new ElectronServiceView( | ||
83 | service, | ||
84 | this.resources, | ||
85 | partition, | ||
86 | this, | ||
87 | ); | ||
88 | this.webContentsIdToServiceView.set( | ||
89 | serviceView.webContentsId, | ||
90 | serviceView, | ||
91 | ); | ||
92 | return serviceView; | ||
93 | } | ||
94 | throw new TypeError('Unexpected ProfileSession is not a WrappedSession'); | ||
95 | } | ||
96 | |||
97 | async loadServiceInject(): Promise<void> { | ||
98 | const injectPackage = 'service-inject'; | ||
99 | const injectFile = 'index.js'; | ||
100 | const injectPath = this.resources.getPath(injectPackage, injectFile); | ||
101 | this.serviceInjectSource = { | ||
102 | code: await readFile(injectPath, 'utf-8'), | ||
103 | url: this.resources.getFileURL(injectPackage, injectFile), | ||
104 | }; | ||
105 | } | ||
106 | |||
107 | dispose(): void { | ||
108 | if (this.webContentsIdToServiceView.size > 0) { | ||
109 | throw new Error( | ||
110 | 'Must dispose all ServiceView instances before disposing ViewFactory', | ||
111 | ); | ||
112 | } | ||
113 | ipcMain.removeHandler(ServiceToMainIpcMessage.ApiExposedInMainWorld); | ||
114 | } | ||
115 | |||
116 | unregisterServiceView(id: number): void { | ||
117 | this.webContentsIdToServiceView.delete(id); | ||
118 | } | ||
119 | } | ||
diff --git a/packages/main/src/infrastructure/electron/types.ts b/packages/main/src/infrastructure/electron/types.ts new file mode 100644 index 0000000..9f03214 --- /dev/null +++ b/packages/main/src/infrastructure/electron/types.ts | |||
@@ -0,0 +1,67 @@ | |||
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 { BrowserViewBounds } from '@sophie/shared'; | ||
22 | |||
23 | import type MainStore from '../../stores/MainStore'; | ||
24 | import type Profile from '../../stores/Profile'; | ||
25 | import type Service from '../../stores/Service'; | ||
26 | |||
27 | export interface ViewFactory { | ||
28 | createMainWindow(store: MainStore): Promise<MainWindow>; | ||
29 | |||
30 | createPartition(profile: Profile): Partition; | ||
31 | |||
32 | createServiceView(service: Service, partition: Partition): ServiceView; | ||
33 | |||
34 | loadServiceInject(): Promise<void>; | ||
35 | |||
36 | dispose(): void; | ||
37 | } | ||
38 | |||
39 | export interface MainWindow { | ||
40 | bringToForeground(): void; | ||
41 | |||
42 | setServiceView(serviceView: ServiceView | undefined): void; | ||
43 | |||
44 | dispose(): void; | ||
45 | } | ||
46 | |||
47 | export interface Partition { | ||
48 | readonly id: string; | ||
49 | |||
50 | dispose(): void; | ||
51 | } | ||
52 | |||
53 | export interface ServiceView { | ||
54 | readonly id: string; | ||
55 | |||
56 | readonly partitionId: string; | ||
57 | |||
58 | loadURL(url: string): Promise<void>; | ||
59 | |||
60 | goBack(): void; | ||
61 | |||
62 | goForward(): void; | ||
63 | |||
64 | setBounds(bounds: BrowserViewBounds): void; | ||
65 | |||
66 | dispose(): void; | ||
67 | } | ||