From b7fc1ae6ff2a04e7f40d62b92848bed8d2695a09 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sat, 25 Jan 2025 19:22:24 +0100 Subject: [PATCH] Extract theme context logic into its own provider --- src/modules/core/contexts/AppContext.tsx | 133 +++++------------- src/modules/core/utils/MediaQuery.tsx | 2 +- .../ReaderProgressBarDirectionWrapper.tsx | 2 +- src/modules/settings/screens/Appearance.tsx | 4 +- src/modules/theme/components/ThemePreview.tsx | 4 +- ...emeModeContext.tsx => AppThemeContext.tsx} | 4 +- .../contexts/AppThemeContextProvider.tsx | 79 +++++++++++ src/modules/theme/services/ThemeCreator.ts | 36 +++-- 8 files changed, 137 insertions(+), 127 deletions(-) rename src/modules/theme/contexts/{ThemeModeContext.tsx => AppThemeContext.tsx} (89%) create mode 100644 src/modules/theme/contexts/AppThemeContextProvider.tsx diff --git a/src/modules/core/contexts/AppContext.tsx b/src/modules/core/contexts/AppContext.tsx index d14539ed..c57ea766 100644 --- a/src/modules/core/contexts/AppContext.tsx +++ b/src/modules/core/contexts/AppContext.tsx @@ -6,118 +6,53 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Direction, StyledEngineProvider, ThemeProvider, useColorScheme } from '@mui/material/styles'; -import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { StyledEngineProvider } from '@mui/material/styles'; +import React from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import { QueryParamProvider } from 'use-query-params'; import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6'; -import { useTranslation } from 'react-i18next'; -import { CacheProvider } from '@emotion/react'; import { SnackbarProvider } from 'notistack'; -import { createAndSetTheme } from '@/modules/theme/services/ThemeCreator.ts'; -import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx'; -import { ThemeMode, ThemeModeContext } from '@/modules/theme/contexts/ThemeModeContext.tsx'; import { NavBarContextProvider } from '@/modules/navigation-bar/contexts/NavBarContextProvider.tsx'; import { LibraryOptionsContextProvider } from '@/modules/library/contexts/LibraryOptionsProvider.tsx'; import { ActiveDeviceContextProvider } from '@/modules/device/contexts/DeviceContext.tsx'; -import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; -import { AppThemes, getTheme } from '@/modules/theme/services/AppThemes.ts'; -import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; import { ReaderContextProvider } from '@/modules/reader/contexts/ReaderContextProvider.tsx'; -import { DIRECTION_TO_CACHE } from '@/modules/theme/ThemeDirectionCache.ts'; import { AppHotkeysProvider } from '@/modules/hotkeys/contexts/AppHotkeysProvider.tsx'; import { SnackbarWithDescription } from '@/modules/core/components/snackbar/SnackbarWithDescription.tsx'; import { AppPageHistoryContextProvider } from '@/modules/core/contexts/AppPageHistoryContextProvider.tsx'; +import { AppThemeContextProvider } from '@/modules/theme/contexts/AppThemeContextProvider.tsx'; interface Props { children: React.ReactNode; } -export const AppContext: React.FC = ({ children }) => { - const directionRef = useRef('ltr'); - const { i18n } = useTranslation(); - - const currentDirection = i18n.dir(); - - if (directionRef.current !== currentDirection) { - document.dir = currentDirection; - directionRef.current = currentDirection; - } - - const { - settings: { customThemes }, - } = useMetadataServerSettings(); - - const [systemThemeMode, setSystemThemeMode] = useState(MediaQuery.getSystemThemeMode()); - useLayoutEffect(() => { - const unsubscribe = MediaQuery.listenToSystemThemeChange(setSystemThemeMode); - - return () => unsubscribe(); - }, []); - - const [appTheme, setAppTheme] = useLocalStorage('appTheme', 'default'); - const [themeMode, setThemeMode] = useLocalStorage('themeMode', ThemeMode.SYSTEM); - const [pureBlackMode, setPureBlackMode] = useLocalStorage('pureBlackMode', false); - - const { mode } = useColorScheme(); - const actualThemeMode = mode ?? themeMode ?? 'dark'; - - const darkThemeContext = useMemo( - () => ({ - appTheme, - setAppTheme, - themeMode, - setThemeMode, - pureBlackMode, - setPureBlackMode, - }), - [themeMode, pureBlackMode, appTheme], - ); - - const theme = useMemo( - () => - createAndSetTheme( - actualThemeMode as ThemeMode, - getTheme(appTheme, customThemes), - pureBlackMode, - currentDirection, - ), - [actualThemeMode, currentDirection, systemThemeMode, pureBlackMode, appTheme, customThemes], - ); - - return ( - - - - - - - - - - - - - {children} - - - - - - - - - - - - - ); -}; +export const AppContext: React.FC = ({ children }) => ( + + + + + + + + + + + {children} + + + + + + + + + + +); diff --git a/src/modules/core/utils/MediaQuery.tsx b/src/modules/core/utils/MediaQuery.tsx index ca97436d..acaf757e 100644 --- a/src/modules/core/utils/MediaQuery.tsx +++ b/src/modules/core/utils/MediaQuery.tsx @@ -10,7 +10,7 @@ import useMediaQuery from '@mui/material/useMediaQuery'; import { Breakpoint } from '@mui/material/styles'; import { useCallback, useState } from 'react'; import { getCurrentTheme } from '@/modules/theme/services/ThemeCreator.ts'; -import { ThemeMode } from '@/modules/theme/contexts/ThemeModeContext.tsx'; +import { ThemeMode } from '@/modules/theme/contexts/AppThemeContext.tsx'; import { AppStorage } from '@/lib/storage/AppStorage.ts'; import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx'; diff --git a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarDirectionWrapper.tsx b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarDirectionWrapper.tsx index a70f498a..9b3ae2ba 100644 --- a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarDirectionWrapper.tsx +++ b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarDirectionWrapper.tsx @@ -13,7 +13,7 @@ import Box, { BoxProps } from '@mui/material/Box'; import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx'; import { AppThemes, getTheme } from '@/modules/theme/services/AppThemes.ts'; -import { ThemeMode } from '@/modules/theme/contexts/ThemeModeContext.tsx'; +import { ThemeMode } from '@/modules/theme/contexts/AppThemeContext.tsx'; import { createTheme } from '@/modules/theme/services/ThemeCreator.ts'; import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; import { DIRECTION_TO_CACHE } from '@/modules/theme/ThemeDirectionCache.ts'; diff --git a/src/modules/settings/screens/Appearance.tsx b/src/modules/settings/screens/Appearance.tsx index fc2568dd..4c8c39db 100644 --- a/src/modules/settings/screens/Appearance.tsx +++ b/src/modules/settings/screens/Appearance.tsx @@ -16,7 +16,7 @@ import ListSubheader from '@mui/material/ListSubheader'; import Switch from '@mui/material/Switch'; import Link from '@mui/material/Link'; import { useColorScheme } from '@mui/material/styles'; -import { ThemeMode, ThemeModeContext } from '@/modules/theme/contexts/ThemeModeContext.tsx'; +import { ThemeMode, AppThemeContext } from '@/modules/theme/contexts/AppThemeContext.tsx'; import { Select } from '@/modules/core/components/inputs/Select.tsx'; import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; import { NumberSetting } from '@/modules/core/components/settings/NumberSetting.tsx'; @@ -39,7 +39,7 @@ import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContex export const Appearance = () => { const { t, i18n } = useTranslation(); - const { themeMode, setThemeMode, pureBlackMode, setPureBlackMode } = useContext(ThemeModeContext); + const { themeMode, setThemeMode, pureBlackMode, setPureBlackMode } = useContext(AppThemeContext); const { mode, setMode } = useColorScheme(); const actualThemeMode = (mode ?? themeMode) as ThemeMode; diff --git a/src/modules/theme/components/ThemePreview.tsx b/src/modules/theme/components/ThemePreview.tsx index 6ecbddf5..c09adc38 100644 --- a/src/modules/theme/components/ThemePreview.tsx +++ b/src/modules/theme/components/ThemePreview.tsx @@ -19,7 +19,7 @@ import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; import EditIcon from '@mui/icons-material/Edit'; import { bindDialog, usePopupState } from 'material-ui-popup-state/hooks'; -import { ThemeModeContext } from '@/modules/theme/contexts/ThemeModeContext.tsx'; +import { AppThemeContext } from '@/modules/theme/contexts/AppThemeContext.tsx'; import { AppTheme, hasMissingFonts, loadThemeFonts } from '@/modules/theme/services/AppThemes.ts'; import { createTheme } from '@/modules/theme/services/ThemeCreator.ts'; import { ThemeCreationDialog } from '@/modules/theme/components/CreateThemeDialog.tsx'; @@ -37,7 +37,7 @@ export const ThemePreview = ({ appTheme, onDelete }: { appTheme: AppTheme; onDel const { t } = useTranslation(); const theme = useTheme(); - const { themeMode, setAppTheme, appTheme: activeAppTheme, pureBlackMode } = useContext(ThemeModeContext); + const { themeMode, setAppTheme, appTheme: activeAppTheme, pureBlackMode } = useContext(AppThemeContext); const popupState = usePopupState({ variant: 'popover', popupId: `theme-edit-dialog-${appTheme.id}` }); diff --git a/src/modules/theme/contexts/ThemeModeContext.tsx b/src/modules/theme/contexts/AppThemeContext.tsx similarity index 89% rename from src/modules/theme/contexts/ThemeModeContext.tsx rename to src/modules/theme/contexts/AppThemeContext.tsx index cf8b9931..dafcd835 100644 --- a/src/modules/theme/contexts/ThemeModeContext.tsx +++ b/src/modules/theme/contexts/AppThemeContext.tsx @@ -15,7 +15,7 @@ export enum ThemeMode { LIGHT = 'light', } -type ThemeModeContextType = { +export type TAppThemeContext = { appTheme: AppThemes; setAppTheme: React.Dispatch>; themeMode: ThemeMode; @@ -24,7 +24,7 @@ type ThemeModeContextType = { setPureBlackMode: React.Dispatch>; }; -export const ThemeModeContext = React.createContext({ +export const AppThemeContext = React.createContext({ appTheme: 'default', setAppTheme: (): void => {}, themeMode: ThemeMode.SYSTEM, diff --git a/src/modules/theme/contexts/AppThemeContextProvider.tsx b/src/modules/theme/contexts/AppThemeContextProvider.tsx new file mode 100644 index 00000000..ab7743f1 --- /dev/null +++ b/src/modules/theme/contexts/AppThemeContextProvider.tsx @@ -0,0 +1,79 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { ReactNode, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { Direction, ThemeProvider, useColorScheme } from '@mui/material/styles'; +import { CacheProvider } from '@emotion/react'; +import { useTranslation } from 'react-i18next'; +import { AppThemeContext, ThemeMode } from '@/modules/theme/contexts/AppThemeContext.tsx'; +import { DIRECTION_TO_CACHE } from '@/modules/theme/ThemeDirectionCache.ts'; +import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; +import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; +import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx'; +import { AppThemes, getTheme } from '@/modules/theme/services/AppThemes.ts'; +import { createAndSetTheme } from '@/modules/theme/services/ThemeCreator.ts'; + +export const AppThemeContextProvider = ({ children }: { children: ReactNode }) => { + const { i18n } = useTranslation(); + const { mode } = useColorScheme(); + const { + settings: { customThemes }, + } = useMetadataServerSettings(); + const [appTheme, setAppTheme] = useLocalStorage('appTheme', 'default'); + const [themeMode, setThemeMode] = useLocalStorage('themeMode', ThemeMode.SYSTEM); + const [pureBlackMode, setPureBlackMode] = useLocalStorage('pureBlackMode', false); + + const directionRef = useRef('ltr'); + + const [systemThemeMode, setSystemThemeMode] = useState(MediaQuery.getSystemThemeMode()); + + const actualThemeMode = mode ?? themeMode ?? 'dark'; + const currentDirection = i18n.dir(); + + const appThemeContext = useMemo( + () => ({ + appTheme, + setAppTheme, + themeMode, + setThemeMode, + pureBlackMode, + setPureBlackMode, + }), + [themeMode, pureBlackMode, appTheme], + ); + + const theme = useMemo( + () => + createAndSetTheme( + actualThemeMode as ThemeMode, + getTheme(appTheme, customThemes), + pureBlackMode, + currentDirection, + ), + [actualThemeMode, currentDirection, systemThemeMode, pureBlackMode, appTheme, customThemes], + ); + + useLayoutEffect(() => { + const unsubscribe = MediaQuery.listenToSystemThemeChange(setSystemThemeMode); + + return () => unsubscribe(); + }, []); + + if (directionRef.current !== currentDirection) { + document.dir = currentDirection; + directionRef.current = currentDirection; + } + + return ( + + + {children} + + + ); +}; diff --git a/src/modules/theme/services/ThemeCreator.ts b/src/modules/theme/services/ThemeCreator.ts index b1d78473..5c58ed90 100644 --- a/src/modules/theme/services/ThemeCreator.ts +++ b/src/modules/theme/services/ThemeCreator.ts @@ -21,7 +21,7 @@ import { useCallback } from 'react'; import { deepmerge } from '@mui/utils'; // eslint-disable-next-line no-restricted-imports import { PaletteBackgroundChannel } from '@mui/material/styles/createThemeWithVars'; -import { ThemeMode } from '@/modules/theme/contexts/ThemeModeContext.tsx'; +import { ThemeMode } from '@/modules/theme/contexts/AppThemeContext.tsx'; import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; import { AppTheme, loadThemeFonts } from '@/modules/theme/services/AppThemes.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; @@ -29,15 +29,9 @@ import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts'; const SCROLLBAR_SIZE = 14; -declare module '@mui/material/styles' { - interface CssThemeVariables { - enabled: true; - } -} - const getBackgroundColor = ( type: 'light' | 'dark', - appTheme: AppTheme, + appTheme: AppTheme['muiTheme'], theme: Theme, setPureBlackMode: boolean = false, ): (Partial & Partial) | undefined => { @@ -49,11 +43,8 @@ const getBackgroundColor = ( } if (type === 'light' && !!theme.colorSchemes.light) { - if ( - typeof appTheme.muiTheme.colorSchemes?.light === 'object' && - appTheme.muiTheme.colorSchemes.light.palette?.background - ) { - return appTheme.muiTheme.colorSchemes.light.palette.background; + if (typeof appTheme.colorSchemes?.light === 'object' && appTheme.colorSchemes.light.palette?.background) { + return appTheme.colorSchemes.light.palette.background; } return { @@ -63,11 +54,8 @@ const getBackgroundColor = ( } if (type === 'dark' && !!theme.colorSchemes.dark) { - if ( - typeof appTheme.muiTheme.colorSchemes?.dark === 'object' && - appTheme.muiTheme.colorSchemes.dark.palette?.background - ) { - return appTheme.muiTheme.colorSchemes.dark.palette.background; + if (typeof appTheme.colorSchemes?.dark === 'object' && appTheme.colorSchemes.dark.palette?.background) { + return appTheme.colorSchemes.dark.palette.background; } return { @@ -84,6 +72,7 @@ export const createTheme = ( appTheme: AppTheme, pureBlackMode: boolean = false, direction: Direction = 'ltr', + dynamicColor: string | null = null, ) => { const systemMode = MediaQuery.getSystemThemeMode(); @@ -91,6 +80,13 @@ export const createTheme = ( const isDarkMode = mode === ThemeMode.DARK; const setPureBlackMode = isDarkMode && pureBlackMode; + const themeDynamicColor = { + ...appTheme.muiTheme, + light: { palette: { primary: { main: dynamicColor } } }, + dark: { palette: { primary: { main: dynamicColor } } }, + }; + const themeFinalColor = dynamicColor ? themeDynamicColor : appTheme.muiTheme; + const themeForColors = createMuiTheme({ ...appTheme.muiTheme, defaultColorScheme: mode }); const suwayomiTheme = createMuiTheme( @@ -101,14 +97,14 @@ export const createTheme = ( light: appTheme.muiTheme.colorSchemes?.light ? { palette: { - background: getBackgroundColor('light', appTheme, themeForColors), + background: getBackgroundColor('light', themeFinalColor, themeForColors), }, } : undefined, dark: appTheme.muiTheme.colorSchemes?.dark ? { palette: { - background: getBackgroundColor('dark', appTheme, themeForColors, setPureBlackMode), + background: getBackgroundColor('dark', themeFinalColor, themeForColors, setPureBlackMode), }, } : undefined,