diff options
Diffstat (limited to 'packages/renderer')
14 files changed, 615 insertions, 40 deletions
diff --git a/packages/renderer/src/components/ThemeProvider.tsx b/packages/renderer/src/components/ThemeProvider.tsx index 3943371..5fc1f33 100644 --- a/packages/renderer/src/components/ThemeProvider.tsx +++ b/packages/renderer/src/components/ThemeProvider.tsx | |||
@@ -37,6 +37,18 @@ export default observer( | |||
37 | palette: { | 37 | palette: { |
38 | mode: shouldUseDarkColors ? 'dark' : 'light', | 38 | mode: shouldUseDarkColors ? 'dark' : 'light', |
39 | }, | 39 | }, |
40 | components: { | ||
41 | MuiBadge: { | ||
42 | styleOverrides: { | ||
43 | standard: { | ||
44 | // Reduce badge with to make the unread message badge with "99+" unread messages | ||
45 | // fit within the sidebar. Applied for all badges for consistency. | ||
46 | paddingLeft: 4, | ||
47 | paddingRight: 4, | ||
48 | }, | ||
49 | }, | ||
50 | }, | ||
51 | }, | ||
40 | }); | 52 | }); |
41 | 53 | ||
42 | return <MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>; | 54 | return <MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>; |
diff --git a/packages/renderer/src/components/locationBar/ButtonAdornment.tsx b/packages/renderer/src/components/locationBar/ButtonAdornment.tsx new file mode 100644 index 0000000..2cf230b --- /dev/null +++ b/packages/renderer/src/components/locationBar/ButtonAdornment.tsx | |||
@@ -0,0 +1,69 @@ | |||
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 InputAdornment from '@mui/material/InputAdornment'; | ||
22 | import { styled } from '@mui/material/styles'; | ||
23 | |||
24 | export const NO_LABEL_BUTTON_CLASS_NAME = 'ButtonAdornment-NoLabel'; | ||
25 | |||
26 | const ButtonAdornment = styled(InputAdornment, { | ||
27 | name: 'ButtonAdornment', | ||
28 | })(({ theme, position }) => { | ||
29 | const { direction } = theme; | ||
30 | const left = direction === 'ltr' ? 'start' : 'end'; | ||
31 | return { | ||
32 | ...(position === left | ||
33 | ? { | ||
34 | marginRight: 2, | ||
35 | marginLeft: -8, | ||
36 | } | ||
37 | : { | ||
38 | marginLeft: 2, | ||
39 | marginRight: -8, | ||
40 | }), | ||
41 | '.MuiButton-root': { | ||
42 | minWidth: 32, | ||
43 | height: 32, | ||
44 | paddingLeft: 6, | ||
45 | paddingRight: 6, | ||
46 | borderRadius: 16, | ||
47 | }, | ||
48 | ...(direction === 'ltr' | ||
49 | ? { | ||
50 | '.MuiButton-startIcon': { | ||
51 | marginLeft: 0, | ||
52 | }, | ||
53 | [`.${NO_LABEL_BUTTON_CLASS_NAME} .MuiButton-startIcon`]: { | ||
54 | marginRight: 0, | ||
55 | }, | ||
56 | } | ||
57 | : { | ||
58 | '.MuiButton-startIcon': { | ||
59 | marginRight: 0, | ||
60 | marginLeft: theme.spacing(1), | ||
61 | }, | ||
62 | [`.${NO_LABEL_BUTTON_CLASS_NAME} .MuiButton-startIcon`]: { | ||
63 | marginLeft: 0, | ||
64 | }, | ||
65 | }), | ||
66 | }; | ||
67 | }); | ||
68 | |||
69 | export default ButtonAdornment; | ||
diff --git a/packages/renderer/src/components/locationBar/GoAdornment.tsx b/packages/renderer/src/components/locationBar/GoAdornment.tsx new file mode 100644 index 0000000..43c8b7b --- /dev/null +++ b/packages/renderer/src/components/locationBar/GoAdornment.tsx | |||
@@ -0,0 +1,38 @@ | |||
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 IconGo from '@mui/icons-material/Send'; | ||
22 | import Button from '@mui/material/Button'; | ||
23 | import React from 'react'; | ||
24 | |||
25 | import ButtonAdornment, { NO_LABEL_BUTTON_CLASS_NAME } from './ButtonAdornment'; | ||
26 | |||
27 | export default function GoAdornment(): JSX.Element { | ||
28 | return ( | ||
29 | <ButtonAdornment position="end"> | ||
30 | <Button | ||
31 | aria-label="Go" | ||
32 | color="inherit" | ||
33 | startIcon={<IconGo />} | ||
34 | className={NO_LABEL_BUTTON_CLASS_NAME} | ||
35 | /> | ||
36 | </ButtonAdornment> | ||
37 | ); | ||
38 | } | ||
diff --git a/packages/renderer/src/components/locationBar/IconAdornment.tsx b/packages/renderer/src/components/locationBar/IconAdornment.tsx new file mode 100644 index 0000000..1a2fa83 --- /dev/null +++ b/packages/renderer/src/components/locationBar/IconAdornment.tsx | |||
@@ -0,0 +1,41 @@ | |||
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 InputAdornment from '@mui/material/InputAdornment'; | ||
22 | import { styled } from '@mui/material/styles'; | ||
23 | |||
24 | const IconAdornment = styled(InputAdornment, { | ||
25 | name: 'IconAdornment', | ||
26 | })(({ theme: { direction }, position }) => { | ||
27 | const left = direction === 'ltr' ? 'start' : 'end'; | ||
28 | return { | ||
29 | ...(position === left | ||
30 | ? { | ||
31 | marginLeft: -2, | ||
32 | marginRight: 8, | ||
33 | } | ||
34 | : { | ||
35 | marginLeft: 8, | ||
36 | marginRight: -2, | ||
37 | }), | ||
38 | }; | ||
39 | }); | ||
40 | |||
41 | export default IconAdornment; | ||
diff --git a/packages/renderer/src/components/locationBar/LocationBar.tsx b/packages/renderer/src/components/locationBar/LocationBar.tsx index e1f470d..8b079f0 100644 --- a/packages/renderer/src/components/locationBar/LocationBar.tsx +++ b/packages/renderer/src/components/locationBar/LocationBar.tsx | |||
@@ -27,6 +27,8 @@ import { useStore } from '../StoreProvider'; | |||
27 | import LocationTextField from './LocationTextField'; | 27 | import LocationTextField from './LocationTextField'; |
28 | import NavigationButtons from './NavigationButtons'; | 28 | import NavigationButtons from './NavigationButtons'; |
29 | 29 | ||
30 | export const LOCATION_BAR_ID = 'Sophie-LocationBar'; | ||
31 | |||
30 | const LocationBarRoot = styled('header', { | 32 | const LocationBarRoot = styled('header', { |
31 | name: 'LocationBar', | 33 | name: 'LocationBar', |
32 | slot: 'Root', | 34 | slot: 'Root', |
@@ -34,6 +36,8 @@ const LocationBarRoot = styled('header', { | |||
34 | display: hidden ? 'none' : 'flex', | 36 | display: hidden ? 'none' : 'flex', |
35 | flexDirection: 'row', | 37 | flexDirection: 'row', |
36 | padding: theme.spacing(1), | 38 | padding: theme.spacing(1), |
39 | // Align the bottom border with the service switcher in the sidebar. | ||
40 | paddingBottom: `calc(${theme.spacing(1)} - 1px)`, | ||
37 | gap: theme.spacing(1), | 41 | gap: theme.spacing(1), |
38 | borderBottom: `1px solid ${theme.palette.divider}`, | 42 | borderBottom: `1px solid ${theme.palette.divider}`, |
39 | })); | 43 | })); |
@@ -44,7 +48,7 @@ function LocationBar(): JSX.Element { | |||
44 | } = useStore(); | 48 | } = useStore(); |
45 | 49 | ||
46 | return ( | 50 | return ( |
47 | <LocationBarRoot id="locationBar" hidden={!showLocationBar}> | 51 | <LocationBarRoot id={LOCATION_BAR_ID} hidden={!showLocationBar}> |
48 | <NavigationButtons service={selectedService} /> | 52 | <NavigationButtons service={selectedService} /> |
49 | <LocationTextField service={selectedService} /> | 53 | <LocationTextField service={selectedService} /> |
50 | </LocationBarRoot> | 54 | </LocationBarRoot> |
diff --git a/packages/renderer/src/components/locationBar/LocationOverlayInput.tsx b/packages/renderer/src/components/locationBar/LocationOverlayInput.tsx new file mode 100644 index 0000000..55c1fb8 --- /dev/null +++ b/packages/renderer/src/components/locationBar/LocationOverlayInput.tsx | |||
@@ -0,0 +1,110 @@ | |||
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 { styled } from '@mui/material/styles'; | ||
22 | import React, { forwardRef, ForwardedRef } from 'react'; | ||
23 | |||
24 | const inputClassName = 'LocationOverlayInput-Input'; | ||
25 | |||
26 | const overlayClassName = 'LocationOverlayInput-Overlay'; | ||
27 | |||
28 | const clipClassName = 'LocationOverlayInput-Clip'; | ||
29 | |||
30 | const LocationOverlayInputRoot = styled('div', { | ||
31 | name: 'LocationOverlayInput', | ||
32 | slot: 'Root', | ||
33 | shouldForwardProp: (prop) => prop !== 'overlayVisible', | ||
34 | })<{ overlayVisible: boolean }>(({ theme, overlayVisible }) => { | ||
35 | const itemStyle = { | ||
36 | paddingLeft: 0, | ||
37 | paddingRight: 0, | ||
38 | }; | ||
39 | return { | ||
40 | display: 'flex', | ||
41 | position: 'relative', | ||
42 | flex: 1, | ||
43 | [`.${inputClassName}`]: { | ||
44 | ...itemStyle, | ||
45 | color: overlayVisible ? 'transparent' : 'inherit', | ||
46 | }, | ||
47 | [`.${overlayClassName}`]: { | ||
48 | ...itemStyle, | ||
49 | display: 'flex', | ||
50 | position: 'absolute', | ||
51 | left: 0, | ||
52 | right: 0, | ||
53 | top: 0, | ||
54 | bottom: 0, | ||
55 | // Text rendering with selected transparent text works better on the bottom in light mode. | ||
56 | zIndex: theme.palette.mode === 'dark' ? 999 : -999, | ||
57 | pointerEvents: 'none', | ||
58 | width: 'auto', | ||
59 | }, | ||
60 | [`.${clipClassName}`]: { | ||
61 | flex: 1, | ||
62 | overflow: 'hidden', | ||
63 | whiteSpace: 'nowrap', | ||
64 | textOverflow: 'clip', | ||
65 | }, | ||
66 | }; | ||
67 | }); | ||
68 | |||
69 | export interface LocationOverlayInputProps | ||
70 | extends React.HTMLProps<HTMLInputElement> { | ||
71 | className?: string | undefined; | ||
72 | |||
73 | overlayVisible?: boolean; | ||
74 | |||
75 | overlay?: JSX.Element | undefined; | ||
76 | } | ||
77 | |||
78 | const LocationOverlayInput = forwardRef( | ||
79 | ( | ||
80 | { overlayVisible, overlay, className, ...props }: LocationOverlayInputProps, | ||
81 | ref: ForwardedRef<HTMLInputElement>, | ||
82 | ) => ( | ||
83 | <LocationOverlayInputRoot overlayVisible={overlayVisible ?? false}> | ||
84 | {/* eslint-disable react/jsx-props-no-spreading -- | ||
85 | Deliberately passing props through to the actual input element. | ||
86 | */} | ||
87 | <input | ||
88 | ref={ref} | ||
89 | className={`${className ?? ''} ${inputClassName}`} | ||
90 | {...props} | ||
91 | /> | ||
92 | {/* eslint-enable react/jsx-props-no-spreading */} | ||
93 | {overlayVisible && ( | ||
94 | <div aria-hidden className={`${className ?? ''} ${overlayClassName}`}> | ||
95 | <div className={clipClassName}>{overlay}</div> | ||
96 | </div> | ||
97 | )} | ||
98 | </LocationOverlayInputRoot> | ||
99 | ), | ||
100 | ); | ||
101 | |||
102 | LocationOverlayInput.displayName = 'LocationOverlayInput'; | ||
103 | |||
104 | LocationOverlayInput.defaultProps = { | ||
105 | className: undefined, | ||
106 | overlayVisible: false, | ||
107 | overlay: undefined, | ||
108 | }; | ||
109 | |||
110 | export default LocationOverlayInput; | ||
diff --git a/packages/renderer/src/components/locationBar/LocationTextField.tsx b/packages/renderer/src/components/locationBar/LocationTextField.tsx index a618bf6..f436bf0 100644 --- a/packages/renderer/src/components/locationBar/LocationTextField.tsx +++ b/packages/renderer/src/components/locationBar/LocationTextField.tsx | |||
@@ -18,40 +18,95 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import IconGlobe from '@mui/icons-material/Public'; | ||
22 | import FilledInput from '@mui/material/FilledInput'; | 21 | import FilledInput from '@mui/material/FilledInput'; |
23 | import InputAdornment from '@mui/material/InputAdornment'; | 22 | import { styled } from '@mui/material/styles'; |
24 | import { Service } from '@sophie/shared'; | 23 | import { Service } from '@sophie/shared'; |
24 | import { autorun } from 'mobx'; | ||
25 | import { observer } from 'mobx-react-lite'; | 25 | import { observer } from 'mobx-react-lite'; |
26 | import React from 'react'; | 26 | import React, { useCallback, useEffect, useState } from 'react'; |
27 | |||
28 | import GoAdornment from './GoAdornment'; | ||
29 | import LocationOverlayInput from './LocationOverlayInput'; | ||
30 | import UrlAdornment from './UrlAdornment'; | ||
31 | import UrlOverlay from './UrlOverlay'; | ||
32 | import splitUrl from './splitUrl'; | ||
33 | |||
34 | const LocationTextFieldRoot = styled(FilledInput, { | ||
35 | name: 'LocationTextField', | ||
36 | slot: 'Root', | ||
37 | })(({ theme }) => ({ | ||
38 | paddingLeft: 12, | ||
39 | paddingRight: 12, | ||
40 | borderRadius: 20, | ||
41 | '&.Mui-focused': { | ||
42 | outline: `2px solid ${theme.palette.primary.main}`, | ||
43 | }, | ||
44 | })); | ||
27 | 45 | ||
28 | function LocationTextField({ | 46 | function LocationTextField({ |
29 | service, | 47 | service, |
30 | }: { | 48 | }: { |
31 | service: Service | undefined; | 49 | service: Service | undefined; |
32 | }): JSX.Element { | 50 | }): JSX.Element { |
51 | const [inputFocused, setInputFocused] = useState(false); | ||
52 | const [changed, setChanged] = useState(false); | ||
53 | const [value, setValue] = useState(''); | ||
54 | |||
55 | const resetValue = useCallback(() => { | ||
56 | setValue(service?.currentUrl ?? ''); | ||
57 | setChanged(false); | ||
58 | }, [service, setChanged, setValue]); | ||
59 | |||
60 | useEffect( | ||
61 | () => | ||
62 | autorun(() => { | ||
63 | resetValue(); | ||
64 | }), | ||
65 | [resetValue], | ||
66 | ); | ||
67 | |||
68 | const inputRefCallback = useCallback( | ||
69 | (input: HTMLInputElement) => { | ||
70 | setInputFocused( | ||
71 | document.activeElement !== null && document.activeElement === input, | ||
72 | ); | ||
73 | }, | ||
74 | [setInputFocused], | ||
75 | ); | ||
76 | |||
77 | const splitResult = splitUrl(service?.currentUrl); | ||
78 | |||
33 | return ( | 79 | return ( |
34 | <FilledInput | 80 | <LocationTextFieldRoot |
35 | aria-label="Location" | 81 | inputComponent={LocationOverlayInput} |
36 | inputProps={{ | 82 | inputProps={{ |
83 | 'aria-label': 'Location', | ||
37 | spellCheck: false, | 84 | spellCheck: false, |
85 | overlayVisible: !inputFocused && !changed, | ||
86 | overlay: <UrlOverlay splitResult={splitResult} />, | ||
87 | }} | ||
88 | inputRef={inputRefCallback} | ||
89 | onFocus={() => setInputFocused(true)} | ||
90 | onBlur={() => setInputFocused(false)} | ||
91 | onChange={({ target: { value: newValue } }) => { | ||
92 | setValue(newValue); | ||
93 | setChanged(true); | ||
94 | }} | ||
95 | onKeyUp={(event) => { | ||
96 | if (event.key === 'Escape') { | ||
97 | resetValue(); | ||
98 | event.preventDefault(); | ||
99 | } | ||
38 | }} | 100 | }} |
39 | size="small" | 101 | size="small" |
40 | fullWidth | 102 | fullWidth |
41 | hiddenLabel | 103 | hiddenLabel |
42 | disableUnderline | 104 | disableUnderline |
43 | sx={(theme) => ({ | ||
44 | borderRadius: 1, | ||
45 | '&.Mui-focused': { | ||
46 | outline: `2px solid ${theme.palette.primary.main}`, | ||
47 | }, | ||
48 | })} | ||
49 | startAdornment={ | 105 | startAdornment={ |
50 | <InputAdornment position="start"> | 106 | <UrlAdornment changed={changed} splitResult={splitResult} /> |
51 | <IconGlobe fontSize="small" /> | ||
52 | </InputAdornment> | ||
53 | } | 107 | } |
54 | value={service?.currentUrl ?? ''} | 108 | endAdornment={changed ? <GoAdornment /> : undefined} |
109 | value={value} | ||
55 | /> | 110 | /> |
56 | ); | 111 | ); |
57 | } | 112 | } |
diff --git a/packages/renderer/src/components/locationBar/NavigationButtons.tsx b/packages/renderer/src/components/locationBar/NavigationButtons.tsx index 82ed657..77b02b6 100644 --- a/packages/renderer/src/components/locationBar/NavigationButtons.tsx +++ b/packages/renderer/src/components/locationBar/NavigationButtons.tsx | |||
@@ -23,6 +23,7 @@ import IconArrowForward from '@mui/icons-material/ArrowForward'; | |||
23 | import IconStop from '@mui/icons-material/Close'; | 23 | import IconStop from '@mui/icons-material/Close'; |
24 | import IconHome from '@mui/icons-material/HomeOutlined'; | 24 | import IconHome from '@mui/icons-material/HomeOutlined'; |
25 | import IconRefresh from '@mui/icons-material/Refresh'; | 25 | import IconRefresh from '@mui/icons-material/Refresh'; |
26 | import { useTheme } from '@mui/material'; | ||
26 | import Box from '@mui/material/Box'; | 27 | import Box from '@mui/material/Box'; |
27 | import IconButton from '@mui/material/IconButton'; | 28 | import IconButton from '@mui/material/IconButton'; |
28 | import { Service } from '@sophie/shared'; | 29 | import { Service } from '@sophie/shared'; |
@@ -34,6 +35,8 @@ function NavigationButtons({ | |||
34 | }: { | 35 | }: { |
35 | service: Service | undefined; | 36 | service: Service | undefined; |
36 | }): JSX.Element { | 37 | }): JSX.Element { |
38 | const { direction } = useTheme(); | ||
39 | |||
37 | return ( | 40 | return ( |
38 | <Box | 41 | <Box |
39 | sx={{ | 42 | sx={{ |
@@ -42,16 +45,16 @@ function NavigationButtons({ | |||
42 | }} | 45 | }} |
43 | > | 46 | > |
44 | <IconButton | 47 | <IconButton |
45 | aria-label="Go back" | 48 | aria-label="Back" |
46 | disabled={service === undefined || !service.canGoBack} | 49 | disabled={service === undefined || !service.canGoBack} |
47 | > | 50 | > |
48 | <IconArrowBack /> | 51 | {direction === 'ltr' ? <IconArrowBack /> : <IconArrowForward />} |
49 | </IconButton> | 52 | </IconButton> |
50 | <IconButton | 53 | <IconButton |
51 | aria-label="Go forward" | 54 | aria-label="Forward" |
52 | disabled={service === undefined || !service.canGoForward} | 55 | disabled={service === undefined || !service.canGoForward} |
53 | > | 56 | > |
54 | <IconArrowForward /> | 57 | {direction === 'ltr' ? <IconArrowForward /> : <IconArrowBack />} |
55 | </IconButton> | 58 | </IconButton> |
56 | {service?.state === 'loading' ? ( | 59 | {service?.state === 'loading' ? ( |
57 | <IconButton aria-label="Stop"> | 60 | <IconButton aria-label="Stop"> |
@@ -62,10 +65,7 @@ function NavigationButtons({ | |||
62 | <IconRefresh /> | 65 | <IconRefresh /> |
63 | </IconButton> | 66 | </IconButton> |
64 | )} | 67 | )} |
65 | <IconButton | 68 | <IconButton aria-label="Home" disabled={service === undefined}> |
66 | aria-label="Go to service homepage" | ||
67 | disabled={service === undefined} | ||
68 | > | ||
69 | <IconHome /> | 69 | <IconHome /> |
70 | </IconButton> | 70 | </IconButton> |
71 | </Box> | 71 | </Box> |
diff --git a/packages/renderer/src/components/locationBar/UrlAdornment.tsx b/packages/renderer/src/components/locationBar/UrlAdornment.tsx new file mode 100644 index 0000000..6ede378 --- /dev/null +++ b/packages/renderer/src/components/locationBar/UrlAdornment.tsx | |||
@@ -0,0 +1,91 @@ | |||
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 IconHttps from '@mui/icons-material/HttpsOutlined'; | ||
22 | import IconGlobe from '@mui/icons-material/Public'; | ||
23 | import IconWarning from '@mui/icons-material/Warning'; | ||
24 | import { styled } from '@mui/material'; | ||
25 | import Button from '@mui/material/Button'; | ||
26 | import React from 'react'; | ||
27 | |||
28 | import ButtonAdornment, { NO_LABEL_BUTTON_CLASS_NAME } from './ButtonAdornment'; | ||
29 | import IconAdornment from './IconAdornment'; | ||
30 | import type { SplitResult } from './splitUrl'; | ||
31 | |||
32 | const FastColorChangingButton = styled(Button)(({ theme }) => ({ | ||
33 | transition: theme.transitions.create( | ||
34 | ['background-color', 'box-shadow', 'border-color'], | ||
35 | { | ||
36 | duration: theme.transitions.duration.short, | ||
37 | easing: theme.transitions.easing.easeInOut, | ||
38 | }, | ||
39 | ), | ||
40 | })); | ||
41 | |||
42 | export default function UrlAdornment({ | ||
43 | splitResult, | ||
44 | changed, | ||
45 | }: { | ||
46 | splitResult: SplitResult; | ||
47 | changed: boolean; | ||
48 | }): JSX.Element { | ||
49 | const { type } = splitResult; | ||
50 | if (changed || type === 'empty') { | ||
51 | return ( | ||
52 | <IconAdornment position="start"> | ||
53 | <IconGlobe fontSize="small" /> | ||
54 | </IconAdornment> | ||
55 | ); | ||
56 | } | ||
57 | switch (type) { | ||
58 | case 'valid': { | ||
59 | const { secure } = splitResult; | ||
60 | return secure ? ( | ||
61 | <ButtonAdornment position="start"> | ||
62 | <FastColorChangingButton | ||
63 | aria-label="Show certificate" | ||
64 | color="inherit" | ||
65 | className={NO_LABEL_BUTTON_CLASS_NAME} | ||
66 | startIcon={<IconHttps />} | ||
67 | /> | ||
68 | </ButtonAdornment> | ||
69 | ) : ( | ||
70 | <ButtonAdornment position="start"> | ||
71 | <FastColorChangingButton color="error" startIcon={<IconWarning />}> | ||
72 | Not secure | ||
73 | </FastColorChangingButton> | ||
74 | </ButtonAdornment> | ||
75 | ); | ||
76 | } | ||
77 | case 'invalid': | ||
78 | return ( | ||
79 | <ButtonAdornment position="start"> | ||
80 | <FastColorChangingButton color="error" startIcon={<IconWarning />}> | ||
81 | Unknown site | ||
82 | </FastColorChangingButton> | ||
83 | </ButtonAdornment> | ||
84 | ); | ||
85 | default: | ||
86 | /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- | ||
87 | Error handling for impossible case. | ||
88 | */ | ||
89 | throw new Error(`Unexpected SplitResult: ${type}`); | ||
90 | } | ||
91 | } | ||
diff --git a/packages/renderer/src/components/locationBar/UrlOverlay.tsx b/packages/renderer/src/components/locationBar/UrlOverlay.tsx new file mode 100644 index 0000000..f7a3c4c --- /dev/null +++ b/packages/renderer/src/components/locationBar/UrlOverlay.tsx | |||
@@ -0,0 +1,70 @@ | |||
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 { styled } from '@mui/material/styles'; | ||
22 | import React from 'react'; | ||
23 | |||
24 | import type { SplitResult } from './splitUrl'; | ||
25 | |||
26 | const PrimaryFragment = styled('span', { | ||
27 | name: 'LocationOverlayInput', | ||
28 | slot: 'PrimaryFragment', | ||
29 | shouldForwardProp: (prop) => prop !== 'alert', | ||
30 | })<{ alert: boolean }>(({ theme, alert }) => ({ | ||
31 | color: alert ? theme.palette.error.main : theme.palette.text.primary, | ||
32 | })); | ||
33 | |||
34 | const SecondaryFragment = styled('span', { | ||
35 | name: 'LocationOverlayInput', | ||
36 | slot: 'SecondaryFragment', | ||
37 | })(({ theme }) => ({ | ||
38 | color: theme.palette.text.secondary, | ||
39 | })); | ||
40 | |||
41 | export default function UrlOverlay({ | ||
42 | splitResult, | ||
43 | }: { | ||
44 | splitResult: SplitResult; | ||
45 | }): JSX.Element { | ||
46 | const { type } = splitResult; | ||
47 | switch (type) { | ||
48 | case 'valid': { | ||
49 | const { secure, prefix, host, suffix } = splitResult; | ||
50 | return ( | ||
51 | <> | ||
52 | <SecondaryFragment>{prefix}</SecondaryFragment> | ||
53 | <PrimaryFragment alert={!secure}>{host}</PrimaryFragment> | ||
54 | <SecondaryFragment>{suffix}</SecondaryFragment> | ||
55 | </> | ||
56 | ); | ||
57 | } | ||
58 | case 'invalid': { | ||
59 | const { value } = splitResult; | ||
60 | return <PrimaryFragment alert>{value}</PrimaryFragment>; | ||
61 | } | ||
62 | case 'empty': | ||
63 | return <PrimaryFragment alert={false} />; | ||
64 | default: | ||
65 | /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- | ||
66 | Error handling for impossible case. | ||
67 | */ | ||
68 | throw new Error(`Unexpected SplitResult: ${type}`); | ||
69 | } | ||
70 | } | ||
diff --git a/packages/renderer/src/components/locationBar/splitUrl.ts b/packages/renderer/src/components/locationBar/splitUrl.ts new file mode 100644 index 0000000..6e95472 --- /dev/null +++ b/packages/renderer/src/components/locationBar/splitUrl.ts | |||
@@ -0,0 +1,62 @@ | |||
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 | export type SplitResult = | ||
22 | | { | ||
23 | type: 'valid'; | ||
24 | secure: boolean; | ||
25 | prefix: string; | ||
26 | host: string; | ||
27 | suffix: string; | ||
28 | } | ||
29 | | { | ||
30 | type: 'invalid'; | ||
31 | value: string; | ||
32 | } | ||
33 | | { | ||
34 | type: 'empty'; | ||
35 | }; | ||
36 | |||
37 | export default function splitUrl(urlString: string | undefined): SplitResult { | ||
38 | if (urlString === undefined || urlString === '') { | ||
39 | return { type: 'empty' }; | ||
40 | } | ||
41 | let url: URL; | ||
42 | try { | ||
43 | url = new URL(urlString); | ||
44 | } catch { | ||
45 | return { type: 'invalid', value: urlString }; | ||
46 | } | ||
47 | const { protocol, host, username, password, pathname, search, hash } = url; | ||
48 | if (protocol === 'http:' || protocol === 'https:') { | ||
49 | return { | ||
50 | type: 'valid', | ||
51 | secure: protocol === 'https:', | ||
52 | prefix: `${protocol}//${ | ||
53 | username === '' | ||
54 | ? '' | ||
55 | : `${username}${password === '' ? '' : `:${password}`}@` | ||
56 | }`, | ||
57 | host, | ||
58 | suffix: `${pathname}${search}${hash}`, | ||
59 | }; | ||
60 | } | ||
61 | return { type: 'invalid', value: urlString }; | ||
62 | } | ||
diff --git a/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx b/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx index 3b47990..2241476 100644 --- a/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx +++ b/packages/renderer/src/components/sidebar/ServiceSwitcher.tsx | |||
@@ -32,6 +32,7 @@ const ServiceSwitcherRoot = styled(Tabs, { | |||
32 | name: 'ServiceSwitcher', | 32 | name: 'ServiceSwitcher', |
33 | slot: 'Root', | 33 | slot: 'Root', |
34 | })(({ theme }) => ({ | 34 | })(({ theme }) => ({ |
35 | // Move the indicator to the outer side of the window. | ||
35 | '.MuiTabs-indicator': { | 36 | '.MuiTabs-indicator': { |
36 | ...(theme.direction === 'ltr' | 37 | ...(theme.direction === 'ltr' |
37 | ? { | 38 | ? { |
@@ -50,13 +51,15 @@ const ServiceSwitcherTab = styled(Tab, { | |||
50 | slot: 'Tab', | 51 | slot: 'Tab', |
51 | })(({ theme }) => ({ | 52 | })(({ theme }) => ({ |
52 | minWidth: 0, | 53 | minWidth: 0, |
54 | paddingInlineStart: theme.spacing(2), | ||
55 | paddingInlineEnd: `calc(${theme.spacing(2)} - 1px)`, | ||
53 | transition: theme.transitions.create('background-color', { | 56 | transition: theme.transitions.create('background-color', { |
54 | duration: theme.transitions.duration.shortest, | 57 | duration: theme.transitions.duration.shortest, |
55 | }), | 58 | }), |
56 | '&.Mui-selected': { | 59 | '&.Mui-selected': { |
57 | backgroundColor: | 60 | backgroundColor: |
58 | theme.palette.mode === 'dark' | 61 | theme.palette.mode === 'dark' |
59 | ? alpha(theme.palette.text.primary, 0.12) | 62 | ? alpha(theme.palette.text.primary, 0.13) |
60 | : alpha(theme.palette.primary.light, 0.24), | 63 | : alpha(theme.palette.primary.light, 0.24), |
61 | }, | 64 | }, |
62 | })); | 65 | })); |
diff --git a/packages/renderer/src/components/sidebar/Sidebar.tsx b/packages/renderer/src/components/sidebar/Sidebar.tsx index 80826ca..4f27d68 100644 --- a/packages/renderer/src/components/sidebar/Sidebar.tsx +++ b/packages/renderer/src/components/sidebar/Sidebar.tsx | |||
@@ -19,7 +19,6 @@ | |||
19 | */ | 19 | */ |
20 | 20 | ||
21 | import Box from '@mui/material/Box'; | 21 | import Box from '@mui/material/Box'; |
22 | import { alpha } from '@mui/material/styles'; | ||
23 | import React from 'react'; | 22 | import React from 'react'; |
24 | 23 | ||
25 | import ServiceSwitcher from './ServiceSwitcher'; | 24 | import ServiceSwitcher from './ServiceSwitcher'; |
@@ -36,21 +35,29 @@ export default function Sidebar(): JSX.Element { | |||
36 | flexDirection: 'column', | 35 | flexDirection: 'column', |
37 | alignItems: 'center', | 36 | alignItems: 'center', |
38 | paddingY: 1, | 37 | paddingY: 1, |
38 | paddingInlineStart: '1px', | ||
39 | gap: 1, | 39 | gap: 1, |
40 | background: alpha(theme.palette.text.primary, 0.09), | 40 | backgroundColor: |
41 | theme.palette.mode === 'dark' | ||
42 | ? 'rgba(255, 255, 255, 0.09)' | ||
43 | : 'rgba(0, 0, 0, 0.06)', | ||
41 | backgroundClip: 'padding-box', | 44 | backgroundClip: 'padding-box', |
42 | borderInlineEnd: `1px solid ${theme.palette.divider}`, | 45 | borderInlineEnd: `1px solid ${theme.palette.divider}`, |
43 | minWidth: 69, | 46 | minWidth: `calc(${theme.spacing(4)} + 36px)`, |
44 | })} | 47 | })} |
45 | > | 48 | > |
46 | <ToggleLocationBarButton /> | 49 | <ToggleLocationBarButton /> |
47 | <ServiceSwitcher /> | ||
48 | <Box | 50 | <Box |
49 | sx={{ | 51 | sx={{ |
50 | flex: 1, | 52 | flex: 1, |
51 | WebkitAppRegion: 'drag', | 53 | display: 'flex', |
54 | flexDirection: 'column', | ||
55 | justifyContent: 'flex-start', | ||
56 | marginInlineStart: '-1px', | ||
52 | }} | 57 | }} |
53 | /> | 58 | > |
59 | <ServiceSwitcher /> | ||
60 | </Box> | ||
54 | <ToggleDarkModeButton /> | 61 | <ToggleDarkModeButton /> |
55 | </Box> | 62 | </Box> |
56 | ); | 63 | ); |
diff --git a/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx b/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx index 7e20598..60033b0 100644 --- a/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx +++ b/packages/renderer/src/components/sidebar/ToggleLocationBarButton.tsx | |||
@@ -20,12 +20,29 @@ | |||
20 | 20 | ||
21 | import IconChevronLeft from '@mui/icons-material/KeyboardDoubleArrowLeft'; | 21 | import IconChevronLeft from '@mui/icons-material/KeyboardDoubleArrowLeft'; |
22 | import IconChevronRight from '@mui/icons-material/KeyboardDoubleArrowRight'; | 22 | import IconChevronRight from '@mui/icons-material/KeyboardDoubleArrowRight'; |
23 | import { useTheme } from '@mui/material'; | ||
23 | import CircularProgress from '@mui/material/CircularProgress'; | 24 | import CircularProgress from '@mui/material/CircularProgress'; |
24 | import IconButton from '@mui/material/IconButton'; | 25 | import IconButton from '@mui/material/IconButton'; |
25 | import { observer } from 'mobx-react-lite'; | 26 | import { observer } from 'mobx-react-lite'; |
26 | import React from 'react'; | 27 | import React from 'react'; |
27 | 28 | ||
28 | import { useStore } from '../StoreProvider'; | 29 | import { useStore } from '../StoreProvider'; |
30 | import { LOCATION_BAR_ID } from '../locationBar/LocationBar'; | ||
31 | |||
32 | function ToggleLocationBarIcon({ | ||
33 | loading, | ||
34 | show, | ||
35 | }: { | ||
36 | loading: boolean; | ||
37 | show: boolean; | ||
38 | }): JSX.Element { | ||
39 | const { direction } = useTheme(); | ||
40 | if (loading) { | ||
41 | return <CircularProgress color="inherit" size="1.5rem" />; | ||
42 | } | ||
43 | const left = direction === 'ltr' ? show : !show; | ||
44 | return left ? <IconChevronLeft /> : <IconChevronRight />; | ||
45 | } | ||
29 | 46 | ||
30 | function ToggleLocationBarButton(): JSX.Element { | 47 | function ToggleLocationBarButton(): JSX.Element { |
31 | const store = useStore(); | 48 | const store = useStore(); |
@@ -33,21 +50,17 @@ function ToggleLocationBarButton(): JSX.Element { | |||
33 | settings: { selectedService, showLocationBar }, | 50 | settings: { selectedService, showLocationBar }, |
34 | } = store; | 51 | } = store; |
35 | 52 | ||
36 | let icon: JSX.Element; | ||
37 | if (selectedService?.state === 'loading') { | ||
38 | icon = <CircularProgress color="inherit" size="1.5rem" />; | ||
39 | } else { | ||
40 | icon = showLocationBar ? <IconChevronLeft /> : <IconChevronRight />; | ||
41 | } | ||
42 | |||
43 | return ( | 53 | return ( |
44 | <IconButton | 54 | <IconButton |
45 | aria-pressed={showLocationBar} | 55 | aria-pressed={showLocationBar} |
46 | aria-controls="locationBar" | 56 | aria-controls={LOCATION_BAR_ID} |
47 | aria-label="Show location bar" | 57 | aria-label="Show location bar" |
48 | onClick={() => store.toggleLocationBar()} | 58 | onClick={() => store.toggleLocationBar()} |
49 | > | 59 | > |
50 | {icon} | 60 | <ToggleLocationBarIcon |
61 | loading={selectedService?.state === 'loading'} | ||
62 | show={showLocationBar} | ||
63 | /> | ||
51 | </IconButton> | 64 | </IconButton> |
52 | ); | 65 | ); |
53 | } | 66 | } |