1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Refactor app layouts and common components

This commit is contained in:
Bill Thornton 2023-09-27 02:07:40 -04:00
parent 6add573df6
commit 44678a61c2
22 changed files with 353 additions and 262 deletions

View file

@ -1,37 +1,53 @@
import loadable from '@loadable/component';
import { ThemeProvider } from '@mui/material/styles';
import { History } from '@remix-run/router';
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import StableApp from './apps/stable/App';
import { HistoryRouter } from './components/router/HistoryRouter';
import { ApiProvider } from './hooks/useApi';
import { WebConfigProvider } from './hooks/useWebConfig';
import StableApp from 'apps/stable/App';
import AppHeader from 'components/AppHeader';
import Backdrop from 'components/Backdrop';
import { HistoryRouter } from 'components/router/HistoryRouter';
import { ApiProvider } from 'hooks/useApi';
import { WebConfigProvider } from 'hooks/useWebConfig';
import theme from 'themes/theme';
const ExperimentalApp = loadable(() => import('./apps/experimental/App'));
const queryClient = new QueryClient();
const RootApp = ({ history }: { history: History }) => {
const RootAppLayout = () => {
const layoutMode = localStorage.getItem('layout');
const isExperimentalLayout = layoutMode === 'experimental';
return (
<QueryClientProvider client={queryClient}>
<ApiProvider>
<WebConfigProvider>
<HistoryRouter history={history}>
{
layoutMode === 'experimental' ?
<ExperimentalApp /> :
<StableApp />
}
</HistoryRouter>
</WebConfigProvider>
</ApiProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
<>
<Backdrop />
<AppHeader isHidden={isExperimentalLayout} />
{
isExperimentalLayout ?
<ExperimentalApp /> :
<StableApp />
}
</>
);
};
const RootApp = ({ history }: { history: History }) => (
<QueryClientProvider client={queryClient}>
<ApiProvider>
<WebConfigProvider>
<ThemeProvider theme={theme}>
<HistoryRouter history={history}>
<RootAppLayout />
</HistoryRouter>
</ThemeProvider>
</WebConfigProvider>
</ApiProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
export default RootApp;

View file

@ -1,16 +1,16 @@
import React from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { REDIRECTS } from 'apps/stable/routes/_redirects';
import ConnectionRequired from 'components/ConnectionRequired';
import ServerContentPage from 'components/ServerContentPage';
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
import { toRedirectRoute } from 'components/router/Redirect';
import AppLayout from './AppLayout';
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes';
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
import { REDIRECTS } from 'apps/stable/routes/_redirects';
import { toRedirectRoute } from 'components/router/Redirect';
const ExperimentalApp = () => {
return (
@ -38,10 +38,10 @@ const ExperimentalApp = () => {
{LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)}
</Route>
{/* Redirects for old paths */}
{REDIRECTS.map(toRedirectRoute)}
</Route>
{/* Redirects for old paths */}
{REDIRECTS.map(toRedirectRoute)}
</Routes>
);
};

View file

@ -1,18 +1,17 @@
import React, { useCallback, useEffect, useState } from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import { ThemeProvider } from '@mui/material/styles';
import { useTheme } from '@mui/material/styles';
import { Outlet, useLocation } from 'react-router-dom';
import AppHeader from 'components/AppHeader';
import Backdrop from 'components/Backdrop';
import AppBody from 'components/AppBody';
import ElevationScroll from 'components/ElevationScroll';
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
import { useApi } from 'hooks/useApi';
import { useLocalStorage } from 'hooks/useLocalStorage';
import AppToolbar from './components/AppToolbar';
import AppDrawer, { DRAWER_WIDTH, isDrawerPath } from './components/drawers/AppDrawer';
import ElevationScroll from './components/ElevationScroll';
import theme from './theme';
import AppDrawer, { isDrawerPath } from './components/drawers/AppDrawer';
import './AppOverrides.scss';
@ -29,6 +28,7 @@ const AppLayout = () => {
const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned);
const { user } = useApi();
const location = useLocation();
const theme = useTheme();
const isDrawerAvailable = isDrawerPath(location.pathname);
const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user);
@ -47,67 +47,54 @@ const AppLayout = () => {
}, [ isDrawerActive, setIsDrawerActive ]);
return (
<ThemeProvider theme={theme}>
<Backdrop />
<div style={{ display: 'none' }}>
{/*
* TODO: These components are not used, but views interact with them directly so the need to be
* present in the dom. We add them in a hidden element to prevent errors.
*/}
<AppHeader />
</div>
<Box sx={{ display: 'flex' }}>
<ElevationScroll elevate={isDrawerOpen}>
<AppBar
position='fixed'
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.drawer + 1 }}
>
<AppToolbar
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onToggleDrawer}
/>
</AppBar>
</ElevationScroll>
<AppDrawer
open={isDrawerOpen}
onClose={onToggleDrawer}
onOpen={onToggleDrawer}
/>
<Box
component='main'
sx={{
width: '100%',
flexGrow: 1,
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
marginLeft: 0,
...(isDrawerAvailable && {
marginLeft: {
sm: `-${DRAWER_WIDTH}px`
}
}),
...(isDrawerActive && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen
}),
marginLeft: 0
})
}}
<Box sx={{ display: 'flex' }}>
<ElevationScroll elevate={isDrawerOpen}>
<AppBar
position='fixed'
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.drawer + 1 }}
>
<div className='mainAnimatedPages skinBody' />
<div className='skinBody'>
<Outlet />
</div>
</Box>
<AppToolbar
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onToggleDrawer}
/>
</AppBar>
</ElevationScroll>
<AppDrawer
open={isDrawerOpen}
onClose={onToggleDrawer}
onOpen={onToggleDrawer}
/>
<Box
component='main'
sx={{
width: '100%',
flexGrow: 1,
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
marginLeft: 0,
...(isDrawerAvailable && {
marginLeft: {
sm: `-${DRAWER_WIDTH}px`
}
}),
...(isDrawerActive && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen
}),
marginLeft: 0
})
}}
>
<AppBody>
<Outlet />
</AppBody>
</Box>
</ThemeProvider>
</Box>
);
};

View file

@ -1,22 +1,14 @@
import ArrowBack from '@mui/icons-material/ArrowBack';
import MenuIcon from '@mui/icons-material/Menu';
import SearchIcon from '@mui/icons-material/Search';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import Toolbar from '@mui/material/Toolbar';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import React, { FC } from 'react';
import { Link, useLocation } from 'react-router-dom';
import appIcon from 'assets/img/icon-transparent.png';
import { appRouter } from 'components/router/appRouter';
import { useApi } from 'hooks/useApi';
import AppToolbar from 'components/toolbar/AppToolbar';
import globalize from 'scripts/globalize';
import AppTabs from '../tabs/AppTabs';
import { isDrawerPath } from '../drawers/AppDrawer';
import UserMenuButton from './UserMenuButton';
import RemotePlayButton from './RemotePlayButton';
import SyncPlayButton from './SyncPlayButton';
@ -25,120 +17,40 @@ interface AppToolbarProps {
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
}
const onBackButtonClick = () => {
appRouter.back()
.catch(err => {
console.error('[AppToolbar] error calling appRouter.back', err);
});
};
const AppToolbar: FC<AppToolbarProps> = ({
const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
isDrawerOpen,
onDrawerButtonClick
}) => {
const { user } = useApi();
const isUserLoggedIn = Boolean(user);
const location = useLocation();
const isDrawerAvailable = isDrawerPath(location.pathname);
const isBackButtonAvailable = appRouter.canGoBack();
return (
<Toolbar
variant='dense'
sx={{
flexWrap: {
xs: 'wrap',
lg: 'nowrap'
}
}}
>
{isUserLoggedIn && isDrawerAvailable && (
<Tooltip title={globalize.translate(isDrawerOpen ? 'MenuClose' : 'MenuOpen')}>
<IconButton
size='large'
edge='start'
color='inherit'
aria-label={globalize.translate(isDrawerOpen ? 'MenuClose' : 'MenuOpen')}
onClick={onDrawerButtonClick}
>
<MenuIcon />
</IconButton>
</Tooltip>
)}
{isBackButtonAvailable && (
<Tooltip title={globalize.translate('ButtonBack')}>
<IconButton
size='large'
// Set the edge if the drawer button is not shown
edge={!(isUserLoggedIn && isDrawerAvailable) ? 'start' : undefined}
color='inherit'
aria-label={globalize.translate('ButtonBack')}
onClick={onBackButtonClick}
>
<ArrowBack />
</IconButton>
</Tooltip>
)}
<Box
component={Link}
to='/'
color='inherit'
aria-label={globalize.translate('Home')}
sx={{
ml: 2,
display: 'inline-flex',
textDecoration: 'none'
}}
>
<Box
component='img'
src={appIcon}
sx={{
height: '2rem',
marginInlineEnd: 1
}}
/>
<Typography
variant='h6'
noWrap
component='div'
sx={{ display: { xs: 'none', sm: 'inline-block' } }}
>
Jellyfin
</Typography>
</Box>
<AppTabs isDrawerOpen={isDrawerOpen} />
{isUserLoggedIn && (
<AppToolbar
buttons={
<>
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'flex-end' }}>
<SyncPlayButton />
<RemotePlayButton />
<SyncPlayButton />
<RemotePlayButton />
<Tooltip title={globalize.translate('Search')}>
<IconButton
size='large'
aria-label={globalize.translate('Search')}
color='inherit'
component={Link}
to='/search.html'
>
<SearchIcon />
</IconButton>
</Tooltip>
</Box>
<Box sx={{ flexGrow: 0 }}>
<UserMenuButton />
</Box>
<Tooltip title={globalize.translate('Search')}>
<IconButton
size='large'
aria-label={globalize.translate('Search')}
color='inherit'
component={Link}
to='/search.html'
>
<SearchIcon />
</IconButton>
</Tooltip>
</>
)}
</Toolbar>
}
isDrawerAvailable={isDrawerAvailable}
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onDrawerButtonClick}
>
<AppTabs isDrawerOpen={isDrawerOpen} />
</AppToolbar>
);
};
export default AppToolbar;
export default ExperimentalAppToolbar;

View file

@ -1,5 +1,7 @@
import React, { FC } from 'react';
import { Route, Routes } from 'react-router-dom';
import { Route, Routes, useLocation } from 'react-router-dom';
import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from '../../routes/asyncRoutes';
import { LEGACY_ADMIN_ROUTES, LEGACY_USER_ROUTES } from '../../routes/legacyRoutes';
@ -10,7 +12,7 @@ import LiveTvDrawerSection from './dashboard/LiveTvDrawerSection';
import PluginDrawerSection from './dashboard/PluginDrawerSection';
import ServerDrawerSection from './dashboard/ServerDrawerSection';
import MainDrawerContent from './MainDrawerContent';
import ResponsiveDrawer, { ResponsiveDrawerProps } from './ResponsiveDrawer';
import { isTabPath } from '../tabs/tabRoutes';
export const DRAWER_WIDTH = 240;
@ -36,6 +38,20 @@ export const isDrawerPath = (path: string) => (
|| ADMIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path)
);
const Drawer: FC<ResponsiveDrawerProps> = ({ children, ...props }) => {
const location = useLocation();
const hasSecondaryToolBar = isTabPath(location.pathname);
return (
<ResponsiveDrawer
{...props}
hasSecondaryToolBar={hasSecondaryToolBar}
>
{children}
</ResponsiveDrawer>
);
};
const AppDrawer: FC<ResponsiveDrawerProps> = ({
open = false,
onClose,
@ -48,13 +64,13 @@ const AppDrawer: FC<ResponsiveDrawerProps> = ({
key={route.path}
path={route.path}
element={
<ResponsiveDrawer
<Drawer
open={open}
onClose={onClose}
onOpen={onOpen}
>
<MainDrawerContent />
</ResponsiveDrawer>
</Drawer>
}
/>
))
@ -65,7 +81,7 @@ const AppDrawer: FC<ResponsiveDrawerProps> = ({
key={route.path}
path={route.path}
element={
<ResponsiveDrawer
<Drawer
open={open}
onClose={onClose}
onOpen={onOpen}
@ -75,7 +91,7 @@ const AppDrawer: FC<ResponsiveDrawerProps> = ({
<LiveTvDrawerSection />
<AdvancedDrawerSection />
<PluginDrawerSection />
</ResponsiveDrawer>
</Drawer>
}
/>
))

View file

@ -17,12 +17,12 @@ import ListSubheader from '@mui/material/ListSubheader';
import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import ListItemLink from 'components/ListItemLink';
import { appRouter } from 'components/router/appRouter';
import { useApi } from 'hooks/useApi';
import { useWebConfig } from 'hooks/useWebConfig';
import globalize from 'scripts/globalize';
import { appRouter } from 'components/router/appRouter';
import ListItemLink from './ListItemLink';
import LibraryIcon from '../LibraryIcon';
const MainDrawerContent = () => {

View file

@ -15,10 +15,9 @@ import ListSubheader from '@mui/material/ListSubheader';
import React from 'react';
import { useLocation } from 'react-router-dom';
import ListItemLink from 'components/ListItemLink';
import globalize from 'scripts/globalize';
import ListItemLink from '../ListItemLink';
const PLUGIN_PATHS = [
'/installedplugins.html',
'/availableplugins.html',

View file

@ -8,10 +8,9 @@ import ListSubheader from '@mui/material/ListSubheader';
import React from 'react';
import { useLocation } from 'react-router-dom';
import ListItemLink from 'components/ListItemLink';
import globalize from 'scripts/globalize';
import ListItemLink from '../ListItemLink';
const DLNA_PATHS = [
'/dlnasettings.html',
'/dlnaprofiles.html'

View file

@ -6,10 +6,9 @@ import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import React from 'react';
import ListItemLink from 'components/ListItemLink';
import globalize from 'scripts/globalize';
import ListItemLink from '../ListItemLink';
const LiveTvDrawerSection = () => {
return (
<List

View file

@ -8,12 +8,11 @@ import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import React, { useEffect, useState } from 'react';
import ListItemLink from 'components/ListItemLink';
import { useApi } from 'hooks/useApi';
import globalize from 'scripts/globalize';
import Dashboard from 'utils/dashboard';
import ListItemLink from '../ListItemLink';
const PluginDrawerSection = () => {
const { api } = useApi();
const [ pagesInfo, setPagesInfo ] = useState<ConfigurationPageInfo[]>([]);

View file

@ -8,10 +8,9 @@ import ListSubheader from '@mui/material/ListSubheader';
import React from 'react';
import { useLocation } from 'react-router-dom';
import ListItemLink from 'components/ListItemLink';
import globalize from 'scripts/globalize';
import ListItemLink from '../ListItemLink';
const LIBRARY_PATHS = [
'/library.html',
'/librarydisplay.html',

View file

@ -1,8 +1,7 @@
import React from 'react';
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
import AppHeader from 'components/AppHeader';
import Backdrop from 'components/Backdrop';
import AppBody from 'components/AppBody';
import ServerContentPage from 'components/ServerContentPage';
import ConnectionRequired from 'components/ConnectionRequired';
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
@ -14,15 +13,9 @@ import { REDIRECTS } from './routes/_redirects';
import { toRedirectRoute } from 'components/router/Redirect';
const Layout = () => (
<>
<Backdrop />
<AppHeader />
<div className='mainAnimatedPages skinBody' />
<div className='skinBody'>
<Outlet />
</div>
</>
<AppBody>
<Outlet />
</AppBody>
);
const StableApp = () => (
@ -53,10 +46,10 @@ const StableApp = () => (
{/* Suppress warnings for unhandled routes */}
<Route path='*' element={null} />
{/* Redirects for old paths */}
{REDIRECTS.map(toRedirectRoute)}
</Route>
{/* Redirects for old paths */}
{REDIRECTS.map(toRedirectRoute)}
</Routes>
);

View file

@ -0,0 +1,24 @@
import React, { FC, useEffect } from 'react';
import viewContainer from './viewContainer';
/**
* A simple component that includes the correct structure for ViewManager pages
* to exist alongside standard React pages.
*/
const AppBody: FC = ({ children }) => {
useEffect(() => () => {
// Reset view container state on unload
viewContainer.reset();
}, []);
return (
<>
<div className='mainAnimatedPages skinBody' />
<div className='skinBody'>
{children}
</div>
</>
);
};
export default AppBody;

View file

@ -1,19 +1,29 @@
import React, { useEffect } from 'react';
import React, { FC, useEffect } from 'react';
const AppHeader = () => {
interface AppHeaderParams {
isHidden?: boolean
}
const AppHeader: FC<AppHeaderParams> = ({
isHidden = false
}) => {
useEffect(() => {
// Initialize the UI components after first render
import('../scripts/libraryMenu');
}, []);
return (
<>
/**
* NOTE: These components are not used with the new layouts, but legacy views interact with the elements
* directly so they need to be present in the DOM. We use display: none to hide them and prevent errors.
*/
<div style={isHidden ? { display: 'none' } : undefined}>
<div className='mainDrawer hide'>
<div className='mainDrawer-scrollContainer scrollContainer focuscontainer-y' />
</div>
<div className='skinHeader focuscontainer-x' />
<div className='mainDrawerHandle' />
</>
</div>
);
};

View file

@ -5,14 +5,13 @@ import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import Toolbar from '@mui/material/Toolbar';
import useMediaQuery from '@mui/material/useMediaQuery';
import React, { FC, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import browser from 'scripts/browser';
import { DRAWER_WIDTH } from './AppDrawer';
import { isTabPath } from '../tabs/tabRoutes';
export const DRAWER_WIDTH = 240;
export interface ResponsiveDrawerProps {
hasSecondaryToolBar?: boolean
open: boolean
onClose: () => void
onOpen: () => void
@ -20,18 +19,17 @@ export interface ResponsiveDrawerProps {
const ResponsiveDrawer: FC<ResponsiveDrawerProps> = ({
children,
hasSecondaryToolBar = false,
open = false,
onClose,
onOpen
}) => {
const location = useLocation();
const isSmallScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
const isLargeScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('lg'));
const isTallToolbar = isTabPath(location.pathname) && !isLargeScreen;
const getToolbarStyles = useCallback((theme: Theme) => ({
marginBottom: isTallToolbar ? theme.spacing(6) : 0
}), [ isTallToolbar ]);
marginBottom: (hasSecondaryToolBar && !isLargeScreen) ? theme.spacing(6) : 0
}), [ hasSecondaryToolBar, isLargeScreen ]);
return ( isSmallScreen ? (
/* DESKTOP DRAWER */

View file

@ -1,4 +1,4 @@
import loadable from '@loadable/component';
import loadable, { LoadableComponent } from '@loadable/component';
import React from 'react';
import { Route } from 'react-router-dom';
@ -10,13 +10,18 @@ export enum AsyncRouteType {
export interface AsyncRoute {
/** The URL path for this route. */
path: string
/** The relative path to the page component in the routes directory. */
page: string
/** The route should use the page component from the experimental app. */
/**
* The relative path to the page component in the routes directory.
* Will fallback to using the `path` value if not specified.
*/
page?: string
/** The page element to render. */
element?: LoadableComponent<AsyncPageProps>
/** The page type used to load the correct page element. */
type?: AsyncRouteType
}
interface AsyncPageProps {
export interface AsyncPageProps {
/** The relative path to the page component in the routes directory. */
page: string
}
@ -31,14 +36,19 @@ const StableAsyncPage = loadable(
{ cacheKey: (props: AsyncPageProps) => props.page }
);
export const toAsyncPageRoute = ({ path, page, type = AsyncRouteType.Stable }: AsyncRoute) => (
<Route
key={path}
path={path}
element={(
export const toAsyncPageRoute = ({ path, page, element, type = AsyncRouteType.Stable }: AsyncRoute) => {
const Element = element
|| (
type === AsyncRouteType.Experimental ?
<ExperimentalAsyncPage page={page} /> :
<StableAsyncPage page={page} />
)}
/>
);
ExperimentalAsyncPage :
StableAsyncPage
);
return (
<Route
key={path}
path={path}
element={<Element page={page ?? path} />}
/>
);
};

View file

@ -0,0 +1,129 @@
import ArrowBack from '@mui/icons-material/ArrowBack';
import MenuIcon from '@mui/icons-material/Menu';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import Toolbar from '@mui/material/Toolbar';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import React, { FC, ReactNode } from 'react';
import { Link } from 'react-router-dom';
import appIcon from 'assets/img/icon-transparent.png';
import { appRouter } from 'components/router/appRouter';
import { useApi } from 'hooks/useApi';
import globalize from 'scripts/globalize';
import UserMenuButton from './UserMenuButton';
interface AppToolbarProps {
buttons?: ReactNode
isDrawerAvailable: boolean
isDrawerOpen: boolean
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
}
const onBackButtonClick = () => {
appRouter.back()
.catch(err => {
console.error('[AppToolbar] error calling appRouter.back', err);
});
};
const AppToolbar: FC<AppToolbarProps> = ({
buttons,
children,
isDrawerAvailable,
isDrawerOpen,
onDrawerButtonClick
}) => {
const { user } = useApi();
const isUserLoggedIn = Boolean(user);
const isBackButtonAvailable = appRouter.canGoBack();
return (
<Toolbar
variant='dense'
sx={{
flexWrap: {
xs: 'wrap',
lg: 'nowrap'
}
}}
>
{isUserLoggedIn && isDrawerAvailable && (
<Tooltip title={globalize.translate(isDrawerOpen ? 'MenuClose' : 'MenuOpen')}>
<IconButton
size='large'
edge='start'
color='inherit'
aria-label={globalize.translate(isDrawerOpen ? 'MenuClose' : 'MenuOpen')}
onClick={onDrawerButtonClick}
>
<MenuIcon />
</IconButton>
</Tooltip>
)}
{isBackButtonAvailable && (
<Tooltip title={globalize.translate('ButtonBack')}>
<IconButton
size='large'
// Set the edge if the drawer button is not shown
edge={!(isUserLoggedIn && isDrawerAvailable) ? 'start' : undefined}
color='inherit'
aria-label={globalize.translate('ButtonBack')}
onClick={onBackButtonClick}
>
<ArrowBack />
</IconButton>
</Tooltip>
)}
<Box
component={Link}
to='/'
color='inherit'
aria-label={globalize.translate('Home')}
sx={{
ml: 2,
display: 'inline-flex',
textDecoration: 'none'
}}
>
<Box
component='img'
src={appIcon}
sx={{
height: '2rem',
marginInlineEnd: 1
}}
/>
<Typography
variant='h6'
noWrap
component='div'
sx={{ display: { xs: 'none', sm: 'inline-block' } }}
>
Jellyfin
</Typography>
</Box>
{children}
{isUserLoggedIn && (
<>
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'flex-end' }}>
{buttons}
</Box>
<Box sx={{ flexGrow: 0 }}>
<UserMenuButton />
</Box>
</>
)}
</Toolbar>
);
};
export default AppToolbar;

View file

@ -2,11 +2,11 @@ import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import React, { useCallback, useState } from 'react';
import UserAvatar from 'components/UserAvatar';
import { useApi } from 'hooks/useApi';
import globalize from 'scripts/globalize';
import AppUserMenu, { ID } from './menus/AppUserMenu';
import UserAvatar from 'components/UserAvatar';
import AppUserMenu, { ID } from './AppUserMenu';
const UserMenuButton = () => {
const { user } = useApi();

View file

@ -1,5 +1,6 @@
import { createTheme } from '@mui/material/styles';
/** The default Jellyfin app theme for mui */
const theme = createTheme({
palette: {
mode: 'dark',