aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts3
-rw-r--r--packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts20
-rw-r--r--packages/main/src/infrastructure/electron/types.ts4
-rw-r--r--packages/main/src/reactions/loadServices.ts8
-rw-r--r--packages/main/src/stores/MainStore.ts19
-rw-r--r--packages/main/src/stores/Service.ts26
-rw-r--r--packages/renderer/src/components/App.tsx22
-rw-r--r--packages/renderer/src/components/BrowserViewPlaceholder.tsx14
-rw-r--r--packages/renderer/src/components/ServicePanel.tsx76
-rw-r--r--packages/renderer/src/components/banner/InsecureConnectionBanner.tsx7
-rw-r--r--packages/renderer/src/components/banner/NewWindowBanner.tsx7
-rw-r--r--packages/renderer/src/components/errorPage/ErrorPage.tsx8
-rw-r--r--packages/renderer/src/components/locationBar/ExtraButtons.tsx10
-rw-r--r--packages/renderer/src/components/locationBar/LocationBar.tsx24
-rw-r--r--packages/renderer/src/components/locationBar/LocationTextField.tsx21
-rw-r--r--packages/renderer/src/components/locationBar/NavigationButtons.tsx27
-rw-r--r--packages/renderer/src/components/sidebar/ServiceSwitcher.tsx2
-rw-r--r--packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx8
-rw-r--r--packages/renderer/src/stores/RendererStore.ts16
-rw-r--r--packages/renderer/src/stores/Service.ts12
-rw-r--r--packages/shared/src/schemas/Action.ts5
-rw-r--r--packages/shared/src/schemas/ServiceAction.ts6
-rw-r--r--packages/shared/src/stores/SharedStoreBase.ts5
23 files changed, 209 insertions, 141 deletions
diff --git a/packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts b/packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts
index edc6592..b0db115 100644
--- a/packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts
+++ b/packages/main/src/infrastructure/electron/impl/ElectronMainWindow.ts
@@ -153,6 +153,9 @@ export default class ElectronMainWindow implements MainWindow {
153 } 153 }
154 if (serviceView instanceof ElectronServiceView) { 154 if (serviceView instanceof ElectronServiceView) {
155 this.browserWindow.setBrowserView(serviceView.browserView); 155 this.browserWindow.setBrowserView(serviceView.browserView);
156 // If this `BrowserView` hasn't been attached previously,
157 // we must update its bounds _after_ attaching for the resizing to take effect.
158 serviceView.updateBounds();
156 return; 159 return;
157 } 160 }
158 throw new TypeError( 161 throw new TypeError(
diff --git a/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts b/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts
index 2e64269..3118efc 100644
--- a/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts
+++ b/packages/main/src/infrastructure/electron/impl/ElectronServiceView.ts
@@ -18,7 +18,6 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import type { BrowserViewBounds } from '@sophie/shared';
22import { BrowserView } from 'electron'; 21import { BrowserView } from 'electron';
23 22
24import type Service from '../../../stores/Service'; 23import type Service from '../../../stores/Service';
@@ -39,7 +38,7 @@ export default class ElectronServiceView implements ServiceView {
39 readonly browserView: BrowserView; 38 readonly browserView: BrowserView;
40 39
41 constructor( 40 constructor(
42 service: Service, 41 private readonly service: Service,
43 resources: Resources, 42 resources: Resources,
44 partition: ElectronPartition, 43 partition: ElectronPartition,
45 private readonly parent: ElectronViewFactory, 44 private readonly parent: ElectronViewFactory,
@@ -56,6 +55,13 @@ export default class ElectronServiceView implements ServiceView {
56 }); 55 });
57 56
58 this.browserView.setBackgroundColor('#fff'); 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.
59 65
60 const { webContents } = this.browserView; 66 const { webContents } = this.browserView;
61 67
@@ -191,13 +197,17 @@ export default class ElectronServiceView implements ServiceView {
191 this.browserView.webContents.toggleDevTools(); 197 this.browserView.webContents.toggleDevTools();
192 } 198 }
193 199
194 setBounds(bounds: BrowserViewBounds): void { 200 updateBounds(): void {
195 this.browserView.setBounds(bounds); 201 const { x, y, width, height, hasBounds } = this.service;
202 if (!hasBounds) {
203 return;
204 }
205 this.browserView.setBounds({ x, y, width, height });
196 } 206 }
197 207
198 dispose(): void { 208 dispose(): void {
199 this.parent.unregisterServiceView(this.webContentsId);
200 setImmediate(() => { 209 setImmediate(() => {
210 this.parent.unregisterServiceView(this.webContentsId);
201 // 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
202 ( 212 (
203 this.browserView.webContents as unknown as { destroy(): void } 213 this.browserView.webContents as unknown as { destroy(): void }
diff --git a/packages/main/src/infrastructure/electron/types.ts b/packages/main/src/infrastructure/electron/types.ts
index 1321048..92ca9ad 100644
--- a/packages/main/src/infrastructure/electron/types.ts
+++ b/packages/main/src/infrastructure/electron/types.ts
@@ -18,8 +18,6 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import type { BrowserViewBounds } from '@sophie/shared';
22
23import type MainStore from '../../stores/MainStore'; 21import type MainStore from '../../stores/MainStore';
24import type Profile from '../../stores/Profile'; 22import type Profile from '../../stores/Profile';
25import type Service from '../../stores/Service'; 23import type Service from '../../stores/Service';
@@ -67,7 +65,7 @@ export interface ServiceView {
67 65
68 toggleDeveloperTools(): void; 66 toggleDeveloperTools(): void;
69 67
70 setBounds(bounds: BrowserViewBounds): void; 68 updateBounds(): void;
71 69
72 dispose(): void; 70 dispose(): void;
73} 71}
diff --git a/packages/main/src/reactions/loadServices.ts b/packages/main/src/reactions/loadServices.ts
index 4ef6131..f56ac62 100644
--- a/packages/main/src/reactions/loadServices.ts
+++ b/packages/main/src/reactions/loadServices.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
21import { autorun, reaction } from 'mobx'; 21import { reaction } from 'mobx';
22import { addDisposer } from 'mobx-state-tree'; 22import { addDisposer } from 'mobx-state-tree';
23 23
24import type { 24import type {
@@ -94,7 +94,6 @@ export default function loadServices(
94 throw new Error(`Missing Partition ${profileId}`); 94 throw new Error(`Missing Partition ${profileId}`);
95 } 95 }
96 view = viewFactory.createServiceView(service, partition); 96 view = viewFactory.createServiceView(service, partition);
97 view.setBounds(store.browserViewBounds);
98 servicesToViews.set(serviceId, view); 97 servicesToViews.set(serviceId, view);
99 service.setServiceView(view); 98 service.setServiceView(view);
100 const { urlToLoad } = service; 99 const { urlToLoad } = service;
@@ -133,12 +132,7 @@ export default function loadServices(
133 }, 132 },
134 ); 133 );
135 134
136 const resizeDisposer = autorun(() => {
137 store.visibleService?.serviceView?.setBounds(store.browserViewBounds);
138 });
139
140 addDisposer(store, () => { 135 addDisposer(store, () => {
141 resizeDisposer();
142 disposer(); 136 disposer();
143 store.mainWindow?.setServiceView(undefined); 137 store.mainWindow?.setServiceView(undefined);
144 servicesToViews.forEach((serviceView, serviceId) => { 138 servicesToViews.forEach((serviceView, serviceId) => {
diff --git a/packages/main/src/stores/MainStore.ts b/packages/main/src/stores/MainStore.ts
index d717bed..9affbd0 100644
--- a/packages/main/src/stores/MainStore.ts
+++ b/packages/main/src/stores/MainStore.ts
@@ -18,9 +18,9 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import type { Action, BrowserViewBounds } from '@sophie/shared'; 21import type { Action } from '@sophie/shared';
22import type { ResourceKey } from 'i18next'; 22import type { ResourceKey } from 'i18next';
23import { applySnapshot, flow, Instance, types } from 'mobx-state-tree'; 23import { flow, Instance, types } from 'mobx-state-tree';
24 24
25import type I18nStore from '../i18n/I18nStore'; 25import type I18nStore from '../i18n/I18nStore';
26import type { UseTranslationResult } from '../i18n/I18nStore'; 26import type { UseTranslationResult } from '../i18n/I18nStore';
@@ -37,15 +37,6 @@ const log = getLogger('MainStore');
37 37
38const MainStore = types 38const MainStore = types
39 .model('MainStore', { 39 .model('MainStore', {
40 browserViewBounds: types.optional(
41 types.model('BrowserViewBounds', {
42 x: 0,
43 y: 0,
44 width: 0,
45 height: 0,
46 }),
47 {},
48 ),
49 shared: types.optional(SharedStore, {}), 40 shared: types.optional(SharedStore, {}),
50 i18n: types.frozen<I18nStore | undefined>(), 41 i18n: types.frozen<I18nStore | undefined>(),
51 }) 42 })
@@ -83,9 +74,6 @@ const MainStore = types
83 }), 74 }),
84 ) 75 )
85 .actions((self) => ({ 76 .actions((self) => ({
86 setBrowserViewBounds(bounds: BrowserViewBounds): void {
87 applySnapshot(self.browserViewBounds, bounds);
88 },
89 setMainWindow(mainWindow: MainWindow | undefined): void { 77 setMainWindow(mainWindow: MainWindow | undefined): void {
90 self.mainWindow = mainWindow; 78 self.mainWindow = mainWindow;
91 }, 79 },
@@ -121,9 +109,6 @@ const MainStore = types
121 .actions((self) => ({ 109 .actions((self) => ({
122 dispatch(action: Action): void { 110 dispatch(action: Action): void {
123 switch (action.action) { 111 switch (action.action) {
124 case 'set-browser-view-bounds':
125 self.setBrowserViewBounds(action.browserViewBounds);
126 break;
127 case 'set-selected-service-id': 112 case 'set-selected-service-id':
128 self.settings.setSelectedServiceId(action.serviceId); 113 self.settings.setSelectedServiceId(action.serviceId);
129 break; 114 break;
diff --git a/packages/main/src/stores/Service.ts b/packages/main/src/stores/Service.ts
index 1d46dc9..8ba8098 100644
--- a/packages/main/src/stores/Service.ts
+++ b/packages/main/src/stores/Service.ts
@@ -25,6 +25,7 @@ import {
25 defineServiceModel, 25 defineServiceModel,
26 ServiceAction, 26 ServiceAction,
27 type ServiceStateSnapshotIn, 27 type ServiceStateSnapshotIn,
28 type BrowserViewBounds,
28} from '@sophie/shared'; 29} from '@sophie/shared';
29import { type Instance, getSnapshot, cast, flow } from 'mobx-state-tree'; 30import { type Instance, getSnapshot, cast, flow } from 'mobx-state-tree';
30 31
@@ -41,8 +42,18 @@ const Service = defineServiceModel(ServiceSettings)
41 .volatile( 42 .volatile(
42 (): { 43 (): {
43 serviceView: ServiceView | undefined; 44 serviceView: ServiceView | undefined;
45 x: number;
46 y: number;
47 width: number;
48 height: number;
49 hasBounds: boolean;
44 } => ({ 50 } => ({
45 serviceView: undefined, 51 serviceView: undefined,
52 x: 0,
53 y: 0,
54 width: 0,
55 height: 0,
56 hasBounds: false,
46 }), 57 }),
47 ) 58 )
48 .views((self) => ({ 59 .views((self) => ({
@@ -59,10 +70,20 @@ const Service = defineServiceModel(ServiceSettings)
59 })) 70 }))
60 .views((self) => ({ 71 .views((self) => ({
61 get shouldBeVisible(): boolean { 72 get shouldBeVisible(): boolean {
62 return self.shouldBeLoaded && !self.hasError; 73 // Do not attach service views for which we don't know the appropriate frame size,
74 // because they will just appear in a random location until the frame size is determined.
75 return self.shouldBeLoaded && !self.hasError && self.hasBounds;
63 }, 76 },
64 })) 77 }))
65 .actions((self) => ({ 78 .actions((self) => ({
79 setBrowserViewBounds(bounds: BrowserViewBounds): void {
80 self.x = bounds.x;
81 self.y = bounds.y;
82 self.width = bounds.width;
83 self.height = bounds.height;
84 self.hasBounds = true;
85 self.serviceView?.updateBounds();
86 },
66 setServiceView(serviceView: ServiceView | undefined): void { 87 setServiceView(serviceView: ServiceView | undefined): void {
67 self.serviceView = serviceView; 88 self.serviceView = serviceView;
68 }, 89 },
@@ -263,6 +284,9 @@ const Service = defineServiceModel(ServiceSettings)
263 .actions((self) => ({ 284 .actions((self) => ({
264 dispatch(action: ServiceAction): void { 285 dispatch(action: ServiceAction): void {
265 switch (action.action) { 286 switch (action.action) {
287 case 'set-browser-view-bounds':
288 self.setBrowserViewBounds(action.browserViewBounds);
289 break;
266 case 'back': 290 case 'back':
267 self.goBack(); 291 self.goBack();
268 break; 292 break;
diff --git a/packages/renderer/src/components/App.tsx b/packages/renderer/src/components/App.tsx
index d381abf..2f728e7 100644
--- a/packages/renderer/src/components/App.tsx
+++ b/packages/renderer/src/components/App.tsx
@@ -23,12 +23,8 @@ import { observer } from 'mobx-react-lite';
23import React, { useCallback, useEffect } from 'react'; 23import React, { useCallback, useEffect } from 'react';
24import { useTranslation } from 'react-i18next'; 24import { useTranslation } from 'react-i18next';
25 25
26import BrowserViewPlaceholder from './BrowserViewPlaceholder'; 26import ServicePanel from './ServicePanel';
27import { useStore } from './StoreProvider'; 27import { useStore } from './StoreProvider';
28import InsecureConnectionBanner from './banner/InsecureConnectionBanner';
29import NewWindowBanner from './banner/NewWindowBanner';
30import ErrorPage from './errorPage/ErrorPage';
31import LocationBar from './locationBar/LocationBar';
32import Sidebar from './sidebar/Sidebar'; 28import Sidebar from './sidebar/Sidebar';
33 29
34function App({ devMode }: { devMode: boolean }): JSX.Element { 30function App({ devMode }: { devMode: boolean }): JSX.Element {
@@ -37,6 +33,7 @@ function App({ devMode }: { devMode: boolean }): JSX.Element {
37 }); 33 });
38 const { 34 const {
39 settings: { selectedService }, 35 settings: { selectedService },
36 shared: { services },
40 } = useStore(); 37 } = useStore();
41 const { 38 const {
42 settings: { name: serviceName }, 39 settings: { name: serviceName },
@@ -95,20 +92,13 @@ function App({ devMode }: { devMode: boolean }): JSX.Element {
95 <Box 92 <Box
96 sx={{ 93 sx={{
97 flex: 1, 94 flex: 1,
98 display: 'flex',
99 overflow: 'hidden',
100 flexDirection: 'column',
101 alignItems: 'stretch',
102 height: '100%', 95 height: '100%',
103 zIndex: 100, 96 position: 'relative',
104 }} 97 }}
105 > 98 >
106 <LocationBar /> 99 {services.map((service) => (
107 <InsecureConnectionBanner service={selectedService} /> 100 <ServicePanel key={service.id} service={service} />
108 <NewWindowBanner service={selectedService} /> 101 ))}
109 <BrowserViewPlaceholder>
110 <ErrorPage service={selectedService} />
111 </BrowserViewPlaceholder>
112 </Box> 102 </Box>
113 </Box> 103 </Box>
114 ); 104 );
diff --git a/packages/renderer/src/components/BrowserViewPlaceholder.tsx b/packages/renderer/src/components/BrowserViewPlaceholder.tsx
index 9bd1176..2bfc9b0 100644
--- a/packages/renderer/src/components/BrowserViewPlaceholder.tsx
+++ b/packages/renderer/src/components/BrowserViewPlaceholder.tsx
@@ -22,29 +22,29 @@ import Box from '@mui/material/Box';
22import throttle from 'lodash-es/throttle'; 22import throttle from 'lodash-es/throttle';
23import React, { ReactNode, useCallback, useRef } from 'react'; 23import React, { ReactNode, useCallback, useRef } from 'react';
24 24
25import { useStore } from './StoreProvider'; 25import Service from '../stores/Service';
26 26
27function BrowserViewPlaceholder({ 27function BrowserViewPlaceholder({
28 service,
28 children, 29 children,
29}: { 30}: {
31 service: Service;
30 children?: ReactNode; 32 children?: ReactNode;
31}): JSX.Element { 33}): JSX.Element {
32 const store = useStore();
33
34 // eslint-disable-next-line react-hooks/exhaustive-deps -- react-hooks doesn't support `throttle`. 34 // eslint-disable-next-line react-hooks/exhaustive-deps -- react-hooks doesn't support `throttle`.
35 const onResize = useCallback( 35 const onResize = useCallback(
36 throttle(([entry]: ResizeObserverEntry[]) => { 36 throttle(([entry]: ResizeObserverEntry[]) => {
37 if (entry) { 37 if (entry) {
38 const { x, y, width, height } = entry.target.getBoundingClientRect(); 38 const { x, y, width, height } = entry.target.getBoundingClientRect();
39 store.setBrowserViewBounds({ 39 service.setBrowserViewBounds({
40 x: Math.round(x), 40 x: Math.round(x),
41 y: Math.round(y), 41 y: Math.round(y),
42 width: Math.round(width), 42 width: Math.round(width),
43 height: Math.round(height), 43 height: Math.round(height),
44 }); 44 });
45 } 45 }
46 }, 40), 46 }, 100),
47 [store], 47 [service],
48 ); 48 );
49 49
50 const resizeObserverRef = useRef<ResizeObserver | undefined>(); 50 const resizeObserverRef = useRef<ResizeObserver | undefined>();
@@ -61,7 +61,7 @@ function BrowserViewPlaceholder({
61 resizeObserverRef.current = new ResizeObserver(onResize); 61 resizeObserverRef.current = new ResizeObserver(onResize);
62 resizeObserverRef.current.observe(element); 62 resizeObserverRef.current.observe(element);
63 }, 63 },
64 [onResize, resizeObserverRef], 64 [onResize],
65 ); 65 );
66 66
67 return ( 67 return (
diff --git a/packages/renderer/src/components/ServicePanel.tsx b/packages/renderer/src/components/ServicePanel.tsx
new file mode 100644
index 0000000..de58d24
--- /dev/null
+++ b/packages/renderer/src/components/ServicePanel.tsx
@@ -0,0 +1,76 @@
1/*
2 * Copyright (C) 2021-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
21import Box from '@mui/material/Box';
22import { observer } from 'mobx-react-lite';
23import React from 'react';
24
25import Service from '../stores/Service';
26
27import BrowserViewPlaceholder from './BrowserViewPlaceholder';
28import { useStore } from './StoreProvider';
29import InsecureConnectionBanner from './banner/InsecureConnectionBanner';
30import NewWindowBanner from './banner/NewWindowBanner';
31import ErrorPage from './errorPage/ErrorPage';
32import LocationBar from './locationBar/LocationBar';
33
34export function getServicePanelID(service: Service): string {
35 return `Sophie-${service.id}-ServicePanel`;
36}
37
38function ServicePanel({ service }: { service: Service }): JSX.Element {
39 const {
40 settings: { selectedService },
41 } = useStore();
42
43 const {
44 settings: { name },
45 } = service;
46 const visible = service === selectedService;
47
48 return (
49 <Box
50 id={getServicePanelID(service)}
51 role="tabpanel"
52 aria-label={name}
53 sx={{
54 position: 'absolute',
55 top: 0,
56 left: 0,
57 bottom: 0,
58 right: 0,
59 display: 'flex',
60 overflow: 'hidden',
61 visibility: visible ? 'visible' : 'hidden',
62 flexDirection: 'column',
63 alignItems: 'stretch',
64 }}
65 >
66 <LocationBar service={service} />
67 <InsecureConnectionBanner service={service} />
68 <NewWindowBanner service={service} />
69 <BrowserViewPlaceholder service={service}>
70 <ErrorPage service={service} />
71 </BrowserViewPlaceholder>
72 </Box>
73 );
74}
75
76export default observer(ServicePanel);
diff --git a/packages/renderer/src/components/banner/InsecureConnectionBanner.tsx b/packages/renderer/src/components/banner/InsecureConnectionBanner.tsx
index 7a03fce..0b70db6 100644
--- a/packages/renderer/src/components/banner/InsecureConnectionBanner.tsx
+++ b/packages/renderer/src/components/banner/InsecureConnectionBanner.tsx
@@ -34,17 +34,12 @@ import NotificationBanner from './NotificationBanner';
34function InsecureConnectionBanner({ 34function InsecureConnectionBanner({
35 service, 35 service,
36}: { 36}: {
37 service: Service | undefined; 37 service: Service;
38}): JSX.Element | null { 38}): JSX.Element | null {
39 const { t } = useTranslation(undefined, { 39 const { t } = useTranslation(undefined, {
40 keyPrefix: 'banner.insecureConnection', 40 keyPrefix: 'banner.insecureConnection',
41 }); 41 });
42 42
43 if (service === undefined) {
44 // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering.
45 return null;
46 }
47
48 const { 43 const {
49 canReconnectSecurely, 44 canReconnectSecurely,
50 hasError, 45 hasError,
diff --git a/packages/renderer/src/components/banner/NewWindowBanner.tsx b/packages/renderer/src/components/banner/NewWindowBanner.tsx
index 478de8e..07fafda 100644
--- a/packages/renderer/src/components/banner/NewWindowBanner.tsx
+++ b/packages/renderer/src/components/banner/NewWindowBanner.tsx
@@ -32,17 +32,12 @@ import NotificationBanner from './NotificationBanner';
32function NewWindowBanner({ 32function NewWindowBanner({
33 service, 33 service,
34}: { 34}: {
35 service: Service | undefined; 35 service: Service;
36}): JSX.Element | null { 36}): JSX.Element | null {
37 const { t } = useTranslation(undefined, { 37 const { t } = useTranslation(undefined, {
38 keyPrefix: 'banner.newWindow', 38 keyPrefix: 'banner.newWindow',
39 }); 39 });
40 40
41 if (service === undefined) {
42 // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering.
43 return null;
44 }
45
46 const { 41 const {
47 popups, 42 popups,
48 settings: { name }, 43 settings: { name },
diff --git a/packages/renderer/src/components/errorPage/ErrorPage.tsx b/packages/renderer/src/components/errorPage/ErrorPage.tsx
index 99ca020..10f54cd 100644
--- a/packages/renderer/src/components/errorPage/ErrorPage.tsx
+++ b/packages/renderer/src/components/errorPage/ErrorPage.tsx
@@ -123,14 +123,10 @@ function formatError(service: Service, t: TFunction): ErrorDetails {
123 } 123 }
124} 124}
125 125
126function ErrorPage({ 126function ErrorPage({ service }: { service: Service }): JSX.Element | null {
127 service,
128}: {
129 service: Service | undefined;
130}): JSX.Element | null {
131 const { t } = useTranslation(undefined); 127 const { t } = useTranslation(undefined);
132 128
133 if (service === undefined || !service.hasError) { 129 if (!service.hasError) {
134 // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering. 130 // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering.
135 return null; 131 return null;
136 } 132 }
diff --git a/packages/renderer/src/components/locationBar/ExtraButtons.tsx b/packages/renderer/src/components/locationBar/ExtraButtons.tsx
index ef90199..4d4c3c4 100644
--- a/packages/renderer/src/components/locationBar/ExtraButtons.tsx
+++ b/packages/renderer/src/components/locationBar/ExtraButtons.tsx
@@ -27,11 +27,7 @@ import { useTranslation } from 'react-i18next';
27 27
28import type Service from '../../stores/Service'; 28import type Service from '../../stores/Service';
29 29
30function ExtraButtons({ 30function ExtraButtons({ service }: { service: Service }): JSX.Element {
31 service,
32}: {
33 service: Service | undefined;
34}): JSX.Element {
35 const { t } = useTranslation(undefined, { 31 const { t } = useTranslation(undefined, {
36 keyPrefix: 'toolbar', 32 keyPrefix: 'toolbar',
37 }); 33 });
@@ -40,8 +36,8 @@ function ExtraButtons({
40 <Box display="flex"> 36 <Box display="flex">
41 <IconButton 37 <IconButton
42 aria-label={t('openInBrowser')} 38 aria-label={t('openInBrowser')}
43 disabled={service?.currentUrl === undefined} 39 disabled={service.currentUrl === undefined}
44 onClick={() => service?.openCurrentURLInExternalBrowser()} 40 onClick={() => service.openCurrentURLInExternalBrowser()}
45 > 41 >
46 <IconOpenInBrowser /> 42 <IconOpenInBrowser />
47 </IconButton> 43 </IconButton>
diff --git a/packages/renderer/src/components/locationBar/LocationBar.tsx b/packages/renderer/src/components/locationBar/LocationBar.tsx
index 54ead8e..c290722 100644
--- a/packages/renderer/src/components/locationBar/LocationBar.tsx
+++ b/packages/renderer/src/components/locationBar/LocationBar.tsx
@@ -22,13 +22,16 @@ import { styled } from '@mui/material/styles';
22import { observer } from 'mobx-react-lite'; 22import { observer } from 'mobx-react-lite';
23import React from 'react'; 23import React from 'react';
24 24
25import type Service from '../../stores/Service';
25import { useStore } from '../StoreProvider'; 26import { useStore } from '../StoreProvider';
26 27
27import ExtraButtons from './ExtraButtons'; 28import ExtraButtons from './ExtraButtons';
28import LocationTextField from './LocationTextField'; 29import LocationTextField from './LocationTextField';
29import NavigationButtons from './NavigationButtons'; 30import NavigationButtons from './NavigationButtons';
30 31
31export const LOCATION_BAR_ID = 'Sophie-LocationBar'; 32export function getLocaltionBarID(service: Service): string {
33 return `Sophie-${service.id}-LocationBar`;
34}
32 35
33const LocationBarRoot = styled('header', { 36const LocationBarRoot = styled('header', {
34 name: 'LocationBar', 37 name: 'LocationBar',
@@ -41,17 +44,22 @@ const LocationBarRoot = styled('header', {
41 borderBottom: `1px solid ${theme.palette.divider}`, 44 borderBottom: `1px solid ${theme.palette.divider}`,
42})); 45}));
43 46
44function LocationBar(): JSX.Element { 47function LocationBar({ service }: { service: Service }): JSX.Element {
45 const { 48 const {
46 shared: { locationBarVisible }, 49 settings: { showLocationBar },
47 settings: { selectedService },
48 } = useStore(); 50 } = useStore();
49 51
52 const { alwaysShowLocationBar } = service;
53 const locationBarVisible = showLocationBar || alwaysShowLocationBar;
54
50 return ( 55 return (
51 <LocationBarRoot id={LOCATION_BAR_ID} hidden={!locationBarVisible}> 56 <LocationBarRoot
52 <NavigationButtons service={selectedService} /> 57 id={getLocaltionBarID(service)}
53 <LocationTextField service={selectedService} /> 58 hidden={!locationBarVisible}
54 <ExtraButtons service={selectedService} /> 59 >
60 <NavigationButtons service={service} />
61 <LocationTextField service={service} />
62 <ExtraButtons service={service} />
55 </LocationBarRoot> 63 </LocationBarRoot>
56 ); 64 );
57} 65}
diff --git a/packages/renderer/src/components/locationBar/LocationTextField.tsx b/packages/renderer/src/components/locationBar/LocationTextField.tsx
index 85cf794..1d6b561 100644
--- a/packages/renderer/src/components/locationBar/LocationTextField.tsx
+++ b/packages/renderer/src/components/locationBar/LocationTextField.tsx
@@ -20,7 +20,6 @@
20 20
21import FilledInput from '@mui/material/FilledInput'; 21import FilledInput from '@mui/material/FilledInput';
22import { styled } from '@mui/material/styles'; 22import { styled } from '@mui/material/styles';
23import { SecurityLabelKind } from '@sophie/shared';
24import { autorun } from 'mobx'; 23import { autorun } from 'mobx';
25import { observer } from 'mobx-react-lite'; 24import { observer } from 'mobx-react-lite';
26import React, { useCallback, useEffect, useState } from 'react'; 25import React, { useCallback, useEffect, useState } from 'react';
@@ -44,19 +43,15 @@ const LocationTextFieldRoot = styled(FilledInput, {
44 }, 43 },
45})); 44}));
46 45
47function LocationTextField({ 46function LocationTextField({ service }: { service: Service }): JSX.Element {
48 service,
49}: {
50 service: Service | undefined;
51}): JSX.Element {
52 const [inputFocused, setInputFocused] = useState(false); 47 const [inputFocused, setInputFocused] = useState(false);
53 const [changed, setChanged] = useState(false); 48 const [changed, setChanged] = useState(false);
54 const [value, setValue] = useState(''); 49 const [value, setValue] = useState('');
55 50
56 const resetValue = useCallback(() => { 51 const resetValue = useCallback(() => {
57 setValue(service?.currentUrl ?? ''); 52 setValue(service.currentUrl ?? '');
58 setChanged(false); 53 setChanged(false);
59 }, [service, setChanged, setValue]); 54 }, [service]);
60 55
61 useEffect( 56 useEffect(
62 () => 57 () =>
@@ -84,8 +79,8 @@ function LocationTextField({
84 overlayVisible: !inputFocused && !changed, 79 overlayVisible: !inputFocused && !changed,
85 overlay: ( 80 overlay: (
86 <UrlOverlay 81 <UrlOverlay
87 url={service?.currentUrl ?? ''} 82 url={service.currentUrl ?? ''}
88 alert={service?.hasSecurityLabelWarning ?? false} 83 alert={service.hasSecurityLabelWarning ?? false}
89 /> 84 />
90 ), 85 ),
91 }} 86 }}
@@ -102,7 +97,7 @@ function LocationTextField({
102 resetValue(); 97 resetValue();
103 break; 98 break;
104 case 'Enter': 99 case 'Enter':
105 service?.go(value); 100 service.go(value);
106 break; 101 break;
107 default: 102 default:
108 // Nothing to do, let the key event through. 103 // Nothing to do, let the key event through.
@@ -117,14 +112,14 @@ function LocationTextField({
117 disableUnderline 112 disableUnderline
118 startAdornment={ 113 startAdornment={
119 <SecurityLabel 114 <SecurityLabel
120 kind={service?.securityLabel ?? SecurityLabelKind.Empty} 115 kind={service.securityLabel}
121 changed={changed} 116 changed={changed}
122 position="start" 117 position="start"
123 /> 118 />
124 } 119 }
125 endAdornment={ 120 endAdornment={
126 changed ? ( 121 changed ? (
127 <GoButton onClick={() => service?.go(value)} position="end" /> 122 <GoButton onClick={() => service.go(value)} position="end" />
128 ) : undefined 123 ) : undefined
129 } 124 }
130 value={value} 125 value={value}
diff --git a/packages/renderer/src/components/locationBar/NavigationButtons.tsx b/packages/renderer/src/components/locationBar/NavigationButtons.tsx
index 9c4ebdb..96e40e7 100644
--- a/packages/renderer/src/components/locationBar/NavigationButtons.tsx
+++ b/packages/renderer/src/components/locationBar/NavigationButtons.tsx
@@ -32,11 +32,7 @@ import { useTranslation } from 'react-i18next';
32 32
33import type Service from '../../stores/Service'; 33import type Service from '../../stores/Service';
34 34
35function NavigationButtons({ 35function NavigationButtons({ service }: { service: Service }): JSX.Element {
36 service,
37}: {
38 service: Service | undefined;
39}): JSX.Element {
40 const { t } = useTranslation(undefined, { 36 const { t } = useTranslation(undefined, {
41 keyPrefix: 'toolbar', 37 keyPrefix: 'toolbar',
42 }); 38 });
@@ -46,36 +42,31 @@ function NavigationButtons({
46 <Box display="flex"> 42 <Box display="flex">
47 <IconButton 43 <IconButton
48 aria-label={t('back')} 44 aria-label={t('back')}
49 disabled={service === undefined || !service.canGoBack} 45 disabled={!service.canGoBack}
50 onClick={() => service?.goBack()} 46 onClick={() => service.goBack()}
51 > 47 >
52 {direction === 'ltr' ? <IconArrowBack /> : <IconArrowForward />} 48 {direction === 'ltr' ? <IconArrowBack /> : <IconArrowForward />}
53 </IconButton> 49 </IconButton>
54 <IconButton 50 <IconButton
55 aria-label={t('forward')} 51 aria-label={t('forward')}
56 disabled={service === undefined || !service.canGoForward} 52 disabled={!service.canGoForward}
57 onClick={() => service?.goForward()} 53 onClick={() => service.goForward()}
58 > 54 >
59 {direction === 'ltr' ? <IconArrowForward /> : <IconArrowBack />} 55 {direction === 'ltr' ? <IconArrowForward /> : <IconArrowBack />}
60 </IconButton> 56 </IconButton>
61 {service?.loading ?? false ? ( 57 {service.loading ? (
62 <IconButton aria-label={t('stop')} onClick={() => service?.stop()}> 58 <IconButton aria-label={t('stop')} onClick={() => service.stop()}>
63 <IconStop /> 59 <IconStop />
64 </IconButton> 60 </IconButton>
65 ) : ( 61 ) : (
66 <IconButton 62 <IconButton
67 aria-label={t('reload')} 63 aria-label={t('reload')}
68 disabled={service === undefined} 64 onClick={(event) => service.reload(event.shiftKey)}
69 onClick={(event) => service?.reload(event.shiftKey)}
70 > 65 >
71 <IconRefresh /> 66 <IconRefresh />
72 </IconButton> 67 </IconButton>
73 )} 68 )}
74 <IconButton 69 <IconButton aria-label={t('home')} onClick={() => service.goHome()}>
75 aria-label={t('home')}
76 disabled={service === undefined}
77 onClick={() => service?.goHome()}
78 >
79 <IconHome /> 70 <IconHome />
80 </IconButton> 71 </IconButton>
81 </Box> 72 </Box>
diff --git a/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx b/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx
index 0ebd359..24cfd0c 100644
--- a/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx
+++ b/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx
@@ -27,6 +27,7 @@ import React from 'react';
27import { useTranslation } from 'react-i18next'; 27import { useTranslation } from 'react-i18next';
28 28
29import type Service from '../../stores/Service'; 29import type Service from '../../stores/Service';
30import { getServicePanelID } from '../ServicePanel';
30import { useStore } from '../StoreProvider'; 31import { useStore } from '../StoreProvider';
31 32
32import ServiceIcon from './ServiceIcon'; 33import ServiceIcon from './ServiceIcon';
@@ -112,6 +113,7 @@ function ServiceSwitcher(): JSX.Element {
112 value={service.id} 113 value={service.id}
113 icon={<ServiceIcon service={service} />} 114 icon={<ServiceIcon service={service} />}
114 aria-label={getServiceTitle(service, t)} 115 aria-label={getServiceTitle(service, t)}
116 aria-controls={getServicePanelID(service)}
115 /> 117 />
116 ))} 118 ))}
117 </ServiceSwitcherRoot> 119 </ServiceSwitcherRoot>
diff --git a/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx b/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx
index 7fc559d..c697170 100644
--- a/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx
+++ b/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx
@@ -28,7 +28,7 @@ import React from 'react';
28import { useTranslation } from 'react-i18next'; 28import { useTranslation } from 'react-i18next';
29 29
30import { useStore } from '../StoreProvider'; 30import { useStore } from '../StoreProvider';
31import { LOCATION_BAR_ID } from '../locationBar/LocationBar'; 31import { getLocaltionBarID } from '../locationBar/LocationBar';
32 32
33function ToggleLocationBarIcon({ 33function ToggleLocationBarIcon({
34 loading, 34 loading,
@@ -54,13 +54,17 @@ function ToggleLocationBarButton(): JSX.Element {
54 const { selectedService } = settings; 54 const { selectedService } = settings;
55 55
56 return ( 56 return (
57 /* eslint-disable react/jsx-props-no-spreading -- Conditionally set the aria-controls prop. */
57 <IconButton 58 <IconButton
58 disabled={!canToggleLocationBar} 59 disabled={!canToggleLocationBar}
59 aria-pressed={locationBarVisible} 60 aria-pressed={locationBarVisible}
60 aria-controls={LOCATION_BAR_ID} 61 {...(selectedService === undefined
62 ? {}
63 : { 'aria-controls': getLocaltionBarID(selectedService) })}
61 aria-label={t('toolbar.toggleLocationBar')} 64 aria-label={t('toolbar.toggleLocationBar')}
62 onClick={() => settings.toggleLocationBar()} 65 onClick={() => settings.toggleLocationBar()}
63 > 66 >
67 {/* eslint-enable react/jsx-props-no-spreading */}
64 <ToggleLocationBarIcon 68 <ToggleLocationBarIcon
65 loading={selectedService?.loading ?? false} 69 loading={selectedService?.loading ?? false}
66 show={locationBarVisible} 70 show={locationBarVisible}
diff --git a/packages/renderer/src/stores/RendererStore.ts b/packages/renderer/src/stores/RendererStore.ts
index 7052162..a3983ca 100644
--- a/packages/renderer/src/stores/RendererStore.ts
+++ b/packages/renderer/src/stores/RendererStore.ts
@@ -18,14 +18,14 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { BrowserViewBounds, SophieRenderer } from '@sophie/shared'; 21import type { SophieRenderer } from '@sophie/shared';
22import { applySnapshot, applyPatch, Instance, types } from 'mobx-state-tree'; 22import { applySnapshot, applyPatch, Instance, types } from 'mobx-state-tree';
23 23
24import { getLogger } from '../utils/log'; 24import { getLogger } from '../utils/log';
25 25
26import GlobalSettings from './GlobalSettings'; 26import type GlobalSettings from './GlobalSettings';
27import RendererEnv, { getEnv } from './RendererEnv'; 27import type RendererEnv from './RendererEnv';
28import Service from './Service'; 28import type Service from './Service';
29import SharedStore from './SharedStore'; 29import SharedStore from './SharedStore';
30 30
31const log = getLogger('RendererStore'); 31const log = getLogger('RendererStore');
@@ -41,14 +41,6 @@ const RendererStore = types
41 get services(): Service[] { 41 get services(): Service[] {
42 return self.shared.services; 42 return self.shared.services;
43 }, 43 },
44 }))
45 .actions((self) => ({
46 setBrowserViewBounds(browserViewBounds: BrowserViewBounds): void {
47 getEnv(self).dispatchMainAction({
48 action: 'set-browser-view-bounds',
49 browserViewBounds,
50 });
51 },
52 })); 44 }));
53 45
54/* 46/*
diff --git a/packages/renderer/src/stores/Service.ts b/packages/renderer/src/stores/Service.ts
index dcaf96e..c8d513f 100644
--- a/packages/renderer/src/stores/Service.ts
+++ b/packages/renderer/src/stores/Service.ts
@@ -18,7 +18,11 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { defineServiceModel, ServiceAction } from '@sophie/shared'; 21import {
22 BrowserViewBounds,
23 defineServiceModel,
24 ServiceAction,
25} from '@sophie/shared';
22import { Instance } from 'mobx-state-tree'; 26import { Instance } from 'mobx-state-tree';
23 27
24import { getEnv } from './RendererEnv'; 28import { getEnv } from './RendererEnv';
@@ -49,6 +53,12 @@ const Service = defineServiceModel(ServiceSettings)
49 } 53 }
50 54
51 return { 55 return {
56 setBrowserViewBounds(browserViewBounds: BrowserViewBounds): void {
57 dispatch({
58 action: 'set-browser-view-bounds',
59 browserViewBounds,
60 });
61 },
52 goBack(): void { 62 goBack(): void {
53 dispatch({ 63 dispatch({
54 action: 'back', 64 action: 'back',
diff --git a/packages/shared/src/schemas/Action.ts b/packages/shared/src/schemas/Action.ts
index ce983fa..7ece27a 100644
--- a/packages/shared/src/schemas/Action.ts
+++ b/packages/shared/src/schemas/Action.ts
@@ -20,7 +20,6 @@
20 20
21import { z } from 'zod'; 21import { z } from 'zod';
22 22
23import { BrowserViewBounds } from './BrowserViewBounds';
24import { ServiceAction } from './ServiceAction'; 23import { ServiceAction } from './ServiceAction';
25import { ThemeSource } from './ThemeSource'; 24import { ThemeSource } from './ThemeSource';
26 25
@@ -31,10 +30,6 @@ export const Action = /* @__PURE__ */ (() =>
31 serviceId: z.string(), 30 serviceId: z.string(),
32 }), 31 }),
33 z.object({ 32 z.object({
34 action: z.literal('set-browser-view-bounds'),
35 browserViewBounds: BrowserViewBounds,
36 }),
37 z.object({
38 action: z.literal('set-theme-source'), 33 action: z.literal('set-theme-source'),
39 themeSource: ThemeSource, 34 themeSource: ThemeSource,
40 }), 35 }),
diff --git a/packages/shared/src/schemas/ServiceAction.ts b/packages/shared/src/schemas/ServiceAction.ts
index c0f07d6..9486aaf 100644
--- a/packages/shared/src/schemas/ServiceAction.ts
+++ b/packages/shared/src/schemas/ServiceAction.ts
@@ -20,9 +20,15 @@
20 20
21import { z } from 'zod'; 21import { z } from 'zod';
22 22
23import { BrowserViewBounds } from './BrowserViewBounds';
24
23export const ServiceAction = /* @__PURE__ */ (() => 25export const ServiceAction = /* @__PURE__ */ (() =>
24 z.union([ 26 z.union([
25 z.object({ 27 z.object({
28 action: z.literal('set-browser-view-bounds'),
29 browserViewBounds: BrowserViewBounds,
30 }),
31 z.object({
26 action: z.literal('back'), 32 action: z.literal('back'),
27 }), 33 }),
28 z.object({ 34 z.object({
diff --git a/packages/shared/src/stores/SharedStoreBase.ts b/packages/shared/src/stores/SharedStoreBase.ts
index e4b3a38..949ef6a 100644
--- a/packages/shared/src/stores/SharedStoreBase.ts
+++ b/packages/shared/src/stores/SharedStoreBase.ts
@@ -65,7 +65,10 @@ export function defineSharedStoreModel<
65 return settings.showLocationBar || self.alwaysShowLocationBar; 65 return settings.showLocationBar || self.alwaysShowLocationBar;
66 }, 66 },
67 get canToggleLocationBar(): boolean { 67 get canToggleLocationBar(): boolean {
68 return !self.alwaysShowLocationBar; 68 return (
69 !self.alwaysShowLocationBar &&
70 self.settings.selectedService !== undefined
71 );
69 }, 72 },
70 })); 73 }));
71} 74}