diff options
Diffstat (limited to 'packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts')
-rw-r--r-- | packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts | 130 |
1 files changed, 113 insertions, 17 deletions
diff --git a/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts b/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts index c4f7e4d..91247c8 100644 --- a/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts +++ b/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts | |||
@@ -18,15 +18,17 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import type { BrowserViewBounds } from '@sophie/shared'; | ||
22 | import { BrowserView } from 'electron'; | 21 | import { BrowserView } from 'electron'; |
23 | 22 | ||
24 | import type Service from '../../../stores/Service'; | 23 | import type Service from '../../../stores/Service.js'; |
25 | import type Resources from '../../resources/Resources'; | 24 | import getLogger from '../../../utils/getLogger.js'; |
26 | import type { ServiceView } from '../types'; | 25 | import type Resources from '../../resources/Resources.js'; |
26 | import type { ServiceView } from '../types.js'; | ||
27 | 27 | ||
28 | import ElectronPartition from './ElectronPartition'; | 28 | import ElectronPartition from './ElectronPartition.js'; |
29 | import type ElectronViewFactory from './ElectronViewFactory'; | 29 | import type ElectronViewFactory from './ElectronViewFactory.js'; |
30 | |||
31 | const log = getLogger('ElectronServiceView'); | ||
30 | 32 | ||
31 | export default class ElectronServiceView implements ServiceView { | 33 | export default class ElectronServiceView implements ServiceView { |
32 | readonly id: string; | 34 | readonly id: string; |
@@ -36,7 +38,7 @@ export default class ElectronServiceView implements ServiceView { | |||
36 | readonly browserView: BrowserView; | 38 | readonly browserView: BrowserView; |
37 | 39 | ||
38 | constructor( | 40 | constructor( |
39 | service: Service, | 41 | private readonly service: Service, |
40 | resources: Resources, | 42 | resources: Resources, |
41 | partition: ElectronPartition, | 43 | partition: ElectronPartition, |
42 | private readonly parent: ElectronViewFactory, | 44 | private readonly parent: ElectronViewFactory, |
@@ -52,6 +54,15 @@ export default class ElectronServiceView implements ServiceView { | |||
52 | }, | 54 | }, |
53 | }); | 55 | }); |
54 | 56 | ||
57 | this.browserView.setBackgroundColor('#fff'); | ||
58 | this.browserView.setAutoResize({ | ||
59 | width: true, | ||
60 | height: true, | ||
61 | }); | ||
62 | // Util we first attach `browserView` to a `BrowserWindow`, | ||
63 | // `setBounds` calls will be ignored, so there's no point in callind `updateBounds` here. | ||
64 | // It will be called by `ElectronMainWindow` when we first attach this service to it. | ||
65 | |||
55 | const { webContents } = this.browserView; | 66 | const { webContents } = this.browserView; |
56 | 67 | ||
57 | function setLocation(url: string) { | 68 | function setLocation(url: string) { |
@@ -72,20 +83,83 @@ export default class ElectronServiceView implements ServiceView { | |||
72 | } | 83 | } |
73 | }); | 84 | }); |
74 | 85 | ||
86 | webContents.on( | ||
87 | 'did-fail-load', | ||
88 | (_event, errorCode, errorDesc, url, isMainFrame) => { | ||
89 | if (errorCode === -3) { | ||
90 | // Do not signal error on ABORTED, since that corresponds to an action requested by the user. | ||
91 | // Other events (`did-start-loading` or `did-stop-loading`) will cause service state changes | ||
92 | // that are appropriate for the user action. | ||
93 | log.debug('Loading', url, 'in service', this.id, 'aborted by user'); | ||
94 | return; | ||
95 | } | ||
96 | if (isMainFrame) { | ||
97 | setLocation(url); | ||
98 | service.setFailed(errorCode, errorDesc); | ||
99 | log.warn( | ||
100 | 'Failed to load', | ||
101 | url, | ||
102 | 'in service', | ||
103 | this.id, | ||
104 | errorCode, | ||
105 | errorDesc, | ||
106 | ); | ||
107 | } | ||
108 | }, | ||
109 | ); | ||
110 | |||
111 | /** | ||
112 | * We use the `'certificate-error'` event instead of `session.setCertificateVerifyProc` | ||
113 | * because: | ||
114 | * | ||
115 | * 1. `'certificate-error'` is bound to the `webContents`, so we can display the certificate | ||
116 | * in the place of the correct service. Note that chromium still manages certificate trust | ||
117 | * per session, so we can't have different trusted certificates for each service of a | ||
118 | * profile. | ||
119 | * 2. The results of `'certificate-error'` are _not_ cached, so we can initially reject | ||
120 | * the certificate but we can still accept it once the user trusts it temporarily. | ||
121 | */ | ||
122 | webContents.on( | ||
123 | 'certificate-error', | ||
124 | (event, url, error, certificate, callback, isMainFrame) => { | ||
125 | if (service.isCertificateTemporarilyTrusted(certificate)) { | ||
126 | event.preventDefault(); | ||
127 | callback(true); | ||
128 | return; | ||
129 | } | ||
130 | if (isMainFrame) { | ||
131 | setLocation(url); | ||
132 | service.setCertificateError(error, certificate); | ||
133 | } | ||
134 | callback(false); | ||
135 | }, | ||
136 | ); | ||
137 | |||
75 | webContents.on('page-title-updated', (_event, title) => { | 138 | webContents.on('page-title-updated', (_event, title) => { |
76 | service.setTitle(title); | 139 | service.setTitle(title); |
77 | }); | 140 | }); |
78 | 141 | ||
79 | webContents.on('did-start-loading', () => { | 142 | webContents.on('did-start-loading', () => { |
80 | service.startedLoading(); | 143 | service.startLoading(); |
144 | }); | ||
145 | |||
146 | webContents.on('did-stop-loading', () => { | ||
147 | service.finishLoading(); | ||
81 | }); | 148 | }); |
82 | 149 | ||
83 | webContents.on('did-finish-load', () => { | 150 | webContents.on('render-process-gone', (_event, details) => { |
84 | service.finishedLoading(); | 151 | const { reason, exitCode } = details; |
152 | service.setCrashed(reason, exitCode); | ||
85 | }); | 153 | }); |
86 | 154 | ||
87 | webContents.on('render-process-gone', () => { | 155 | webContents.setWindowOpenHandler(({ url }) => { |
88 | service.crashed(); | 156 | // TODO Add filtering (allowlist) by URL. |
157 | // TODO Handle `new-window` disposition where the service wants an object returned by | ||
158 | // `window.open`. | ||
159 | // TODO Handle downloads with `save-to-disk` disposition. | ||
160 | // TODO Handle POST bodies where the window must be allowed to open or the data is lost. | ||
161 | service.addBlockedPopup(url); | ||
162 | return { action: 'deny' }; | ||
89 | }); | 163 | }); |
90 | } | 164 | } |
91 | 165 | ||
@@ -93,8 +167,10 @@ export default class ElectronServiceView implements ServiceView { | |||
93 | return this.browserView.webContents.id; | 167 | return this.browserView.webContents.id; |
94 | } | 168 | } |
95 | 169 | ||
96 | loadURL(url: string): Promise<void> { | 170 | loadURL(url: string): void { |
97 | return this.browserView.webContents.loadURL(url); | 171 | this.browserView.webContents.loadURL(url).catch((error) => { |
172 | log.warn('Error while loading', url, 'in service', this.id, error); | ||
173 | }); | ||
98 | } | 174 | } |
99 | 175 | ||
100 | goBack(): void { | 176 | goBack(): void { |
@@ -105,13 +181,33 @@ export default class ElectronServiceView implements ServiceView { | |||
105 | this.browserView.webContents.goForward(); | 181 | this.browserView.webContents.goForward(); |
106 | } | 182 | } |
107 | 183 | ||
108 | setBounds(bounds: BrowserViewBounds): void { | 184 | reload(ignoreCache: boolean): void { |
109 | this.browserView.setBounds(bounds); | 185 | if (ignoreCache) { |
186 | this.browserView.webContents.reloadIgnoringCache(); | ||
187 | } else { | ||
188 | this.browserView.webContents.reload(); | ||
189 | } | ||
190 | } | ||
191 | |||
192 | stop(): void { | ||
193 | this.browserView.webContents.stop(); | ||
194 | } | ||
195 | |||
196 | toggleDeveloperTools(): void { | ||
197 | this.browserView.webContents.toggleDevTools(); | ||
198 | } | ||
199 | |||
200 | updateBounds(): void { | ||
201 | const { x, y, width, height, hasBounds } = this.service; | ||
202 | if (!hasBounds) { | ||
203 | return; | ||
204 | } | ||
205 | this.browserView.setBounds({ x, y, width, height }); | ||
110 | } | 206 | } |
111 | 207 | ||
112 | dispose(): void { | 208 | dispose(): void { |
113 | this.parent.unregisterServiceView(this.webContentsId); | ||
114 | setImmediate(() => { | 209 | setImmediate(() => { |
210 | this.parent.unregisterServiceView(this.webContentsId); | ||
115 | // Undocumented electron API, see e.g., https://github.com/electron/electron/issues/29626 | 211 | // Undocumented electron API, see e.g., https://github.com/electron/electron/issues/29626 |
116 | ( | 212 | ( |
117 | this.browserView.webContents as unknown as { destroy(): void } | 213 | this.browserView.webContents as unknown as { destroy(): void } |