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:
parent
6add573df6
commit
44678a61c2
22 changed files with 353 additions and 262 deletions
|
@ -1,37 +1,53 @@
|
||||||
import loadable from '@loadable/component';
|
import loadable from '@loadable/component';
|
||||||
|
import { ThemeProvider } from '@mui/material/styles';
|
||||||
import { History } from '@remix-run/router';
|
import { History } from '@remix-run/router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
|
|
||||||
import StableApp from './apps/stable/App';
|
import StableApp from 'apps/stable/App';
|
||||||
import { HistoryRouter } from './components/router/HistoryRouter';
|
import AppHeader from 'components/AppHeader';
|
||||||
import { ApiProvider } from './hooks/useApi';
|
import Backdrop from 'components/Backdrop';
|
||||||
import { WebConfigProvider } from './hooks/useWebConfig';
|
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 ExperimentalApp = loadable(() => import('./apps/experimental/App'));
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const RootApp = ({ history }: { history: History }) => {
|
const RootAppLayout = () => {
|
||||||
const layoutMode = localStorage.getItem('layout');
|
const layoutMode = localStorage.getItem('layout');
|
||||||
|
const isExperimentalLayout = layoutMode === 'experimental';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<>
|
||||||
<ApiProvider>
|
<Backdrop />
|
||||||
<WebConfigProvider>
|
<AppHeader isHidden={isExperimentalLayout} />
|
||||||
<HistoryRouter history={history}>
|
|
||||||
{
|
{
|
||||||
layoutMode === 'experimental' ?
|
isExperimentalLayout ?
|
||||||
<ExperimentalApp /> :
|
<ExperimentalApp /> :
|
||||||
<StableApp />
|
<StableApp />
|
||||||
}
|
}
|
||||||
</HistoryRouter>
|
</>
|
||||||
</WebConfigProvider>
|
|
||||||
</ApiProvider>
|
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
|
||||||
</QueryClientProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
export default RootApp;
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { REDIRECTS } from 'apps/stable/routes/_redirects';
|
||||||
import ConnectionRequired from 'components/ConnectionRequired';
|
import ConnectionRequired from 'components/ConnectionRequired';
|
||||||
import ServerContentPage from 'components/ServerContentPage';
|
import ServerContentPage from 'components/ServerContentPage';
|
||||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||||
|
import { toRedirectRoute } from 'components/router/Redirect';
|
||||||
|
|
||||||
import AppLayout from './AppLayout';
|
import AppLayout from './AppLayout';
|
||||||
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||||
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
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 = () => {
|
const ExperimentalApp = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -38,10 +38,10 @@ const ExperimentalApp = () => {
|
||||||
|
|
||||||
{LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)}
|
{LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)}
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Redirects for old paths */}
|
|
||||||
{REDIRECTS.map(toRedirectRoute)}
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
{/* Redirects for old paths */}
|
||||||
|
{REDIRECTS.map(toRedirectRoute)}
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import AppBar from '@mui/material/AppBar';
|
import AppBar from '@mui/material/AppBar';
|
||||||
import Box from '@mui/material/Box';
|
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 { Outlet, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import AppHeader from 'components/AppHeader';
|
import AppBody from 'components/AppBody';
|
||||||
import Backdrop from 'components/Backdrop';
|
import ElevationScroll from 'components/ElevationScroll';
|
||||||
|
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||||
|
|
||||||
import AppToolbar from './components/AppToolbar';
|
import AppToolbar from './components/AppToolbar';
|
||||||
import AppDrawer, { DRAWER_WIDTH, isDrawerPath } from './components/drawers/AppDrawer';
|
import AppDrawer, { isDrawerPath } from './components/drawers/AppDrawer';
|
||||||
import ElevationScroll from './components/ElevationScroll';
|
|
||||||
import theme from './theme';
|
|
||||||
|
|
||||||
import './AppOverrides.scss';
|
import './AppOverrides.scss';
|
||||||
|
|
||||||
|
@ -29,6 +28,7 @@ const AppLayout = () => {
|
||||||
const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned);
|
const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned);
|
||||||
const { user } = useApi();
|
const { user } = useApi();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
const isDrawerAvailable = isDrawerPath(location.pathname);
|
const isDrawerAvailable = isDrawerPath(location.pathname);
|
||||||
const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user);
|
const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user);
|
||||||
|
@ -47,67 +47,54 @@ const AppLayout = () => {
|
||||||
}, [ isDrawerActive, setIsDrawerActive ]);
|
}, [ isDrawerActive, setIsDrawerActive ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
<Backdrop />
|
<ElevationScroll elevate={isDrawerOpen}>
|
||||||
|
<AppBar
|
||||||
<div style={{ display: 'none' }}>
|
position='fixed'
|
||||||
{/*
|
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.drawer + 1 }}
|
||||||
* 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
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className='mainAnimatedPages skinBody' />
|
<AppToolbar
|
||||||
<div className='skinBody'>
|
isDrawerOpen={isDrawerOpen}
|
||||||
<Outlet />
|
onDrawerButtonClick={onToggleDrawer}
|
||||||
</div>
|
/>
|
||||||
</Box>
|
</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>
|
</Box>
|
||||||
</ThemeProvider>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 SearchIcon from '@mui/icons-material/Search';
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import appIcon from 'assets/img/icon-transparent.png';
|
import AppToolbar from 'components/toolbar/AppToolbar';
|
||||||
import { appRouter } from 'components/router/appRouter';
|
|
||||||
import { useApi } from 'hooks/useApi';
|
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import AppTabs from '../tabs/AppTabs';
|
import AppTabs from '../tabs/AppTabs';
|
||||||
import { isDrawerPath } from '../drawers/AppDrawer';
|
import { isDrawerPath } from '../drawers/AppDrawer';
|
||||||
import UserMenuButton from './UserMenuButton';
|
|
||||||
import RemotePlayButton from './RemotePlayButton';
|
import RemotePlayButton from './RemotePlayButton';
|
||||||
import SyncPlayButton from './SyncPlayButton';
|
import SyncPlayButton from './SyncPlayButton';
|
||||||
|
|
||||||
|
@ -25,120 +17,40 @@ interface AppToolbarProps {
|
||||||
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const onBackButtonClick = () => {
|
const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
|
||||||
appRouter.back()
|
|
||||||
.catch(err => {
|
|
||||||
console.error('[AppToolbar] error calling appRouter.back', err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppToolbar: FC<AppToolbarProps> = ({
|
|
||||||
isDrawerOpen,
|
isDrawerOpen,
|
||||||
onDrawerButtonClick
|
onDrawerButtonClick
|
||||||
}) => {
|
}) => {
|
||||||
const { user } = useApi();
|
|
||||||
const isUserLoggedIn = Boolean(user);
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const isDrawerAvailable = isDrawerPath(location.pathname);
|
const isDrawerAvailable = isDrawerPath(location.pathname);
|
||||||
const isBackButtonAvailable = appRouter.canGoBack();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toolbar
|
<AppToolbar
|
||||||
variant='dense'
|
buttons={
|
||||||
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 && (
|
|
||||||
<>
|
<>
|
||||||
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'flex-end' }}>
|
<SyncPlayButton />
|
||||||
<SyncPlayButton />
|
<RemotePlayButton />
|
||||||
<RemotePlayButton />
|
|
||||||
|
|
||||||
<Tooltip title={globalize.translate('Search')}>
|
<Tooltip title={globalize.translate('Search')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size='large'
|
size='large'
|
||||||
aria-label={globalize.translate('Search')}
|
aria-label={globalize.translate('Search')}
|
||||||
color='inherit'
|
color='inherit'
|
||||||
component={Link}
|
component={Link}
|
||||||
to='/search.html'
|
to='/search.html'
|
||||||
>
|
>
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 0 }}>
|
|
||||||
<UserMenuButton />
|
|
||||||
</Box>
|
|
||||||
</>
|
</>
|
||||||
)}
|
}
|
||||||
</Toolbar>
|
isDrawerAvailable={isDrawerAvailable}
|
||||||
|
isDrawerOpen={isDrawerOpen}
|
||||||
|
onDrawerButtonClick={onDrawerButtonClick}
|
||||||
|
>
|
||||||
|
<AppTabs isDrawerOpen={isDrawerOpen} />
|
||||||
|
</AppToolbar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AppToolbar;
|
export default ExperimentalAppToolbar;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import React, { FC } from 'react';
|
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 { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from '../../routes/asyncRoutes';
|
||||||
import { LEGACY_ADMIN_ROUTES, LEGACY_USER_ROUTES } from '../../routes/legacyRoutes';
|
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 PluginDrawerSection from './dashboard/PluginDrawerSection';
|
||||||
import ServerDrawerSection from './dashboard/ServerDrawerSection';
|
import ServerDrawerSection from './dashboard/ServerDrawerSection';
|
||||||
import MainDrawerContent from './MainDrawerContent';
|
import MainDrawerContent from './MainDrawerContent';
|
||||||
import ResponsiveDrawer, { ResponsiveDrawerProps } from './ResponsiveDrawer';
|
import { isTabPath } from '../tabs/tabRoutes';
|
||||||
|
|
||||||
export const DRAWER_WIDTH = 240;
|
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)
|
|| 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> = ({
|
const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
||||||
open = false,
|
open = false,
|
||||||
onClose,
|
onClose,
|
||||||
|
@ -48,13 +64,13 @@ const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
||||||
key={route.path}
|
key={route.path}
|
||||||
path={route.path}
|
path={route.path}
|
||||||
element={
|
element={
|
||||||
<ResponsiveDrawer
|
<Drawer
|
||||||
open={open}
|
open={open}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onOpen={onOpen}
|
onOpen={onOpen}
|
||||||
>
|
>
|
||||||
<MainDrawerContent />
|
<MainDrawerContent />
|
||||||
</ResponsiveDrawer>
|
</Drawer>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
@ -65,7 +81,7 @@ const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
||||||
key={route.path}
|
key={route.path}
|
||||||
path={route.path}
|
path={route.path}
|
||||||
element={
|
element={
|
||||||
<ResponsiveDrawer
|
<Drawer
|
||||||
open={open}
|
open={open}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onOpen={onOpen}
|
onOpen={onOpen}
|
||||||
|
@ -75,7 +91,7 @@ const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
||||||
<LiveTvDrawerSection />
|
<LiveTvDrawerSection />
|
||||||
<AdvancedDrawerSection />
|
<AdvancedDrawerSection />
|
||||||
<PluginDrawerSection />
|
<PluginDrawerSection />
|
||||||
</ResponsiveDrawer>
|
</Drawer>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
|
|
@ -17,12 +17,12 @@ import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ListItemLink from 'components/ListItemLink';
|
||||||
|
import { appRouter } from 'components/router/appRouter';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
import { useWebConfig } from 'hooks/useWebConfig';
|
import { useWebConfig } from 'hooks/useWebConfig';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
import { appRouter } from 'components/router/appRouter';
|
|
||||||
|
|
||||||
import ListItemLink from './ListItemLink';
|
|
||||||
import LibraryIcon from '../LibraryIcon';
|
import LibraryIcon from '../LibraryIcon';
|
||||||
|
|
||||||
const MainDrawerContent = () => {
|
const MainDrawerContent = () => {
|
||||||
|
|
|
@ -15,10 +15,9 @@ import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ListItemLink from 'components/ListItemLink';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import ListItemLink from '../ListItemLink';
|
|
||||||
|
|
||||||
const PLUGIN_PATHS = [
|
const PLUGIN_PATHS = [
|
||||||
'/installedplugins.html',
|
'/installedplugins.html',
|
||||||
'/availableplugins.html',
|
'/availableplugins.html',
|
||||||
|
|
|
@ -8,10 +8,9 @@ import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ListItemLink from 'components/ListItemLink';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import ListItemLink from '../ListItemLink';
|
|
||||||
|
|
||||||
const DLNA_PATHS = [
|
const DLNA_PATHS = [
|
||||||
'/dlnasettings.html',
|
'/dlnasettings.html',
|
||||||
'/dlnaprofiles.html'
|
'/dlnaprofiles.html'
|
||||||
|
|
|
@ -6,10 +6,9 @@ import ListItemText from '@mui/material/ListItemText';
|
||||||
import ListSubheader from '@mui/material/ListSubheader';
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import ListItemLink from 'components/ListItemLink';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import ListItemLink from '../ListItemLink';
|
|
||||||
|
|
||||||
const LiveTvDrawerSection = () => {
|
const LiveTvDrawerSection = () => {
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
|
|
|
@ -8,12 +8,11 @@ import ListItemText from '@mui/material/ListItemText';
|
||||||
import ListSubheader from '@mui/material/ListSubheader';
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import ListItemLink from 'components/ListItemLink';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
import Dashboard from 'utils/dashboard';
|
import Dashboard from 'utils/dashboard';
|
||||||
|
|
||||||
import ListItemLink from '../ListItemLink';
|
|
||||||
|
|
||||||
const PluginDrawerSection = () => {
|
const PluginDrawerSection = () => {
|
||||||
const { api } = useApi();
|
const { api } = useApi();
|
||||||
const [ pagesInfo, setPagesInfo ] = useState<ConfigurationPageInfo[]>([]);
|
const [ pagesInfo, setPagesInfo ] = useState<ConfigurationPageInfo[]>([]);
|
||||||
|
|
|
@ -8,10 +8,9 @@ import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ListItemLink from 'components/ListItemLink';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import ListItemLink from '../ListItemLink';
|
|
||||||
|
|
||||||
const LIBRARY_PATHS = [
|
const LIBRARY_PATHS = [
|
||||||
'/library.html',
|
'/library.html',
|
||||||
'/librarydisplay.html',
|
'/librarydisplay.html',
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
|
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
import AppHeader from 'components/AppHeader';
|
import AppBody from 'components/AppBody';
|
||||||
import Backdrop from 'components/Backdrop';
|
|
||||||
import ServerContentPage from 'components/ServerContentPage';
|
import ServerContentPage from 'components/ServerContentPage';
|
||||||
import ConnectionRequired from 'components/ConnectionRequired';
|
import ConnectionRequired from 'components/ConnectionRequired';
|
||||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||||
|
@ -14,15 +13,9 @@ import { REDIRECTS } from './routes/_redirects';
|
||||||
import { toRedirectRoute } from 'components/router/Redirect';
|
import { toRedirectRoute } from 'components/router/Redirect';
|
||||||
|
|
||||||
const Layout = () => (
|
const Layout = () => (
|
||||||
<>
|
<AppBody>
|
||||||
<Backdrop />
|
<Outlet />
|
||||||
<AppHeader />
|
</AppBody>
|
||||||
|
|
||||||
<div className='mainAnimatedPages skinBody' />
|
|
||||||
<div className='skinBody'>
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const StableApp = () => (
|
const StableApp = () => (
|
||||||
|
@ -53,10 +46,10 @@ const StableApp = () => (
|
||||||
|
|
||||||
{/* Suppress warnings for unhandled routes */}
|
{/* Suppress warnings for unhandled routes */}
|
||||||
<Route path='*' element={null} />
|
<Route path='*' element={null} />
|
||||||
|
|
||||||
{/* Redirects for old paths */}
|
|
||||||
{REDIRECTS.map(toRedirectRoute)}
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
{/* Redirects for old paths */}
|
||||||
|
{REDIRECTS.map(toRedirectRoute)}
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
24
src/components/AppBody.tsx
Normal file
24
src/components/AppBody.tsx
Normal 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;
|
|
@ -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(() => {
|
useEffect(() => {
|
||||||
// Initialize the UI components after first render
|
// Initialize the UI components after first render
|
||||||
import('../scripts/libraryMenu');
|
import('../scripts/libraryMenu');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
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 hide'>
|
||||||
<div className='mainDrawer-scrollContainer scrollContainer focuscontainer-y' />
|
<div className='mainDrawer-scrollContainer scrollContainer focuscontainer-y' />
|
||||||
</div>
|
</div>
|
||||||
<div className='skinHeader focuscontainer-x' />
|
<div className='skinHeader focuscontainer-x' />
|
||||||
<div className='mainDrawerHandle' />
|
<div className='mainDrawerHandle' />
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,14 +5,13 @@ import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
|
|
||||||
import browser from 'scripts/browser';
|
import browser from 'scripts/browser';
|
||||||
|
|
||||||
import { DRAWER_WIDTH } from './AppDrawer';
|
export const DRAWER_WIDTH = 240;
|
||||||
import { isTabPath } from '../tabs/tabRoutes';
|
|
||||||
|
|
||||||
export interface ResponsiveDrawerProps {
|
export interface ResponsiveDrawerProps {
|
||||||
|
hasSecondaryToolBar?: boolean
|
||||||
open: boolean
|
open: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onOpen: () => void
|
onOpen: () => void
|
||||||
|
@ -20,18 +19,17 @@ export interface ResponsiveDrawerProps {
|
||||||
|
|
||||||
const ResponsiveDrawer: FC<ResponsiveDrawerProps> = ({
|
const ResponsiveDrawer: FC<ResponsiveDrawerProps> = ({
|
||||||
children,
|
children,
|
||||||
|
hasSecondaryToolBar = false,
|
||||||
open = false,
|
open = false,
|
||||||
onClose,
|
onClose,
|
||||||
onOpen
|
onOpen
|
||||||
}) => {
|
}) => {
|
||||||
const location = useLocation();
|
|
||||||
const isSmallScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
|
const isSmallScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
|
||||||
const isLargeScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('lg'));
|
const isLargeScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('lg'));
|
||||||
const isTallToolbar = isTabPath(location.pathname) && !isLargeScreen;
|
|
||||||
|
|
||||||
const getToolbarStyles = useCallback((theme: Theme) => ({
|
const getToolbarStyles = useCallback((theme: Theme) => ({
|
||||||
marginBottom: isTallToolbar ? theme.spacing(6) : 0
|
marginBottom: (hasSecondaryToolBar && !isLargeScreen) ? theme.spacing(6) : 0
|
||||||
}), [ isTallToolbar ]);
|
}), [ hasSecondaryToolBar, isLargeScreen ]);
|
||||||
|
|
||||||
return ( isSmallScreen ? (
|
return ( isSmallScreen ? (
|
||||||
/* DESKTOP DRAWER */
|
/* DESKTOP DRAWER */
|
|
@ -1,4 +1,4 @@
|
||||||
import loadable from '@loadable/component';
|
import loadable, { LoadableComponent } from '@loadable/component';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Route } from 'react-router-dom';
|
import { Route } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -10,13 +10,18 @@ export enum AsyncRouteType {
|
||||||
export interface AsyncRoute {
|
export interface AsyncRoute {
|
||||||
/** The URL path for this route. */
|
/** The URL path for this route. */
|
||||||
path: string
|
path: string
|
||||||
/** The relative path to the page component in the routes directory. */
|
/**
|
||||||
page: string
|
* The relative path to the page component in the routes directory.
|
||||||
/** The route should use the page component from the experimental app. */
|
* 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
|
type?: AsyncRouteType
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AsyncPageProps {
|
export interface AsyncPageProps {
|
||||||
/** The relative path to the page component in the routes directory. */
|
/** The relative path to the page component in the routes directory. */
|
||||||
page: string
|
page: string
|
||||||
}
|
}
|
||||||
|
@ -31,14 +36,19 @@ const StableAsyncPage = loadable(
|
||||||
{ cacheKey: (props: AsyncPageProps) => props.page }
|
{ cacheKey: (props: AsyncPageProps) => props.page }
|
||||||
);
|
);
|
||||||
|
|
||||||
export const toAsyncPageRoute = ({ path, page, type = AsyncRouteType.Stable }: AsyncRoute) => (
|
export const toAsyncPageRoute = ({ path, page, element, type = AsyncRouteType.Stable }: AsyncRoute) => {
|
||||||
<Route
|
const Element = element
|
||||||
key={path}
|
|| (
|
||||||
path={path}
|
|
||||||
element={(
|
|
||||||
type === AsyncRouteType.Experimental ?
|
type === AsyncRouteType.Experimental ?
|
||||||
<ExperimentalAsyncPage page={page} /> :
|
ExperimentalAsyncPage :
|
||||||
<StableAsyncPage page={page} />
|
StableAsyncPage
|
||||||
)}
|
);
|
||||||
/>
|
|
||||||
);
|
return (
|
||||||
|
<Route
|
||||||
|
key={path}
|
||||||
|
path={path}
|
||||||
|
element={<Element page={page ?? path} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
129
src/components/toolbar/AppToolbar.tsx
Normal file
129
src/components/toolbar/AppToolbar.tsx
Normal 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;
|
|
@ -2,11 +2,11 @@ import IconButton from '@mui/material/IconButton';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import UserAvatar from 'components/UserAvatar';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import AppUserMenu, { ID } from './menus/AppUserMenu';
|
import AppUserMenu, { ID } from './AppUserMenu';
|
||||||
import UserAvatar from 'components/UserAvatar';
|
|
||||||
|
|
||||||
const UserMenuButton = () => {
|
const UserMenuButton = () => {
|
||||||
const { user } = useApi();
|
const { user } = useApi();
|
|
@ -1,5 +1,6 @@
|
||||||
import { createTheme } from '@mui/material/styles';
|
import { createTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
|
/** The default Jellyfin app theme for mui */
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
mode: 'dark',
|
mode: 'dark',
|
Loading…
Add table
Add a link
Reference in a new issue