diff --git a/CHANGELOG.md b/CHANGELOG.md index efe69ec4..0cce8c87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] (Preview) +### Added +- (**Theme**) Add an option to save the dynamic color theme on the manga page as a custom theme + ### Changed - (**Theme**) Improve automatic theme background colors - (**Theme**) Adjust "primary color" of dynamic color theme on manga pages diff --git a/public/locales/en.json b/public/locales/en.json index 9c3c14ec..7730915c 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -1156,6 +1156,8 @@ "appearance": { "manga_dynamic_color_schemes": { "description": "Changes the theme colors on the manga page based on the thumbnail", + "save": "Save color theme", + "set_active": "Set as active theme", "title": "Dynamic theme colors on manga page" }, "manga_thumbnail_backdrop": { diff --git a/src/features/manga/components/MangaToolbarMenu.tsx b/src/features/manga/components/MangaToolbarMenu.tsx index a987b04d..371aa8a8 100644 --- a/src/features/manga/components/MangaToolbarMenu.tsx +++ b/src/features/manga/components/MangaToolbarMenu.tsx @@ -21,10 +21,17 @@ import SyncAltIcon from '@mui/icons-material/SyncAlt'; import { useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import { AwaitableComponent } from 'awaitable-component'; +import ColorLensIcon from '@mui/icons-material/ColorLens'; import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; import { MangaType } from '@/lib/graphql/generated/graphql.ts'; import { AppRoutes } from '@/base/AppRoute.constants.ts'; import { CategorySelect } from '@/features/category/components/CategorySelect.tsx'; +import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts'; +import { useAppThemeContext } from '@/features/theme/AppThemeContext.tsx'; +import { createAppColorTheme } from '@/features/theme/services/ThemeCreator.ts'; +import { getTheme } from '@/features/theme/services/AppThemes.ts'; +import { ThemeCreationDialog } from '@/features/theme/components/CreateThemeDialog.tsx'; +import { MediaQuery } from '@/base/utils/MediaQuery.tsx'; interface IProps { manga: Pick; @@ -37,6 +44,8 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => { const theme = useTheme(); const isLargeScreen = useMediaQuery(theme.breakpoints.up('sm')); + const { settings } = useMetadataServerSettings(); + const { dynamicColor, appTheme, shouldUsePureBlackMode, themeMode } = useAppThemeContext(); const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); @@ -48,10 +57,37 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => { AwaitableComponent.show(CategorySelect, { mangaId: manga.id }); }; + const saveDynamicColorTheme = () => { + AwaitableComponent.show(ThemeCreationDialog, { + mode: 'create', + appTheme: { + id: '', + getName: () => '', + isCustom: true, + muiTheme: createAppColorTheme( + getTheme(appTheme, settings.customThemes).muiTheme, + dynamicColor, + shouldUsePureBlackMode, + MediaQuery.getThemeMode(themeMode), + ), + }, + }); + }; + return ( <> {isLargeScreen && ( <> + {settings.mangaDynamicColorSchemes && ( + + + + + + )} { @@ -127,6 +163,14 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => { {t('manga.label.reload_from_source')} + {settings.mangaDynamicColorSchemes && ( + + + + + {t('settings.appearance.manga_dynamic_color_schemes.save')} + + )} {manga.inLibrary && [ ((e) => { throw e; @@ -117,6 +127,7 @@ export const ThemeCreationDialog = ({ const [invalidTheme, setInvalidTheme] = useState(false); const [isCreating, setIsCreating] = useState(false); const [didThemeChange, setDidThemeChange] = useState(mode === 'create'); + const [setAsActiveTheme, setSetAsActiveTheme] = useState(false); return ( @@ -214,36 +225,55 @@ export const ThemeCreationDialog = ({ )} - - + + + setIsCreating(true); + updateCustomThemes('customThemes', { ...customThemes, [theme.id]: theme }) + .then(() => { + makeToast( + t(dialogModeToTranslationKey[mode].success, { theme: theme.getName() }), + 'success', + ); + + if (setAsActiveTheme) { + setAppTheme(theme.getName()); + } + + onSubmit(); + }) + .catch((updateError) => + makeToast( + t(dialogModeToTranslationKey[mode].failure, { theme: theme.getName() }), + 'error', + getErrorMessage(updateError), + ), + ) + .finally(() => setIsCreating(false)); + }} + color="primary" + > + {t(dialogModeToTranslationKey[mode].button)} + + + ); diff --git a/src/features/theme/services/ThemeCreator.ts b/src/features/theme/services/ThemeCreator.ts index b1c5d625..bc240588 100644 --- a/src/features/theme/services/ThemeCreator.ts +++ b/src/features/theme/services/ThemeCreator.ts @@ -19,13 +19,13 @@ import { useCallback } from 'react'; import { deepmerge } from '@mui/utils'; import { PaletteBackgroundChannel } from 'node_modules/@mui/material/esm/styles/createThemeWithVars'; import { complement, hsl, parseToHsl } from 'polished'; -import { MediaQuery } from '@/base/utils/MediaQuery.tsx'; import { AppTheme } from '@/features/theme/services/AppThemes.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { applyStyles } from '@/base/utils/ApplyStyles.ts'; import { TAppThemeContext, ThemeMode } from '@/features/theme/AppTheme.types.ts'; import { ThemeFontLoader } from '@/features/theme/services/ThemeFontLoader.ts'; import { coerceIn } from '@/lib/HelperFunctions.ts'; +import { MediaQuery } from '@/base/utils/MediaQuery.tsx'; const SCROLLBAR_SIZE = 14; @@ -108,7 +108,7 @@ const getVibrantColorForTheme = ( return mode === ThemeMode.LIGHT ? palette.DarkVibrant.hex : palette.LightVibrant.hex; }; -const createAppColorTheme = ( +export const createAppColorTheme = ( appTheme: AppTheme['muiTheme'], dynamicColor: TAppThemeContext['dynamicColor'], setPureBlackMode: boolean, @@ -163,9 +163,7 @@ export const createTheme = ( direction: Direction = 'ltr', dynamicColor: TAppThemeContext['dynamicColor'] = null, ) => { - const systemMode = MediaQuery.getSystemThemeMode(); - - const mode = themeMode === ThemeMode.SYSTEM ? systemMode : themeMode; + const mode = MediaQuery.getThemeMode(themeMode); const isDarkMode = mode === ThemeMode.DARK; const setPureBlackMode = isDarkMode && pureBlackMode;