Skip to content

Commit ad9b01e

Browse files
authored
feat: implement theme switching with system preference support (#902)
* feat: implement theme switching with system preference support * feat: improve code readability by formatting imports in Layout component * feat: refactor theme handling by introducing ThemeKey type and centralizing theme icons * feat: update default theme to 'system' for improved user experience
1 parent bf16f1c commit ad9b01e

File tree

3 files changed

+52
-24
lines changed

3 files changed

+52
-24
lines changed

ui/src/layout/Header.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,23 @@ import AccountCircle from '@mui/icons-material/AccountCircle';
99
import Chat from '@mui/icons-material/Chat';
1010
import DevicesOther from '@mui/icons-material/DevicesOther';
1111
import ExitToApp from '@mui/icons-material/ExitToApp';
12-
import Highlight from '@mui/icons-material/Highlight';
12+
import Brightness4 from '@mui/icons-material/Brightness4';
13+
import Brightness7 from '@mui/icons-material/Brightness7';
14+
import BrightnessAuto from '@mui/icons-material/BrightnessAuto';
1315
import GitHubIcon from '@mui/icons-material/GitHub';
1416
import MenuIcon from '@mui/icons-material/Menu';
1517
import Apps from '@mui/icons-material/Apps';
1618
import SupervisorAccount from '@mui/icons-material/SupervisorAccount';
1719
import React, {CSSProperties} from 'react';
1820
import {Link} from 'react-router-dom';
1921
import {useMediaQuery} from '@mui/material';
22+
import {ThemeKey} from './theme';
23+
24+
const themeIcons: Record<ThemeKey, React.ReactElement> = {
25+
dark: <Brightness4 />,
26+
light: <Brightness7 />,
27+
system: <BrightnessAuto />,
28+
};
2029

2130
const useStyles = makeStyles()((theme: Theme) => ({
2231
appBar: {
@@ -67,6 +76,7 @@ interface IProps {
6776
name: string;
6877
admin: boolean;
6978
version: string;
79+
themeMode: ThemeKey;
7080
toggleTheme: VoidFunction;
7181
showSettings: VoidFunction;
7282
logout: VoidFunction;
@@ -84,9 +94,11 @@ const Header = ({
8494
style,
8595
setNavOpen,
8696
showSettings,
97+
themeMode,
8798
}: IProps) => {
8899
const {classes} = useStyles();
89-
100+
const themeLabel = `Toggle theme (current: ${themeMode})`;
101+
const themeIcon = themeIcons[themeMode];
90102
return (
91103
<AppBar
92104
sx={{position: {xs: 'sticky', sm: 'fixed'}}}
@@ -117,8 +129,13 @@ const Header = ({
117129
/>
118130
)}
119131
<div>
120-
<IconButton onClick={toggleTheme} color="inherit" size="large">
121-
<Highlight />
132+
<IconButton
133+
onClick={toggleTheme}
134+
color="inherit"
135+
size="large"
136+
title={themeLabel}
137+
aria-label={themeLabel}>
138+
{themeIcon}
122139
</IconButton>
123140

124141
<a

ui/src/layout/Layout.tsx

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import {createTheme, ThemeProvider, StyledEngineProvider, Theme} from '@mui/material';
1+
import {
2+
createTheme,
3+
ThemeProvider,
4+
StyledEngineProvider,
5+
Theme,
6+
useMediaQuery,
7+
} from '@mui/material';
28
import {makeStyles} from 'tss-react/mui';
39
import CssBaseline from '@mui/material/CssBaseline';
410
import * as React from 'react';
@@ -19,6 +25,7 @@ import {ConnectionErrorBanner} from '../common/ConnectionErrorBanner';
1925
import {useStores} from '../stores';
2026
import {SnackbarProvider} from 'notistack';
2127
import LoadingSpinner from '../common/LoadingSpinner';
28+
import {isThemeKey, ThemeKey} from './theme';
2229

2330
const useStyles = makeStyles()((theme: Theme) => ({
2431
content: {
@@ -34,22 +41,6 @@ const useStyles = makeStyles()((theme: Theme) => ({
3441
}));
3542

3643
const localStorageThemeKey = 'gotify-theme';
37-
type ThemeKey = 'dark' | 'light';
38-
const themeMap: Record<ThemeKey, Theme> = {
39-
light: createTheme({
40-
palette: {
41-
mode: 'light',
42-
},
43-
}),
44-
dark: createTheme({
45-
palette: {
46-
mode: 'dark',
47-
},
48-
}),
49-
};
50-
51-
const isThemeKey = (value: string | null): value is ThemeKey =>
52-
value === 'light' || value === 'dark';
5344

5445
const Layout = observer(() => {
5546
const {
@@ -66,15 +57,30 @@ const Layout = observer(() => {
6657
const {classes} = useStyles();
6758
const [currentTheme, setCurrentTheme] = React.useState<ThemeKey>(() => {
6859
const stored = window.localStorage.getItem(localStorageThemeKey);
69-
return isThemeKey(stored) ? stored : 'dark';
60+
return isThemeKey(stored) ? stored : 'system';
7061
});
71-
const theme = themeMap[currentTheme];
62+
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
63+
const paletteMode = currentTheme === 'system' ? (prefersDark ? 'dark' : 'light') : currentTheme;
64+
const theme = React.useMemo(
65+
() =>
66+
createTheme({
67+
palette: {
68+
mode: paletteMode,
69+
},
70+
}),
71+
[paletteMode]
72+
);
7273
const {version} = config.get('version');
7374
const [navOpen, setNavOpen] = React.useState(false);
7475
const [showSettings, setShowSettings] = React.useState(false);
7576

7677
const toggleTheme = () => {
77-
const next = currentTheme === 'dark' ? 'light' : 'dark';
78+
const nextMap: Record<ThemeKey, ThemeKey> = {
79+
dark: 'light',
80+
light: 'system',
81+
system: 'dark',
82+
};
83+
const next = nextMap[currentTheme];
7884
setCurrentTheme(next);
7985
localStorage.setItem(localStorageThemeKey, next);
8086
};
@@ -107,6 +113,7 @@ const Layout = observer(() => {
107113
style={{top: !connectionErrorMessage ? 0 : 64}}
108114
version={version}
109115
loggedIn={loggedIn}
116+
themeMode={currentTheme}
110117
toggleTheme={toggleTheme}
111118
showSettings={() => setShowSettings(true)}
112119
logout={logout}

ui/src/layout/theme.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type ThemeKey = 'dark' | 'light' | 'system';
2+
3+
export const isThemeKey = (value: string | null): value is ThemeKey =>
4+
value === 'light' || value === 'dark' || value === 'system';

0 commit comments

Comments
 (0)