diff --git a/package.json b/package.json index e2a6ce51..08476539 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@mui/icons-material": "6.3.0", "@mui/material": "6.3.0", "@mui/system": "6.3.0", + "@mui/utils": "^6.4.0", "@mui/x-date-pickers": "7.23.3", "apollo-upload-client": "18.0.1", "csstype": "3.1.3", diff --git a/public/locales/en.json b/public/locales/en.json index 6c92faf8..b335f7c6 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -1034,10 +1034,7 @@ "invalid_json": "Invalid json", "invalid_name": "The theme name must be unique and has a limit of 16 characters" }, - "info": { - "creating_theme": "Create a custom theme with the <2>MUI Theme Creator, copy everything after \"themeOptions\" from \"{\" to \"}\" and paste it into the \"theme\" text field.", - "theme_mode_lock": "In case \"mode\" or \"type\" is defined in the \"palette\" object, the theme will be locked into this mode and will not change regardless of the selected apps theme mode" - }, + "description": "See the official <0>MUI documentation for how to customize the theme.\nThe palette API is not supported, you have to use the new <1>color schemes API.\n\nYou can use theme creators like <2>MUI Theme Creator, however, you have to adjust the created object according to the new color schemes API.\n\nIn case no background is defined, a background will be calculated based on the primary color", "theme_name": "Name" }, "failure": "Could not create theme", diff --git a/src/lib/mui/MuiStyles.types.d.ts b/src/lib/mui/MuiStyles.types.d.ts new file mode 100644 index 00000000..d5a94c31 --- /dev/null +++ b/src/lib/mui/MuiStyles.types.d.ts @@ -0,0 +1,15 @@ +/* + * 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 '@mui/material/styles'; + +declare module '@mui/material/styles' { + interface CssThemeVariables { + enabled: true; + } +} diff --git a/src/lib/storage/AppStorage.ts b/src/lib/storage/AppStorage.ts index 52eb51c3..f40746a4 100644 --- a/src/lib/storage/AppStorage.ts +++ b/src/lib/storage/AppStorage.ts @@ -26,7 +26,9 @@ export class Storage { return this.parseValue(this.getItem(key), defaultValue); } - setItem(key: string, value: unknown, emitEvent: boolean = true): void { + setItem(key: string, value: unknown, emitEvent: boolean = true, stringify: boolean = true): void { + const currentValue = this.getItem(key); + const fireEvent = (valueToStore: string | undefined) => { if (!emitEvent) { return; @@ -35,7 +37,7 @@ export class Storage { window.dispatchEvent( new StorageEvent('storage', { key, - oldValue: this.getItem(key), + oldValue: currentValue, newValue: valueToStore, }), ); @@ -47,10 +49,10 @@ export class Storage { return; } - const valueToStore = JSON.stringify(value); + const valueToStore = stringify ? JSON.stringify(value) : value; - this.storage.setItem(key, valueToStore); - fireEvent(valueToStore); + this.storage.setItem(key, valueToStore as string); + fireEvent(valueToStore as string); } } diff --git a/src/modules/core/contexts/AppContext.tsx b/src/modules/core/contexts/AppContext.tsx index 93ed7bf5..bc0646ed 100644 --- a/src/modules/core/contexts/AppContext.tsx +++ b/src/modules/core/contexts/AppContext.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Direction, StyledEngineProvider, ThemeProvider } from '@mui/material/styles'; +import { Direction, StyledEngineProvider, ThemeProvider, useColorScheme } from '@mui/material/styles'; import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import { QueryParamProvider } from 'use-query-params'; @@ -58,6 +58,9 @@ export const AppContext: React.FC = ({ children }) => { 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, @@ -71,8 +74,14 @@ export const AppContext: React.FC = ({ children }) => { ); const theme = useMemo( - () => createAndSetTheme(themeMode, getTheme(appTheme, customThemes), pureBlackMode, currentDirection), - [themeMode, currentDirection, systemThemeMode, pureBlackMode, appTheme, customThemes], + () => + createAndSetTheme( + actualThemeMode as ThemeMode, + getTheme(appTheme, customThemes), + pureBlackMode, + currentDirection, + ), + [actualThemeMode, currentDirection, systemThemeMode, pureBlackMode, appTheme, customThemes], ); return ( diff --git a/src/modules/core/utils/MediaQuery.tsx b/src/modules/core/utils/MediaQuery.tsx index 52975b7d..ca97436d 100644 --- a/src/modules/core/utils/MediaQuery.tsx +++ b/src/modules/core/utils/MediaQuery.tsx @@ -85,7 +85,7 @@ export class MediaQuery { } static getThemeMode(): Exclude { - const themeMode = AppStorage.local.getItemParsed('themeMode', ThemeMode.SYSTEM); + const themeMode = AppStorage.local.getItem('mui-mode') as ThemeMode; const isSystemMode = themeMode === ThemeMode.SYSTEM; if (isSystemMode) { diff --git a/src/modules/settings/screens/Appearance.tsx b/src/modules/settings/screens/Appearance.tsx index fe4c1e66..38800e2b 100644 --- a/src/modules/settings/screens/Appearance.tsx +++ b/src/modules/settings/screens/Appearance.tsx @@ -15,6 +15,7 @@ import MenuItem from '@mui/material/MenuItem'; 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 { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { ThemeMode, ThemeModeContext } from '@/modules/theme/contexts/ThemeModeContext.tsx'; import { Select } from '@/modules/core/components/inputs/Select.tsx'; @@ -23,7 +24,6 @@ import { NumberSetting } from '@/modules/core/components/settings/NumberSetting. import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx'; import { I18nResourceCode, i18nResources } from '@/i18n'; import { langCodeToName } from '@/modules/core/utils/Languages.ts'; -import { getTheme } from '@/modules/theme/services/AppThemes.ts'; import { ThemeList } from '@/modules/theme/components/ThemeList.tsx'; import { createUpdateMetadataServerSettings, @@ -35,10 +35,13 @@ import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts' import { makeToast } from '@/modules/core/utils/Toast.ts'; import { MetadataThemeSettings } from '@/modules/theme/AppTheme.types.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { AppStorage } from '@/lib/storage/AppStorage.ts'; export const Appearance = () => { const { t, i18n } = useTranslation(); - const { themeMode, setThemeMode, pureBlackMode, setPureBlackMode, appTheme } = useContext(ThemeModeContext); + const { themeMode, setThemeMode, pureBlackMode, setPureBlackMode } = useContext(ThemeModeContext); + const { mode, setMode } = useColorScheme(); + const actualThemeMode = (mode ?? themeMode) as ThemeMode; const { setTitle, setAction } = useContext(NavBarContext); useLayoutEffect(() => { @@ -59,8 +62,7 @@ export const Appearance = () => { makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)), ); - const isDarkMode = - getTheme(appTheme).muiTheme.palette?.mode === 'dark' || MediaQuery.getThemeMode() === ThemeMode.DARK; + const isDarkMode = MediaQuery.getThemeMode() === ThemeMode.DARK; const DEFAULT_ITEM_WIDTH = 300; const [itemWidth, setItemWidth] = useLocalStorage('ItemWidth', DEFAULT_ITEM_WIDTH); @@ -89,7 +91,17 @@ export const Appearance = () => { > - value={themeMode} onChange={(e) => setThemeMode(e.target.value as ThemeMode)}> + + value={actualThemeMode} + onChange={(e) => { + const newMode = e.target.value as 'system' | 'light' | 'dark'; + + setThemeMode(newMode as ThemeMode); + setMode(newMode); + // in case a non "colorSchemes" mui theme is active, "setMode" does not update the mode ("mui-mode") value + AppStorage.local.setItem('mui-mode', newMode, true, false); + }} + > System diff --git a/src/modules/theme/Themes.ts b/src/modules/theme/Themes.ts index 87655930..07db4628 100644 --- a/src/modules/theme/Themes.ts +++ b/src/modules/theme/Themes.ts @@ -6,22 +6,40 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { ThemeOptions } from '@mui/material/styles'; +import { CssVarsThemeOptions } from '@mui/material/styles'; import { t as translate } from 'i18next'; -export type TBaseTheme = { isCustom: boolean; getName: () => string; muiTheme: ThemeOptions }; +export type TBaseTheme = { + isCustom: boolean; + getName: () => string; + muiTheme: Omit; +}; export const themes = { default: { isCustom: false, getName: () => translate('global.label.default'), muiTheme: { - palette: { - primary: { - main: '#5b74ef', + colorSchemes: { + light: { + palette: { + primary: { + main: '#5b74ef', + }, + secondary: { + main: '#efd65b', + }, + }, }, - secondary: { - main: '#efd65b', + dark: { + palette: { + primary: { + main: '#5b74ef', + }, + secondary: { + main: '#efd65b', + }, + }, }, }, }, @@ -30,12 +48,26 @@ export const themes = { isCustom: false, getName: () => translate('settings.appearance.theme.themes.lavender'), muiTheme: { - palette: { - primary: { - main: '#5c58dc', + colorSchemes: { + light: { + palette: { + primary: { + main: '#5c58dc', + }, + secondary: { + main: '#d8dc58', + }, + }, }, - secondary: { - main: '#d8dc58', + dark: { + palette: { + primary: { + main: '#5c58dc', + }, + secondary: { + main: '#d8dc58', + }, + }, }, }, }, @@ -44,12 +76,26 @@ export const themes = { isCustom: false, getName: () => translate('settings.appearance.theme.themes.dune'), muiTheme: { - palette: { - primary: { - main: '#897869', + colorSchemes: { + light: { + palette: { + primary: { + main: '#897869', + }, + secondary: { + main: '#697a89', + }, + }, }, - secondary: { - main: '#697a89', + dark: { + palette: { + primary: { + main: '#897869', + }, + secondary: { + main: '#697a89', + }, + }, }, }, }, @@ -58,12 +104,26 @@ export const themes = { isCustom: false, getName: () => translate('settings.appearance.theme.themes.rosegold'), muiTheme: { - palette: { - primary: { - main: '#E9A7A1', + colorSchemes: { + light: { + palette: { + primary: { + main: '#E9A7A1', + }, + secondary: { + main: '#A1E3E9', + }, + }, }, - secondary: { - main: '#A1E3E9', + dark: { + palette: { + primary: { + main: '#E9A7A1', + }, + secondary: { + main: '#A1E3E9', + }, + }, }, }, }, @@ -72,12 +132,26 @@ export const themes = { isCustom: false, getName: () => translate('settings.appearance.theme.themes.forest_dew'), muiTheme: { - palette: { - primary: { - main: '#53a584', + colorSchemes: { + light: { + palette: { + primary: { + main: '#53a584', + }, + secondary: { + main: '#a55374', + }, + }, }, - secondary: { - main: '#a55374', + dark: { + palette: { + primary: { + main: '#53a584', + }, + secondary: { + main: '#a55374', + }, + }, }, }, }, @@ -86,12 +160,26 @@ export const themes = { isCustom: false, getName: () => translate('settings.appearance.theme.themes.mountain_sunset'), muiTheme: { - palette: { - primary: { - main: '#c55a77', + colorSchemes: { + light: { + palette: { + primary: { + main: '#c55a77', + }, + secondary: { + main: '#5ac5a8', + }, + }, }, - secondary: { - main: '#5ac5a8', + dark: { + palette: { + primary: { + main: '#c55a77', + }, + secondary: { + main: '#5ac5a8', + }, + }, }, }, }, @@ -100,12 +188,26 @@ export const themes = { isCustom: false, getName: () => translate('settings.appearance.theme.themes.crimson'), muiTheme: { - palette: { - primary: { - main: '#DC143C', + colorSchemes: { + light: { + palette: { + primary: { + main: '#DC143C', + }, + secondary: { + main: '#14DCB4', + }, + }, }, - secondary: { - main: '#14DCB4', + dark: { + palette: { + primary: { + main: '#DC143C', + }, + secondary: { + main: '#14DCB4', + }, + }, }, }, }, @@ -114,12 +216,26 @@ export const themes = { isCustom: false, getName: () => translate('settings.appearance.theme.themes.minty_miracles'), muiTheme: { - palette: { - primary: { - main: '#5CE6A1', + colorSchemes: { + light: { + palette: { + primary: { + main: '#5CE6A1', + }, + secondary: { + main: '#E65CA1', + }, + }, }, - secondary: { - main: '#E65CA1', + dark: { + palette: { + primary: { + main: '#5CE6A1', + }, + secondary: { + main: '#E65CA1', + }, + }, }, }, }, @@ -128,12 +244,26 @@ export const themes = { isCustom: false, getName: () => translate('settings.appearance.theme.themes.orange_juice'), muiTheme: { - palette: { - primary: { - main: '#ffb546', + colorSchemes: { + light: { + palette: { + primary: { + main: '#ffb546', + }, + secondary: { + main: '#4690ff', + }, + }, }, - secondary: { - main: '#4690ff', + dark: { + palette: { + primary: { + main: '#ffb546', + }, + secondary: { + main: '#4690ff', + }, + }, }, }, }, @@ -142,12 +272,26 @@ export const themes = { isCustom: false, getName: () => translate('settings.appearance.theme.themes.bright_pink'), muiTheme: { - palette: { - primary: { - main: '#FF007F', + colorSchemes: { + light: { + palette: { + primary: { + main: '#FF007F', + }, + secondary: { + main: '#00FF80', + }, + }, }, - secondary: { - main: '#00FF80', + dark: { + palette: { + primary: { + main: '#FF007F', + }, + secondary: { + main: '#00FF80', + }, + }, }, }, }, @@ -156,12 +300,26 @@ export const themes = { isCustom: false, getName: () => translate('settings.appearance.theme.themes.veronica'), muiTheme: { - palette: { - primary: { - main: '#A020F0', + colorSchemes: { + light: { + palette: { + primary: { + main: '#A020F0', + }, + secondary: { + main: '#70F020', + }, + }, }, - secondary: { - main: '#70F020', + dark: { + palette: { + primary: { + main: '#A020F0', + }, + secondary: { + main: '#70F020', + }, + }, }, }, }, @@ -170,12 +328,26 @@ export const themes = { isCustom: false, getName: () => translate('settings.appearance.theme.themes.tree_frog_green'), muiTheme: { - palette: { - primary: { - main: '#8ace31', + colorSchemes: { + light: { + palette: { + primary: { + main: '#8ace31', + }, + secondary: { + main: '#7531CE', + }, + }, }, - secondary: { - main: '#7531CE', + dark: { + palette: { + primary: { + main: '#8ace31', + }, + secondary: { + main: '#7531CE', + }, + }, }, }, }, diff --git a/src/modules/theme/components/CreateThemeDialog.tsx b/src/modules/theme/components/CreateThemeDialog.tsx index 8e8e6751..e01cd935 100644 --- a/src/modules/theme/components/CreateThemeDialog.tsx +++ b/src/modules/theme/components/CreateThemeDialog.tsx @@ -37,12 +37,26 @@ const baseCustomTheme: AppTheme = { isCustom: true, getName: () => '', muiTheme: { - palette: { - primary: { - main: '#1976d2', + colorSchemes: { + light: { + palette: { + primary: { + main: '#1976d2', + }, + secondary: { + main: '#9c27b0', + }, + }, }, - secondary: { - main: '#9c27b0', + dark: { + palette: { + primary: { + main: '#1976d2', + }, + secondary: { + main: '#9c27b0', + }, + }, }, }, }, @@ -115,10 +129,23 @@ export const ThemeCreationDialog = ({ /> )} {!error && ( - + - - Create a custom theme with the{' '} + + + MUI documentation + + + MUI documentation + - {t('settings.appearance.theme.create.dialog.info.theme_mode_lock')} { {allThemes.map((theme) => ( { makeToast(t('settings.appearance.theme.delete.action', { theme: theme.getName() }), 'info'); updateCustomThemes( diff --git a/src/modules/theme/components/ThemePreview.tsx b/src/modules/theme/components/ThemePreview.tsx index 88c63860..6ecbddf5 100644 --- a/src/modules/theme/components/ThemePreview.tsx +++ b/src/modules/theme/components/ThemePreview.tsx @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'; import { useContext, useMemo } from 'react'; import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; -import { styled, ThemeProvider } from '@mui/material/styles'; +import { styled, ThemeProvider, useTheme } from '@mui/material/styles'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import Card from '@mui/material/Card'; import CardActionArea from '@mui/material/CardActionArea'; @@ -32,17 +32,21 @@ const ThemePreviewBadge = styled(Box)(() => ({ height: '20px', })); -export const ThemePreview = ({ theme, onDelete }: { theme: AppTheme; onDelete: () => void }) => { - const { getName } = theme; +export const ThemePreview = ({ appTheme, onDelete }: { appTheme: AppTheme; onDelete: () => void }) => { + const { getName } = appTheme; const { t } = useTranslation(); - const { themeMode, setAppTheme, appTheme, pureBlackMode } = useContext(ThemeModeContext); + const theme = useTheme(); + const { themeMode, setAppTheme, appTheme: activeAppTheme, pureBlackMode } = useContext(ThemeModeContext); - const popupState = usePopupState({ variant: 'popover', popupId: `theme-edit-dialog-${theme.id}` }); + const popupState = usePopupState({ variant: 'popover', popupId: `theme-edit-dialog-${appTheme.id}` }); - const isSelected = theme.id === appTheme; + const isSelected = appTheme.id === activeAppTheme; - const muiTheme = useMemo(() => createTheme(themeMode, theme, pureBlackMode), [theme, themeMode, pureBlackMode]); + const muiTheme = useMemo( + () => createTheme(themeMode, appTheme, pureBlackMode), + [appTheme, themeMode, pureBlackMode], + ); return ( <> @@ -71,12 +75,15 @@ export const ThemePreview = ({ theme, onDelete }: { theme: AppTheme; onDelete: ( bottom: 0, left: 0, border: '4px solid', - borderColor: isSelected - ? muiTheme.palette.primary.light - : muiTheme.palette.background.paper, + borderColor: isSelected ? muiTheme.palette.primary.light : muiTheme.palette.grey['100'], zIndex: 1, pointerEvents: 'none', borderRadius: 1, + ...theme.applyStyles('dark', { + borderColor: isSelected + ? muiTheme.palette.primary.light + : muiTheme.palette.grey['900'], + }), }} /> { - const needToLoadFonts = hasMissingFonts(theme.muiTheme); + const needToLoadFonts = hasMissingFonts(appTheme.muiTheme); if (!needToLoadFonts) { - setAppTheme(theme.id); + setAppTheme(appTheme.id); return; } makeToast(t('settings.appearance.theme.select.fonts.loading'), 'info'); - loadThemeFonts(theme.muiTheme) - .then(() => setAppTheme(theme.id)) + loadThemeFonts(appTheme.muiTheme) + .then(() => setAppTheme(appTheme.id)) .catch((e) => makeToast( t('settings.appearance.theme.select.fonts.error'), @@ -136,7 +143,7 @@ export const ThemePreview = ({ theme, onDelete }: { theme: AppTheme; onDelete: ( }} /> )} - {!isSelected && theme.isCustom && ( + {!isSelected && appTheme.isCustom && ( { e.preventDefault(); @@ -152,7 +159,7 @@ export const ThemePreview = ({ theme, onDelete }: { theme: AppTheme; onDelete: ( )} - {theme.isCustom && ( + {appTheme.isCustom && ( { e.preventDefault(); @@ -194,7 +201,7 @@ export const ThemePreview = ({ theme, onDelete }: { theme: AppTheme; onDelete: ( {getName()} - + ); }; diff --git a/src/modules/theme/services/AppThemes.ts b/src/modules/theme/services/AppThemes.ts index 2742c656..a6c375a5 100644 --- a/src/modules/theme/services/AppThemes.ts +++ b/src/modules/theme/services/AppThemes.ts @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { ThemeOptions } from '@mui/material/styles'; +import { CssVarsThemeOptions } from '@mui/material/styles'; import WebFont from 'webfontloader'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { ControlledPromise } from '@/lib/ControlledPromise.ts'; @@ -66,12 +66,12 @@ const getFontsFromTheme = (obj: Record, fonts: string[] = []): stri const loadedFonts: string[] = []; -export const hasMissingFonts = (theme: ThemeOptions): boolean => { +export const hasMissingFonts = (theme: CssVarsThemeOptions): boolean => { const themeFonts = getFontsFromTheme(theme.typography ?? {}); return !!themeFonts.length && themeFonts.some((font) => !loadedFonts.includes(font)); }; -export const loadThemeFonts = async (theme: ThemeOptions): Promise => { +export const loadThemeFonts = async (theme: CssVarsThemeOptions): Promise => { const themeFonts = getFontsFromTheme(theme.typography ?? {}); const missingThemeFonts = themeFonts.filter((font) => !loadedFonts.includes(font)); diff --git a/src/modules/theme/services/ThemeCreator.ts b/src/modules/theme/services/ThemeCreator.ts index a2abdbba..b1d78473 100644 --- a/src/modules/theme/services/ThemeCreator.ts +++ b/src/modules/theme/services/ThemeCreator.ts @@ -11,12 +11,16 @@ import { darken, Direction, lighten, - Palette, responsiveFontSizes, Theme, + TypeBackground, useTheme, } from '@mui/material/styles'; import { useCallback } from 'react'; +// eslint-disable-next-line import/no-extraneous-dependencies,no-restricted-imports +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 { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; import { AppTheme, loadThemeFonts } from '@/modules/theme/services/AppThemes.ts'; @@ -25,6 +29,56 @@ 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, + theme: Theme, + setPureBlackMode: boolean = false, +): (Partial & Partial) | undefined => { + if (setPureBlackMode) { + return { + paper: '#111', + default: '#000', + }; + } + + 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; + } + + return { + paper: lighten(theme.colorSchemes.light.palette.primary.dark, 0.8), + default: lighten(theme.colorSchemes.light.palette.primary.dark, 0.9), + }; + } + + 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; + } + + return { + paper: darken(theme.colorSchemes.dark.palette.primary.dark, 0.8), + default: darken(theme.colorSchemes.dark.palette.primary.dark, 0.9), + }; + } + + return undefined; +}; + export const createTheme = ( themeMode: ThemeMode, appTheme: AppTheme, @@ -33,93 +87,78 @@ export const createTheme = ( ) => { const systemMode = MediaQuery.getSystemThemeMode(); - const appThemeType = (appTheme.muiTheme.palette as any)?.type ?? appTheme.muiTheme.palette?.mode; - const isStaticThemeMode = !!appThemeType; - const appThemeMode = appThemeType === 'dark' ? ThemeMode.DARK : ThemeMode.LIGHT; - const staticThemeMode = isStaticThemeMode ? appThemeMode : undefined; - - const mode = staticThemeMode ?? (themeMode === ThemeMode.SYSTEM ? systemMode : themeMode); + const mode = themeMode === ThemeMode.SYSTEM ? systemMode : themeMode; const isDarkMode = mode === ThemeMode.DARK; const setPureBlackMode = isDarkMode && pureBlackMode; - const baseTheme = createMuiTheme({ - direction, - ...appTheme.muiTheme, - palette: { - mode, - ...(appTheme.muiTheme.palette ?? {}), - }, - }); + const themeForColors = createMuiTheme({ ...appTheme.muiTheme, defaultColorScheme: mode }); - const backgroundTrueBlack: Palette['background'] = { - paper: '#111', - default: '#000', - }; - const backgroundDark: Palette['background'] = { - paper: darken(baseTheme.palette.primary.dark, 0.75), - default: darken(baseTheme.palette.primary.dark, 0.85), - }; - const backgroundLight: Palette['background'] = { - paper: lighten(baseTheme.palette.primary.dark, 0.8), - default: lighten(baseTheme.palette.primary.dark, 0.9), - }; - const backgroundThemeMode = isDarkMode ? backgroundDark : backgroundLight; - const automaticBackground = setPureBlackMode ? backgroundTrueBlack : backgroundThemeMode; - const appThemeBackground = appTheme.muiTheme.palette?.background; - - const requiresAutomaticBackground = setPureBlackMode || !appThemeBackground; - const background = requiresAutomaticBackground ? automaticBackground : appThemeBackground; - - const colorTheme = createMuiTheme(baseTheme, { - palette: { - background, - }, - }); - - const suwayomiTheme = createMuiTheme(colorTheme, { - components: { - ...appTheme.muiTheme.components, - MuiUseMediaQuery: { - defaultProps: { - noSsr: true, - }, + const suwayomiTheme = createMuiTheme( + deepmerge(appTheme.muiTheme, { + defaultColorScheme: mode, + direction, + colorSchemes: { + light: appTheme.muiTheme.colorSchemes?.light + ? { + palette: { + background: getBackgroundColor('light', appTheme, themeForColors), + }, + } + : undefined, + dark: appTheme.muiTheme.colorSchemes?.dark + ? { + palette: { + background: getBackgroundColor('dark', appTheme, themeForColors, setPureBlackMode), + }, + } + : undefined, }, - MuiCssBaseline: { - ...appTheme.muiTheme.components?.MuiCssBaseline, - styleOverrides: - typeof appTheme.muiTheme.components?.MuiCssBaseline?.styleOverrides === 'object' - ? { - ...appTheme.muiTheme.components?.MuiCssBaseline?.styleOverrides, - '*::-webkit-scrollbar': applyStyles(CSS.supports('-webkit-touch-callout', 'none'), { - width: `${SCROLLBAR_SIZE}px`, - height: `${SCROLLBAR_SIZE}px`, - // @ts-ignore - '*::-webkit-scrollbar' is a valid key - ...appTheme.muiTheme.components?.MuiCssBaseline?.styleOverrides?.[ - '*::-webkit-scrollbar' - ], - }), - '*::-webkit-scrollbar-thumb': applyStyles(CSS.supports('-webkit-touch-callout', 'none'), { - border: '4px solid rgba(0, 0, 0, 0)', - backgroundClip: 'padding-box', - borderRadius: '9999px', - backgroundColor: `${colorTheme.palette.primary[isDarkMode ? 'dark' : 'light']}`, - // @ts-ignore - '*::-webkit-scrollbar-thumb' is a valid key - ...appTheme.muiTheme.components?.MuiCssBaseline?.styleOverrides?.[ - '*::-webkit-scrollbar-thumb' - ], - }), - '*::-webkit-scrollbar-thumb:hover': applyStyles( - CSS.supports('-webkit-touch-callout', 'none'), - { - borderWidth: '2px', - // @ts-ignore - '*::-webkit-scrollbar-thumb:hover' is a valid key + components: { + ...appTheme.muiTheme.components, + MuiUseMediaQuery: { + defaultProps: { + noSsr: true, + }, + }, + MuiCssBaseline: { + ...appTheme.muiTheme.components?.MuiCssBaseline, + styleOverrides: + typeof appTheme.muiTheme.components?.MuiCssBaseline?.styleOverrides === 'object' + ? { + ...appTheme.muiTheme.components?.MuiCssBaseline?.styleOverrides, + '*::-webkit-scrollbar': applyStyles(CSS.supports('-webkit-touch-callout', 'none'), { + width: `${SCROLLBAR_SIZE}px`, + height: `${SCROLLBAR_SIZE}px`, + // @ts-ignore - '*::-webkit-scrollbar' is a valid key ...appTheme.muiTheme.components?.MuiCssBaseline?.styleOverrides?.[ - '*::-webkit-scrollbar-thumb:hover' + '*::-webkit-scrollbar' ], - }, - ), - } - : ` + }), + '*::-webkit-scrollbar-thumb': applyStyles( + CSS.supports('-webkit-touch-callout', 'none'), + { + border: '4px solid rgba(0, 0, 0, 0)', + backgroundClip: 'padding-box', + borderRadius: '9999px', + backgroundColor: `${themeForColors.palette.primary[isDarkMode ? 'dark' : 'light']}`, + // @ts-ignore - '*::-webkit-scrollbar-thumb' is a valid key + ...appTheme.muiTheme.components?.MuiCssBaseline?.styleOverrides?.[ + '*::-webkit-scrollbar-thumb' + ], + }, + ), + '*::-webkit-scrollbar-thumb:hover': applyStyles( + CSS.supports('-webkit-touch-callout', 'none'), + { + borderWidth: '2px', + // @ts-ignore - '*::-webkit-scrollbar-thumb:hover' is a valid key + ...appTheme.muiTheme.components?.MuiCssBaseline?.styleOverrides?.[ + '*::-webkit-scrollbar-thumb:hover' + ], + }, + ), + } + : ` @supports not (-webkit-touch-callout: none) { /* CSS for other than iOS devices */ *::-webkit-scrollbar { @@ -130,7 +169,7 @@ export const createTheme = ( border: 4px solid rgba(0, 0, 0, 0); background-clip: padding-box; border-radius: 9999px; - background-color: ${colorTheme.palette.primary[isDarkMode ? 'dark' : 'light']}; + background-color: ${themeForColors.palette.primary[isDarkMode ? 'dark' : 'light']}; } *::-webkit-scrollbar-thumb:hover { border-width: 2px; @@ -139,9 +178,10 @@ export const createTheme = ( ${appTheme.muiTheme.components?.MuiCssBaseline?.styleOverrides ?? ''} } `, + }, }, - }, - }); + }), + ); return responsiveFontSizes(suwayomiTheme); }; diff --git a/yarn.lock b/yarn.lock index e5b0aaf6..0b0b6f58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2409,6 +2409,11 @@ resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.20.tgz#16d8c9178b42b62ba95bbedbda8f343feb373d1e" integrity sha512-straFHD7L8v05l/N5vcWk+y7eL9JF0C2mtph/y4BPm3gn2Eh61dDwDB65pa8DLss3WJfDXYC7Kx5yjP0EmXpgw== +"@mui/types@^7.2.21": + version "7.2.21" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.21.tgz#63f50874eda8e4a021a69aaa8ba9597369befda2" + integrity sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww== + "@mui/utils@^5.16.6 || ^6.0.0", "@mui/utils@^6.3.0": version "6.3.0" resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.3.0.tgz#76c80cb663c7c1bdf782de50a5c3f6ff8bf38e21" @@ -2421,6 +2426,18 @@ prop-types "^15.8.1" react-is "^19.0.0" +"@mui/utils@^6.4.0": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.4.0.tgz#817d8135794b8741ad0267b90dc656361f1c0030" + integrity sha512-woOTATWNsTNR3YBh2Ixkj3l5RaxSiGoC9G8gOpYoFw1mZM77LWJeuMHFax7iIW4ahK0Cr35TF9DKtrafJmOmNQ== + dependencies: + "@babel/runtime" "^7.26.0" + "@mui/types" "^7.2.21" + "@types/prop-types" "^15.7.14" + clsx "^2.1.1" + prop-types "^15.8.1" + react-is "^19.0.0" + "@mui/x-date-pickers@7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.23.3.tgz#0c2b3db6a99e6e38aeb0980bfdf98468f415dded"