aboutsummaryrefslogtreecommitdiffstats
path: root/packages/renderer
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-04-24 17:01:25 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-05-16 00:55:02 +0200
commit0b632445a933644c7eb10015eb04b7c21d562900 (patch)
tree5bd1fe200cc747086220dbcba90097bfe2cc4cdc /packages/renderer
parentchore(deps): upgrade dependencies (diff)
downloadsophie-0b632445a933644c7eb10015eb04b7c21d562900.tar.gz
sophie-0b632445a933644c7eb10015eb04b7c21d562900.tar.zst
sophie-0b632445a933644c7eb10015eb04b7c21d562900.zip
refactor: reduce service switcher tearing
We render the location bar and notification banners separately for each service and keep track of the BrowserView size separately for each service to reduce the tearing that appears when people switch services. The tearing cannot be eliminated completely, because it comes from the separation between the main and renderer processes. But we can at least try and reduce the IPC round-tripping and layout calculations required to accurately position the services. This approach has an overhead compared to the single BrowserViewPlaceholder approach, because the renderer process has to layout the location bar and notification for all services, not only the selected one. The number of IPC messages during windows resize is also increased. To compensate, we increase the throttle interval for resize IPC messages and let electron itself resize the BrowserView between IPC updates. (We must still keep pumping IPC messages during window resize, because, e.g., changes in notification banner size due to re-layouting will still affect the required BrowserView size). If further reduction of IPC traffic is needed, we could implement batching for resize IPC messages and more intelligent throttling via a token bucker mechanism. Signed-off-by: Kristóf Marussy <kristof@marussy.com>
Diffstat (limited to 'packages/renderer')
-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
14 files changed, 152 insertions, 102 deletions
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',