Use dynamic color schemes on manga page

This commit is contained in:
schroda
2025-01-26 03:53:19 +01:00
parent 25e3fc7808
commit a81c3c6afa
12 changed files with 556 additions and 38 deletions

View File

@@ -11,4 +11,5 @@ import { AppTheme } from '@/modules/theme/services/AppThemes.ts';
export type MetadataThemeSettings = {
customThemes: Record<string, AppTheme>;
mangaThumbnailBackdrop: boolean;
mangaDynamicColorSchemes: boolean;
};

View File

@@ -6,7 +6,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import React from 'react';
import React, { useContext } from 'react';
import { Palette } from '@vibrant/color';
import { FastAverageColorResult } from 'fast-average-color';
import { AppThemes } from '@/modules/theme/services/AppThemes.ts';
export enum ThemeMode {
@@ -22,6 +24,10 @@ export type TAppThemeContext = {
setThemeMode: React.Dispatch<React.SetStateAction<ThemeMode>>;
pureBlackMode: boolean;
setPureBlackMode: React.Dispatch<React.SetStateAction<boolean>>;
dynamicColor: (NonNullableProperties<Palette> & { average: FastAverageColorResult }) | null;
setDynamicColor: React.Dispatch<
React.SetStateAction<(NonNullableProperties<Palette> & { average: FastAverageColorResult }) | null>
>;
};
export const AppThemeContext = React.createContext<TAppThemeContext>({
@@ -31,4 +37,8 @@ export const AppThemeContext = React.createContext<TAppThemeContext>({
setThemeMode: (): void => {},
pureBlackMode: false,
setPureBlackMode: (): void => {},
dynamicColor: null,
setDynamicColor: (): void => {},
});
export const useAppThemeContext = () => useContext(AppThemeContext);

View File

@@ -10,7 +10,7 @@ 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 { AppThemeContext, TAppThemeContext, 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';
@@ -31,6 +31,7 @@ export const AppThemeContextProvider = ({ children }: { children: ReactNode }) =
const directionRef = useRef<Direction>('ltr');
const [systemThemeMode, setSystemThemeMode] = useState<ThemeMode>(MediaQuery.getSystemThemeMode());
const [dynamicColor, setDynamicColor] = useState<TAppThemeContext['dynamicColor']>(null);
const actualThemeMode = mode ?? themeMode ?? 'dark';
const currentDirection = i18n.dir();
@@ -43,8 +44,10 @@ export const AppThemeContextProvider = ({ children }: { children: ReactNode }) =
setThemeMode,
pureBlackMode,
setPureBlackMode,
dynamicColor,
setDynamicColor,
}),
[themeMode, pureBlackMode, appTheme],
[themeMode, pureBlackMode, appTheme, dynamicColor],
);
const theme = useMemo(
@@ -54,8 +57,9 @@ export const AppThemeContextProvider = ({ children }: { children: ReactNode }) =
getTheme(appTheme, customThemes),
pureBlackMode,
currentDirection,
dynamicColor,
),
[actualThemeMode, currentDirection, systemThemeMode, pureBlackMode, appTheme, customThemes],
[actualThemeMode, currentDirection, systemThemeMode, pureBlackMode, appTheme, customThemes, dynamicColor],
);
useLayoutEffect(() => {

View File

@@ -21,7 +21,8 @@ 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/AppThemeContext.tsx';
import { Palette } from '@vibrant/color';
import { TAppThemeContext, 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';
@@ -67,12 +68,123 @@ const getBackgroundColor = (
return undefined;
};
const createAppThemeWithDynamicPrimaryColor = (
primaryColor: string | null | undefined,
appTheme: AppTheme['muiTheme'],
): AppTheme['muiTheme'] => {
if (!primaryColor) {
return appTheme;
}
return {
...appTheme,
colorSchemes: {
light: { palette: { primary: { main: primaryColor } } },
dark: { palette: { primary: { main: primaryColor } } },
},
} satisfies AppTheme['muiTheme'];
};
const getHighestPopulationColor = (
palette: NonNullableProperties<Palette> | null,
mode: Exclude<ThemeMode, ThemeMode.SYSTEM>,
): string | null => {
if (!palette) {
return null;
}
let highestPopulation = 0;
let highestPopulationKey: string = '';
let highestPopulationColor: string = '';
// eslint-disable-next-line guard-for-in
for (const key in palette) {
const swatch = palette[key];
if (swatch.population > highestPopulation) {
highestPopulationKey = key;
highestPopulation = swatch.population;
highestPopulationColor = swatch.hex;
}
}
if (mode === ThemeMode.LIGHT) {
if (highestPopulationKey.startsWith('Light')) {
return palette[highestPopulationKey.replace('Light', 'Dark')].hex;
}
if (!highestPopulationKey.startsWith('Dark')) {
return palette[`Dark${highestPopulationKey}`].hex;
}
}
if (mode === ThemeMode.DARK) {
if (highestPopulationKey.startsWith('Dark')) {
return palette[highestPopulationKey.replace('Dark', 'Light')].hex;
}
if (!highestPopulationKey.startsWith('Light')) {
return palette[`Light${highestPopulationKey}`].hex;
}
}
return highestPopulationColor;
};
const createAppColorTheme = (
appTheme: AppTheme['muiTheme'],
dynamicColor: TAppThemeContext['dynamicColor'],
setPureBlackMode: boolean,
mode: Exclude<ThemeMode, ThemeMode.SYSTEM>,
): AppTheme['muiTheme'] => {
const appThemeWithDominantPrimaryColor = createAppThemeWithDynamicPrimaryColor(dynamicColor?.average.hex, appTheme);
const themePrimaryColorForBackground = createMuiTheme({
...appThemeWithDominantPrimaryColor,
defaultColorScheme: mode,
});
const themeBackgroundColor = deepmerge(appThemeWithDominantPrimaryColor, {
defaultColorScheme: mode,
colorSchemes: {
light: themePrimaryColorForBackground.colorSchemes?.light
? {
palette: {
background: getBackgroundColor(
'light',
appThemeWithDominantPrimaryColor,
themePrimaryColorForBackground,
),
},
}
: undefined,
dark: themePrimaryColorForBackground.colorSchemes?.dark
? {
palette: {
background: getBackgroundColor(
'dark',
appThemeWithDominantPrimaryColor,
themePrimaryColorForBackground,
setPureBlackMode,
),
},
}
: undefined,
},
});
const appThemeWithVibrantPrimaryColor = createAppThemeWithDynamicPrimaryColor(
getHighestPopulationColor(dynamicColor, mode),
themeBackgroundColor,
);
return deepmerge(themeBackgroundColor, appThemeWithVibrantPrimaryColor);
};
export const createTheme = (
themeMode: ThemeMode,
appTheme: AppTheme,
pureBlackMode: boolean = false,
direction: Direction = 'ltr',
dynamicColor: string | null = null,
dynamicColor: TAppThemeContext['dynamicColor'] = null,
) => {
const systemMode = MediaQuery.getSystemThemeMode();
@@ -80,35 +192,14 @@ 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 appColorTheme = createAppColorTheme(appTheme.muiTheme, dynamicColor, setPureBlackMode, mode);
const themeForColors = createMuiTheme({ ...appTheme.muiTheme, defaultColorScheme: mode });
const themeForColors = createMuiTheme({ ...appColorTheme, defaultColorScheme: mode });
const suwayomiTheme = createMuiTheme(
deepmerge(appTheme.muiTheme, {
deepmerge(appColorTheme, {
defaultColorScheme: mode,
direction,
colorSchemes: {
light: appTheme.muiTheme.colorSchemes?.light
? {
palette: {
background: getBackgroundColor('light', themeFinalColor, themeForColors),
},
}
: undefined,
dark: appTheme.muiTheme.colorSchemes?.dark
? {
palette: {
background: getBackgroundColor('dark', themeFinalColor, themeForColors, setPureBlackMode),
},
}
: undefined,
},
components: {
...appTheme.muiTheme.components,
MuiUseMediaQuery: {