Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for user themes for mui components #5589

Merged
merged 1 commit into from
May 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions src/RootApp.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import loadable from '@loadable/component';
import { ThemeProvider } from '@mui/material/styles';
import { History } from '@remix-run/router';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import React from 'react';

import { ApiProvider } from 'hooks/useApi';
import { WebConfigProvider } from 'hooks/useWebConfig';
import theme from 'themes/theme';
import { queryClient } from 'utils/query/queryClient';

const StableAppRouter = loadable(() => import('./apps/stable/AppRouter'));
Expand All @@ -21,12 +19,10 @@ const RootApp = ({ history }: Readonly<{ history: History }>) => {
<QueryClientProvider client={queryClient}>
<ApiProvider>
<WebConfigProvider>
<ThemeProvider theme={theme}>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The theme provider had to be moved to have access to the useLocation hook to be able to determine if the user is on a dashboard page.

{isExperimentalLayout ?
<RootAppRouter history={history} /> :
<StableAppRouter history={history} />
}
</ThemeProvider>
{isExperimentalLayout ?
<RootAppRouter history={history} /> :
<StableAppRouter history={history} />
}
</WebConfigProvider>
</ApiProvider>
<ReactQueryDevtools initialIsOpen={false} />
Expand Down
5 changes: 3 additions & 2 deletions src/RootAppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import AppHeader from 'components/AppHeader';
import Backdrop from 'components/Backdrop';
import { useLegacyRouterSync } from 'hooks/useLegacyRouterSync';
import { DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
import UserThemeProvider from 'themes/UserThemeProvider';

const router = createHashRouter([
{
Expand All @@ -35,11 +36,11 @@ export default function RootAppRouter({ history }: Readonly<{ history: History}>
*/
function RootAppLayout() {
return (
<>
<UserThemeProvider>
<Backdrop />
<AppHeader isHidden />

<Outlet />
</>
</UserThemeProvider>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import React, { Fragment } from 'react';

import { appHost } from 'components/apphost';
import { useApi } from 'hooks/useApi';
import { useThemes } from 'hooks/useThemes';
import globalize from 'scripts/globalize';

import { DisplaySettingsValues } from './types';
import { useScreensavers } from './hooks/useScreensavers';
import { useServerThemes } from './hooks/useServerThemes';

interface DisplayPreferencesProps {
onChange: (event: SelectChangeEvent | React.SyntheticEvent) => void;
Expand All @@ -25,7 +26,7 @@ interface DisplayPreferencesProps {
export function DisplayPreferences({ onChange, values }: Readonly<DisplayPreferencesProps>) {
const { user } = useApi();
const { screensavers } = useScreensavers();
const { themes } = useServerThemes();
const { themes } = useThemes();

return (
<Stack spacing={3}>
Expand Down

This file was deleted.

5 changes: 3 additions & 2 deletions src/apps/stable/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { STABLE_APP_ROUTES } from './routes/routes';
import Backdrop from 'components/Backdrop';
import AppHeader from 'components/AppHeader';
import { DASHBOARD_APP_PATHS, DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
import UserThemeProvider from 'themes/UserThemeProvider';

const router = createHashRouter([{
element: <StableAppLayout />,
Expand All @@ -32,11 +33,11 @@ function StableAppLayout() {
.some(path => location.pathname.startsWith(`/${path}`));

return (
<>
<UserThemeProvider>
<Backdrop />
<AppHeader isHidden={isNewLayoutPath} />

<Outlet />
</>
</UserThemeProvider>
);
}
16 changes: 16 additions & 0 deletions src/hooks/useThemes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useMemo } from 'react';

import { useWebConfig } from './useWebConfig';

export function useThemes() {
const { themes } = useWebConfig();

const defaultTheme = useMemo(() => {
return themes?.find(theme => theme.default);
}, [ themes ]);

return {
themes: themes || [],
defaultTheme
};
}
57 changes: 57 additions & 0 deletions src/hooks/useUserTheme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useCallback, useEffect, useState } from 'react';

import { currentSettings as userSettings } from 'scripts/settings/userSettings';
import Events from 'utils/events';

import { useApi } from './useApi';
import { useThemes } from './useThemes';

const THEME_FIELD_NAMES = [ 'appTheme', 'dashboardTheme' ];

export function useUserTheme() {
const [ theme, setTheme ] = useState<string>();
const [ dashboardTheme, setDashboardTheme ] = useState<string>();

const { user } = useApi();
const { defaultTheme } = useThemes();

useEffect(() => {
if (defaultTheme) {
if (!theme) setTheme(defaultTheme.id);
if (!dashboardTheme) setDashboardTheme(defaultTheme.id);
}
}, [ dashboardTheme, defaultTheme, theme ]);

// Update the current themes with values from user settings
const updateThemesFromSettings = useCallback(() => {
const userTheme = userSettings.theme();
if (userTheme) setTheme(userTheme);
const userDashboardTheme = userSettings.dashboardTheme();
if (userDashboardTheme) setDashboardTheme(userDashboardTheme);
}, []);

const onUserSettingsChange = useCallback((_e, name?: string) => {
if (name && THEME_FIELD_NAMES.includes(name)) {
updateThemesFromSettings();
}
}, [ updateThemesFromSettings ]);

// Handle user settings changes
useEffect(() => {
Events.on(userSettings, 'change', onUserSettingsChange);

return () => {
Events.off(userSettings, 'change', onUserSettingsChange);
};
}, [ onUserSettingsChange ]);

// Update the theme if the user changes
useEffect(() => {
updateThemesFromSettings();
}, [ updateThemesFromSettings, user ]);

return {
theme,
dashboardTheme
};
}
43 changes: 43 additions & 0 deletions src/themes/UserThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ThemeProvider } from '@mui/material';
import React, { type FC, useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';

import { DASHBOARD_APP_PATHS } from 'apps/dashboard/routes/routes';
import { useUserTheme } from 'hooks/useUserTheme';

import { DEFAULT_THEME, getTheme } from './themes';

const isDashboardThemePage = (pathname: string) => [
// NOTE: The metadata manager doesn't seem to use the dashboard theme
DASHBOARD_APP_PATHS.Dashboard,
DASHBOARD_APP_PATHS.PluginConfig
].some(path => pathname.startsWith(`/${path}`));

const UserThemeProvider: FC = ({ children }) => {
const [ isDashboard, setIsDashboard ] = useState(false);
const [ muiTheme, setMuiTheme ] = useState(DEFAULT_THEME);

const location = useLocation();
const { theme, dashboardTheme } = useUserTheme();

// Check if we are on a dashboard page when the path changes
useEffect(() => {
setIsDashboard(isDashboardThemePage(location.pathname));
}, [ location.pathname ]);

useEffect(() => {
if (isDashboard) {
setMuiTheme(getTheme(dashboardTheme));
} else {
setMuiTheme(getTheme(theme));
}
}, [ dashboardTheme, isDashboard, theme ]);

return (
<ThemeProvider theme={muiTheme}>
{children}
</ThemeProvider>
);
};

export default UserThemeProvider;
27 changes: 27 additions & 0 deletions src/themes/appletv/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import createTheme, { type ThemeOptions } from '@mui/material/styles/createTheme';
import merge from 'lodash-es/merge';

import { DEFAULT_THEME_OPTIONS } from 'themes/defaults';

const themeOptions: ThemeOptions = {
palette: {
mode: 'light',
background: {
default: '#d5e9f2',
paper: '#fff'
}
},
components: {
MuiAppBar: {
styleOverrides: {
colorPrimary: {
backgroundColor: '#bcbcbc'
}
}
}
}
};

const theme = createTheme(merge({}, DEFAULT_THEME_OPTIONS, themeOptions));

export default theme;
16 changes: 16 additions & 0 deletions src/themes/blueradiance/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import createTheme, { type ThemeOptions } from '@mui/material/styles/createTheme';
import merge from 'lodash-es/merge';

import { DEFAULT_THEME_OPTIONS } from 'themes/defaults';

const options: ThemeOptions = {
palette: {
background: {
paper: '#011432'
}
}
};

const theme = createTheme(merge({}, DEFAULT_THEME_OPTIONS, options));

export default theme;
7 changes: 7 additions & 0 deletions src/themes/dark/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import createTheme from '@mui/material/styles/createTheme';

import { DEFAULT_THEME_OPTIONS } from 'themes/defaults';

const theme = createTheme(DEFAULT_THEME_OPTIONS);

export default theme;
19 changes: 3 additions & 16 deletions src/themes/theme.ts → src/themes/defaults.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import { createTheme } from '@mui/material/styles';

declare module '@mui/material/styles' {
interface Palette {
starIcon: Palette['primary'];
}

interface PaletteOptions {
starIcon?: PaletteOptions['primary'];
}
}
import type { ThemeOptions } from '@mui/material/styles/createTheme';

const LIST_ICON_WIDTH = 36;

/** The default Jellyfin app theme for mui */
const theme = createTheme({
export const DEFAULT_THEME_OPTIONS: ThemeOptions = {
palette: {
mode: 'dark',
primary: {
Expand Down Expand Up @@ -109,6 +98,4 @@ const theme = createTheme({
}
}
}
});

export default theme;
};
30 changes: 30 additions & 0 deletions src/themes/light/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import createTheme, { type ThemeOptions } from '@mui/material/styles/createTheme';
import merge from 'lodash-es/merge';

import { DEFAULT_THEME_OPTIONS } from 'themes/defaults';

const options: ThemeOptions = {
palette: {
mode: 'light',
background: {
default: '#f2f2f2',
// NOTE: The original theme uses #303030 for the drawer and app bar but we would need the drawer to use
// dark mode for a color that dark to work properly which would require a separate ThemeProvider just for
// the drawer... which is not worth the trouble in my opinion
paper: '#e8e8e8'
}
},
components: {
MuiAppBar: {
styleOverrides: {
colorPrimary: {
backgroundColor: '#e8e8e8'
}
}
}
}
};

const theme = createTheme(merge({}, DEFAULT_THEME_OPTIONS, options));

export default theme;
22 changes: 22 additions & 0 deletions src/themes/purplehaze/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import createTheme, { type ThemeOptions } from '@mui/material/styles/createTheme';
import merge from 'lodash-es/merge';

import { DEFAULT_THEME_OPTIONS } from 'themes/defaults';

const options: ThemeOptions = {
palette: {
background: {
paper: '#000420'
},
primary: {
main: '#48c3c8'
},
secondary: {
main: '#ff77f1'
}
}
};

const theme = createTheme(merge({}, DEFAULT_THEME_OPTIONS, options));

export default theme;
Loading
Loading