mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Add drawer to experimental layout
This commit is contained in:
parent
bf62e7a15d
commit
27776d57fc
15 changed files with 977 additions and 11 deletions
|
@ -1,23 +1,56 @@
|
||||||
import React, { useCallback, 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 { ThemeProvider } from '@mui/material/styles';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import AppHeader from 'components/AppHeader';
|
import AppHeader from 'components/AppHeader';
|
||||||
import Backdrop from 'components/Backdrop';
|
import Backdrop from 'components/Backdrop';
|
||||||
|
import { useApi } from 'hooks/useApi';
|
||||||
|
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||||
|
|
||||||
import AppToolbar from './components/AppToolbar';
|
import AppToolbar from './components/AppToolbar';
|
||||||
import AppUserMenu from './components/AppUserMenu';
|
import AppUserMenu from './components/AppUserMenu';
|
||||||
|
import AppDrawer, { DRAWER_WIDTH, isDrawerPath } from './components/drawers/AppDrawer';
|
||||||
import ElevationScroll from './components/ElevationScroll';
|
import ElevationScroll from './components/ElevationScroll';
|
||||||
import { ExperimentalAppRoutes } from './routes/AppRoutes';
|
import { ExperimentalAppRoutes } from './routes/AppRoutes';
|
||||||
import theme from './theme';
|
import theme from './theme';
|
||||||
|
|
||||||
import './AppOverrides.scss';
|
import './AppOverrides.scss';
|
||||||
|
|
||||||
|
interface ExperimentalAppSettings {
|
||||||
|
isDrawerPinned: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_EXPERIMENTAL_APP_SETTINGS: ExperimentalAppSettings = {
|
||||||
|
isDrawerPinned: false
|
||||||
|
};
|
||||||
|
|
||||||
const ExperimentalApp = () => {
|
const ExperimentalApp = () => {
|
||||||
|
const [ appSettings, setAppSettings ] = useLocalStorage<ExperimentalAppSettings>('ExperimentalAppSettings', DEFAULT_EXPERIMENTAL_APP_SETTINGS);
|
||||||
|
const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned);
|
||||||
|
const { user } = useApi();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isDrawerAvailable = isDrawerPath(location.pathname);
|
||||||
|
const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user);
|
||||||
|
|
||||||
const [ userMenuAnchorEl, setUserMenuAnchorEl ] = useState<null | HTMLElement>(null);
|
const [ userMenuAnchorEl, setUserMenuAnchorEl ] = useState<null | HTMLElement>(null);
|
||||||
const isUserMenuOpen = Boolean(userMenuAnchorEl);
|
const isUserMenuOpen = Boolean(userMenuAnchorEl);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDrawerActive !== appSettings.isDrawerPinned) {
|
||||||
|
setAppSettings({
|
||||||
|
...appSettings,
|
||||||
|
isDrawerPinned: isDrawerActive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [ appSettings, isDrawerActive, setAppSettings ]);
|
||||||
|
|
||||||
|
const onToggleDrawer = useCallback(() => {
|
||||||
|
setIsDrawerActive(!isDrawerActive);
|
||||||
|
}, [ isDrawerActive, setIsDrawerActive ]);
|
||||||
|
|
||||||
const onUserButtonClick = useCallback((event) => {
|
const onUserButtonClick = useCallback((event) => {
|
||||||
setUserMenuAnchorEl(event.currentTarget);
|
setUserMenuAnchorEl(event.currentTarget);
|
||||||
}, [ setUserMenuAnchorEl ]);
|
}, [ setUserMenuAnchorEl ]);
|
||||||
|
@ -39,17 +72,25 @@ const ExperimentalApp = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
<ElevationScroll>
|
<ElevationScroll elevate={isDrawerOpen}>
|
||||||
<AppBar
|
<AppBar
|
||||||
position='fixed'
|
position='fixed'
|
||||||
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.drawer + 1 }}
|
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.drawer + 1 }}
|
||||||
>
|
>
|
||||||
<AppToolbar
|
<AppToolbar
|
||||||
|
isDrawerOpen={isDrawerOpen}
|
||||||
|
onDrawerButtonClick={onToggleDrawer}
|
||||||
onUserButtonClick={onUserButtonClick}
|
onUserButtonClick={onUserButtonClick}
|
||||||
/>
|
/>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
</ElevationScroll>
|
</ElevationScroll>
|
||||||
|
|
||||||
|
<AppDrawer
|
||||||
|
open={isDrawerOpen}
|
||||||
|
onClose={onToggleDrawer}
|
||||||
|
onOpen={onToggleDrawer}
|
||||||
|
/>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
component='main'
|
component='main'
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -59,7 +100,17 @@ const ExperimentalApp = () => {
|
||||||
easing: theme.transitions.easing.sharp,
|
easing: theme.transitions.easing.sharp,
|
||||||
duration: theme.transitions.duration.leavingScreen
|
duration: theme.transitions.duration.leavingScreen
|
||||||
}),
|
}),
|
||||||
marginLeft: 0
|
marginLeft: {
|
||||||
|
xs: 0,
|
||||||
|
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' />
|
<div className='mainAnimatedPages skinBody' />
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import Avatar from '@mui/material/Avatar';
|
import Avatar from '@mui/material/Avatar';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
@ -7,24 +8,32 @@ 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 Typography from '@mui/material/Typography';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import { ID as UserMenuId } from './AppUserMenu';
|
import { ID as UserMenuId } from './AppUserMenu';
|
||||||
import AppTabs from './tabs/AppTabs';
|
import AppTabs from './tabs/AppTabs';
|
||||||
|
import { isDrawerPath } from './drawers/AppDrawer';
|
||||||
|
|
||||||
interface AppToolbarProps {
|
interface AppToolbarProps {
|
||||||
|
isDrawerOpen: boolean
|
||||||
|
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
||||||
onUserButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
onUserButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppToolbar: FC<AppToolbarProps> = ({
|
const AppToolbar: FC<AppToolbarProps> = ({
|
||||||
|
isDrawerOpen,
|
||||||
|
onDrawerButtonClick,
|
||||||
onUserButtonClick
|
onUserButtonClick
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { api, user } = useApi();
|
const { api, user } = useApi();
|
||||||
const isUserLoggedIn = Boolean(user);
|
const isUserLoggedIn = Boolean(user);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isDrawerAvailable = isDrawerPath(location.pathname);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toolbar
|
<Toolbar
|
||||||
|
@ -36,6 +45,21 @@ const AppToolbar: FC<AppToolbarProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{isUserLoggedIn && isDrawerAvailable && (
|
||||||
|
<Tooltip title={globalize.translate(isDrawerOpen ? 'MenuClose' : 'MenuOpen')}>
|
||||||
|
<IconButton
|
||||||
|
size='large'
|
||||||
|
edge='start'
|
||||||
|
color='inherit'
|
||||||
|
aria-label={globalize.translate(isDrawerOpen ? 'MenuClose' : 'MenuOpen')}
|
||||||
|
sx={{ mr: 2 }}
|
||||||
|
onClick={onDrawerButtonClick}
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
component={Link}
|
component={Link}
|
||||||
to='/'
|
to='/'
|
||||||
|
@ -64,7 +88,7 @@ const AppToolbar: FC<AppToolbarProps> = ({
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<AppTabs />
|
<AppTabs isDrawerOpen={isDrawerOpen} />
|
||||||
|
|
||||||
{isUserLoggedIn && (
|
{isUserLoggedIn && (
|
||||||
<>
|
<>
|
||||||
|
|
50
src/apps/experimental/components/LibraryIcon.tsx
Normal file
50
src/apps/experimental/components/LibraryIcon.tsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import Movie from '@mui/icons-material/Movie';
|
||||||
|
import MusicNote from '@mui/icons-material/MusicNote';
|
||||||
|
import Photo from '@mui/icons-material/Photo';
|
||||||
|
import LiveTv from '@mui/icons-material/LiveTv';
|
||||||
|
import Tv from '@mui/icons-material/Tv';
|
||||||
|
import Theaters from '@mui/icons-material/Theaters';
|
||||||
|
import MusicVideo from '@mui/icons-material/MusicVideo';
|
||||||
|
import Book from '@mui/icons-material/Book';
|
||||||
|
import Collections from '@mui/icons-material/Collections';
|
||||||
|
import Queue from '@mui/icons-material/Queue';
|
||||||
|
import Folder from '@mui/icons-material/Folder';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { CollectionType } from 'types/collectionType';
|
||||||
|
|
||||||
|
interface LibraryIconProps {
|
||||||
|
item: BaseItemDto
|
||||||
|
}
|
||||||
|
|
||||||
|
const LibraryIcon: FC<LibraryIconProps> = ({
|
||||||
|
item
|
||||||
|
}) => {
|
||||||
|
switch (item.CollectionType) {
|
||||||
|
case CollectionType.Movies:
|
||||||
|
return <Movie />;
|
||||||
|
case CollectionType.Music:
|
||||||
|
return <MusicNote />;
|
||||||
|
case CollectionType.HomeVideos:
|
||||||
|
case CollectionType.Photos:
|
||||||
|
return <Photo />;
|
||||||
|
case CollectionType.LiveTv:
|
||||||
|
return <LiveTv />;
|
||||||
|
case CollectionType.TvShows:
|
||||||
|
return <Tv />;
|
||||||
|
case CollectionType.Trailers:
|
||||||
|
return <Theaters />;
|
||||||
|
case CollectionType.MusicVideos:
|
||||||
|
return <MusicVideo />;
|
||||||
|
case CollectionType.Books:
|
||||||
|
return <Book />;
|
||||||
|
case CollectionType.BoxSets:
|
||||||
|
return <Collections />;
|
||||||
|
case CollectionType.Playlists:
|
||||||
|
return <Queue />;
|
||||||
|
default:
|
||||||
|
return <Folder />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LibraryIcon;
|
89
src/apps/experimental/components/drawers/AppDrawer.tsx
Normal file
89
src/apps/experimental/components/drawers/AppDrawer.tsx
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from '../../routes/asyncRoutes';
|
||||||
|
import { LEGACY_ADMIN_ROUTES, LEGACY_USER_ROUTES } from '../../routes/legacyRoutes';
|
||||||
|
|
||||||
|
import AdvancedDrawerSection from './dashboard/AdvancedDrawerSection';
|
||||||
|
import DevicesDrawerSection from './dashboard/DevicesDrawerSection';
|
||||||
|
import LiveTvDrawerSection from './dashboard/LiveTvDrawerSection';
|
||||||
|
import PluginDrawerSection from './dashboard/PluginDrawerSection';
|
||||||
|
import ServerDrawerSection from './dashboard/ServerDrawerSection';
|
||||||
|
import MainDrawerContent from './MainDrawerContent';
|
||||||
|
import ResponsiveDrawer, { ResponsiveDrawerProps } from './ResponsiveDrawer';
|
||||||
|
|
||||||
|
export const DRAWER_WIDTH = 240;
|
||||||
|
|
||||||
|
const DRAWERLESS_ROUTES = [
|
||||||
|
'edititemmetadata.html', // metadata manager
|
||||||
|
'video' // video player
|
||||||
|
];
|
||||||
|
|
||||||
|
const MAIN_DRAWER_ROUTES = [
|
||||||
|
...ASYNC_USER_ROUTES,
|
||||||
|
...LEGACY_USER_ROUTES
|
||||||
|
].filter(route => !DRAWERLESS_ROUTES.includes(route.path));
|
||||||
|
|
||||||
|
const ADMIN_DRAWER_ROUTES = [
|
||||||
|
...ASYNC_ADMIN_ROUTES,
|
||||||
|
...LEGACY_ADMIN_ROUTES,
|
||||||
|
{ path: '/configurationpage' } // Plugin configuration page
|
||||||
|
].filter(route => !DRAWERLESS_ROUTES.includes(route.path));
|
||||||
|
|
||||||
|
/** Utility function to check if a path has a drawer. */
|
||||||
|
export const isDrawerPath = (path: string) => (
|
||||||
|
MAIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path)
|
||||||
|
|| ADMIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path)
|
||||||
|
);
|
||||||
|
|
||||||
|
const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
||||||
|
open = false,
|
||||||
|
onClose,
|
||||||
|
onOpen
|
||||||
|
}) => (
|
||||||
|
<Routes>
|
||||||
|
{
|
||||||
|
MAIN_DRAWER_ROUTES.map(route => (
|
||||||
|
<Route
|
||||||
|
key={route.path}
|
||||||
|
path={route.path}
|
||||||
|
element={
|
||||||
|
<ResponsiveDrawer
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
onOpen={onOpen}
|
||||||
|
>
|
||||||
|
<MainDrawerContent />
|
||||||
|
</ResponsiveDrawer>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ADMIN_DRAWER_ROUTES.map(route => (
|
||||||
|
<Route
|
||||||
|
key={route.path}
|
||||||
|
path={route.path}
|
||||||
|
element={
|
||||||
|
<ResponsiveDrawer
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
onOpen={onOpen}
|
||||||
|
>
|
||||||
|
<ServerDrawerSection />
|
||||||
|
<DevicesDrawerSection />
|
||||||
|
<LiveTvDrawerSection />
|
||||||
|
<AdvancedDrawerSection />
|
||||||
|
<PluginDrawerSection />
|
||||||
|
</ResponsiveDrawer>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
{/* Suppress warnings for unhandled routes */}
|
||||||
|
<Route path='*' element={null} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AppDrawer;
|
45
src/apps/experimental/components/drawers/ListItemLink.tsx
Normal file
45
src/apps/experimental/components/drawers/ListItemLink.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import ListItemButton, { ListItemButtonBaseProps } from '@mui/material/ListItemButton';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Link, useLocation, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface ListItemLinkProps extends ListItemButtonBaseProps {
|
||||||
|
to: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatchingParams = (routeParams: URLSearchParams, currentParams: URLSearchParams) => {
|
||||||
|
for (const param of routeParams) {
|
||||||
|
if (currentParams.get(param[0]) !== param[1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListItemLink: FC<ListItemLinkProps> = ({
|
||||||
|
children,
|
||||||
|
to,
|
||||||
|
...params
|
||||||
|
}) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const [ searchParams ] = useSearchParams();
|
||||||
|
|
||||||
|
const [ toPath, toParams ] = to.split('?');
|
||||||
|
// eslint-disable-next-line compat/compat
|
||||||
|
const toSearchParams = new URLSearchParams(`?${toParams}`);
|
||||||
|
|
||||||
|
const selected = location.pathname === toPath && (!toParams || isMatchingParams(toSearchParams, searchParams));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItemButton
|
||||||
|
component={Link}
|
||||||
|
to={to}
|
||||||
|
selected={selected}
|
||||||
|
{...params}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ListItemButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListItemLink;
|
186
src/apps/experimental/components/drawers/MainDrawerContent.tsx
Normal file
186
src/apps/experimental/components/drawers/MainDrawerContent.tsx
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||||
|
import type { SystemInfo } from '@jellyfin/sdk/lib/generated-client/models/system-info';
|
||||||
|
import { getUserViewsApi } from '@jellyfin/sdk/lib/utils/api/user-views-api';
|
||||||
|
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
|
||||||
|
import Dashboard from '@mui/icons-material/Dashboard';
|
||||||
|
import Edit from '@mui/icons-material/Edit';
|
||||||
|
import Favorite from '@mui/icons-material/Favorite';
|
||||||
|
import Home from '@mui/icons-material/Home';
|
||||||
|
import Link from '@mui/icons-material/Link';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
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 = () => {
|
||||||
|
const { api, user } = useApi();
|
||||||
|
const location = useLocation();
|
||||||
|
const [ systemInfo, setSystemInfo ] = useState<SystemInfo>();
|
||||||
|
const [ userViews, setUserViews ] = useState<BaseItemDto[]>([]);
|
||||||
|
const webConfig = useWebConfig();
|
||||||
|
|
||||||
|
const isHomeSelected = location.pathname === '/home.html' && (!location.search || location.search === '?tab=0');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (api && user?.Id) {
|
||||||
|
getUserViewsApi(api)
|
||||||
|
.getUserViews({ userId: user.Id })
|
||||||
|
.then(({ data }) => {
|
||||||
|
setUserViews(data.Items || []);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.warn('[MainDrawer] failed to fetch user views', err);
|
||||||
|
setUserViews([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
getSystemApi(api)
|
||||||
|
.getSystemInfo()
|
||||||
|
.then(({ data }) => {
|
||||||
|
setSystemInfo(data);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.warn('[MainDrawer] failed to fetch system info', err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setUserViews([]);
|
||||||
|
}
|
||||||
|
}, [ api, user?.Id ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* MAIN LINKS */}
|
||||||
|
<List>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/home.html' selected={isHomeSelected}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Home />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('Home')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/home.html?tab=1'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Favorite />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('Favorites')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
|
||||||
|
{/* CUSTOM LINKS */}
|
||||||
|
{(!!webConfig.menuLinks && webConfig.menuLinks.length > 0) && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<List>
|
||||||
|
{webConfig.menuLinks.map(menuLink => (
|
||||||
|
<ListItem
|
||||||
|
key={`${menuLink.name}_${menuLink.url}`}
|
||||||
|
disablePadding
|
||||||
|
>
|
||||||
|
<ListItemButton
|
||||||
|
component='a'
|
||||||
|
href={menuLink.url}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
{/* TODO: Support custom icons */}
|
||||||
|
<Link />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={menuLink.name} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* LIBRARY LINKS */}
|
||||||
|
{userViews.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<List
|
||||||
|
aria-labelledby='libraries-subheader'
|
||||||
|
subheader={
|
||||||
|
<ListSubheader component='div' id='libraries-subheader'>
|
||||||
|
{globalize.translate('HeaderLibraries')}
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{userViews.map(view => (
|
||||||
|
<ListItem key={view.Id} disablePadding>
|
||||||
|
<ListItemLink
|
||||||
|
to={appRouter.getRouteUrl(view, { context: view.CollectionType }).substring(1)}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<LibraryIcon item={view} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={view.Name} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ADMIN LINKS */}
|
||||||
|
{user?.Policy?.IsAdministrator && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<List
|
||||||
|
aria-labelledby='admin-subheader'
|
||||||
|
subheader={
|
||||||
|
<ListSubheader component='div' id='admin-subheader'>
|
||||||
|
{globalize.translate('HeaderAdmin')}
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/dashboard.html'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Dashboard />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('TabDashboard')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/edititemmetadata.html'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Edit />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('MetadataManager')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* FOOTER */}
|
||||||
|
<Divider style={{ marginTop: 'auto' }} />
|
||||||
|
<List>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText
|
||||||
|
primary={systemInfo?.ServerName ? systemInfo.ServerName : 'Jellyfin'}
|
||||||
|
secondary={systemInfo?.Version ? `v${systemInfo.Version}` : ''}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainDrawerContent;
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { Theme } from '@mui/material/styles';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Drawer from '@mui/material/Drawer';
|
||||||
|
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 interface ResponsiveDrawerProps {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onOpen: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResponsiveDrawer: FC<ResponsiveDrawerProps> = ({
|
||||||
|
children,
|
||||||
|
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 ]);
|
||||||
|
|
||||||
|
return ( isSmallScreen ? (
|
||||||
|
/* DESKTOP DRAWER */
|
||||||
|
<Drawer
|
||||||
|
sx={{
|
||||||
|
width: DRAWER_WIDTH,
|
||||||
|
flexShrink: 0,
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
width: DRAWER_WIDTH,
|
||||||
|
boxSizing: 'border-box'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
variant='persistent'
|
||||||
|
anchor='left'
|
||||||
|
open={open}
|
||||||
|
>
|
||||||
|
<Toolbar
|
||||||
|
variant='dense'
|
||||||
|
sx={getToolbarStyles}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</Drawer>
|
||||||
|
) : (
|
||||||
|
/* MOBILE DRAWER */
|
||||||
|
<SwipeableDrawer
|
||||||
|
anchor='left'
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
onOpen={onOpen}
|
||||||
|
// Disable swipe to open on iOS since it interferes with back navigation
|
||||||
|
disableDiscovery={browser.iOS}
|
||||||
|
ModalProps={{
|
||||||
|
keepMounted: true // Better open performance on mobile.
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Toolbar
|
||||||
|
variant='dense'
|
||||||
|
sx={getToolbarStyles}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
role='presentation'
|
||||||
|
// Close the drawer when the content is clicked
|
||||||
|
onClick={onClose}
|
||||||
|
onKeyDown={onClose}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</SwipeableDrawer>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResponsiveDrawer;
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { Lan, VpnKey, Article, EditNotifications, Extension, Schedule, ExpandLess, ExpandMore } from '@mui/icons-material';
|
||||||
|
import Collapse from '@mui/material/Collapse';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
|
import React from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
|
import ListItemLink from '../ListItemLink';
|
||||||
|
|
||||||
|
const PLUGIN_PATHS = [
|
||||||
|
'/installedplugins.html',
|
||||||
|
'/availableplugins.html',
|
||||||
|
'/repositories.html',
|
||||||
|
'/configurationpage'
|
||||||
|
];
|
||||||
|
|
||||||
|
const AdvancedDrawerSection = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isPluginSectionOpen = PLUGIN_PATHS.includes(location.pathname);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
aria-labelledby='advanced-subheader'
|
||||||
|
subheader={
|
||||||
|
<ListSubheader component='div' id='advanced-subheader'>
|
||||||
|
{globalize.translate('TabAdvanced')}
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/networking.html'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Lan />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('TabNetworking')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/apikeys.html'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<VpnKey />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('HeaderApiKeys')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/log.html'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Article />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('TabLogs')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/notificationsettings.html'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<EditNotifications />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('TabNotifications')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/installedplugins.html' selected={false}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Extension />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('TabPlugins')} />
|
||||||
|
{isPluginSectionOpen ? <ExpandLess /> : <ExpandMore />}
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
<Collapse in={isPluginSectionOpen} timeout='auto' unmountOnExit>
|
||||||
|
<List component='div' disablePadding>
|
||||||
|
<ListItemLink to='/installedplugins.html' sx={{ pl: 4 }}>
|
||||||
|
<ListItemText inset primary={globalize.translate('TabMyPlugins')} />
|
||||||
|
</ListItemLink>
|
||||||
|
<ListItemLink to='/availableplugins.html' sx={{ pl: 4 }}>
|
||||||
|
<ListItemText inset primary={globalize.translate('TabCatalog')} />
|
||||||
|
</ListItemLink>
|
||||||
|
<ListItemLink to='/repositories.html' sx={{ pl: 4 }}>
|
||||||
|
<ListItemText inset primary={globalize.translate('TabRepositories')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</List>
|
||||||
|
</Collapse>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/scheduledtasks.html'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Schedule />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('TabScheduledTasks')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdvancedDrawerSection;
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { Devices, Analytics, Input, ExpandLess, ExpandMore } from '@mui/icons-material';
|
||||||
|
import Collapse from '@mui/material/Collapse';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
|
import React from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
|
import ListItemLink from '../ListItemLink';
|
||||||
|
|
||||||
|
const DLNA_PATHS = [
|
||||||
|
'/dlnasettings.html',
|
||||||
|
'/dlnaprofiles.html'
|
||||||
|
];
|
||||||
|
|
||||||
|
const DevicesDrawerSection = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isDlnaSectionOpen = DLNA_PATHS.includes(location.pathname);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
aria-labelledby='devices-subheader'
|
||||||
|
subheader={
|
||||||
|
<ListSubheader component='div' id='devices-subheader'>
|
||||||
|
{globalize.translate('HeaderDevices')}
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/devices.html'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Devices />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('HeaderDevices')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/serveractivity.html'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Analytics />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('HeaderActivity')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/dlnasettings.html' selected={false}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Input />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={'DLNA'} />
|
||||||
|
{isDlnaSectionOpen ? <ExpandLess /> : <ExpandMore />}
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
<Collapse in={isDlnaSectionOpen} timeout='auto' unmountOnExit>
|
||||||
|
<List component='div' disablePadding>
|
||||||
|
<ListItemLink to='/dlnasettings.html' sx={{ pl: 4 }}>
|
||||||
|
<ListItemText inset primary={globalize.translate('Settings')} />
|
||||||
|
</ListItemLink>
|
||||||
|
<ListItemLink to='/dlnaprofiles.html' sx={{ pl: 4 }}>
|
||||||
|
<ListItemText inset primary={globalize.translate('TabProfiles')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</List>
|
||||||
|
</Collapse>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DevicesDrawerSection;
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { Dvr, LiveTv } from '@mui/icons-material';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
|
import ListItemLink from '../ListItemLink';
|
||||||
|
|
||||||
|
const LiveTvDrawerSection = () => {
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
aria-labelledby='livetv-subheader'
|
||||||
|
subheader={
|
||||||
|
<ListSubheader component='div' id='livetv-subheader'>
|
||||||
|
{globalize.translate('LiveTV')}
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/livetvstatus.html'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<LiveTv />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('LiveTV')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/livetvsettings.html'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Dvr />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('HeaderDVR')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LiveTvDrawerSection;
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { ConfigurationPageInfo } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import { getDashboardApi } from '@jellyfin/sdk/lib/utils/api/dashboard-api';
|
||||||
|
import { Folder } from '@mui/icons-material';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
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[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPluginPages = async () => {
|
||||||
|
if (!api) return;
|
||||||
|
|
||||||
|
const pagesResponse = await getDashboardApi(api)
|
||||||
|
.getConfigurationPages({ enableInMainMenu: true });
|
||||||
|
|
||||||
|
setPagesInfo(pagesResponse.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchPluginPages()
|
||||||
|
.catch(err => {
|
||||||
|
console.error('[PluginDrawerSection] unable to fetch plugin config pages', err);
|
||||||
|
});
|
||||||
|
}, [ api ]);
|
||||||
|
|
||||||
|
if (!api || pagesInfo.length < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
aria-labelledby='plugins-subheader'
|
||||||
|
subheader={
|
||||||
|
<ListSubheader component='div' id='plugins-subheader'>
|
||||||
|
{globalize.translate('TabPlugins')}
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
pagesInfo.map(pageInfo => (
|
||||||
|
<ListItem key={pageInfo.PluginId} disablePadding>
|
||||||
|
<ListItemLink to={`/${Dashboard.getPluginUrl(pageInfo.Name)}`}>
|
||||||
|
<ListItemIcon>
|
||||||
|
{/* TODO: Support different icons? */}
|
||||||
|
<Folder />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={pageInfo.DisplayName} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PluginDrawerSection;
|
|
@ -0,0 +1,118 @@
|
||||||
|
import { Dashboard, ExpandLess, ExpandMore, LibraryAdd, People, PlayCircle, Settings } from '@mui/icons-material';
|
||||||
|
import Collapse from '@mui/material/Collapse';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
|
import React from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
|
import ListItemLink from '../ListItemLink';
|
||||||
|
|
||||||
|
const LIBRARY_PATHS = [
|
||||||
|
'/library.html',
|
||||||
|
'/librarydisplay.html',
|
||||||
|
'/metadataimages.html',
|
||||||
|
'/metadatanfo.html'
|
||||||
|
];
|
||||||
|
|
||||||
|
const PLAYBACK_PATHS = [
|
||||||
|
'/encodingsettings.html',
|
||||||
|
'/playbackconfiguration.html',
|
||||||
|
'/streamingsettings.html'
|
||||||
|
];
|
||||||
|
|
||||||
|
const ServerDrawerSection = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isLibrarySectionOpen = LIBRARY_PATHS.includes(location.pathname);
|
||||||
|
const isPlaybackSectionOpen = PLAYBACK_PATHS.includes(location.pathname);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List
|
||||||
|
aria-labelledby='server-subheader'
|
||||||
|
subheader={
|
||||||
|
<ListSubheader component='div' id='server-subheader'>
|
||||||
|
{globalize.translate('TabServer')}
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/dashboard.html'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Dashboard />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('TabDashboard')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/dashboardgeneral.html'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Settings />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('General')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/userprofiles.html'>
|
||||||
|
<ListItemIcon>
|
||||||
|
<People />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('HeaderUsers')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/library.html' selected={false}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<LibraryAdd />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('HeaderLibraries')} />
|
||||||
|
{isLibrarySectionOpen ? <ExpandLess /> : <ExpandMore />}
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
<Collapse in={isLibrarySectionOpen} timeout='auto' unmountOnExit>
|
||||||
|
<List component='div' disablePadding>
|
||||||
|
<ListItemLink to='/library.html' sx={{ pl: 4 }}>
|
||||||
|
<ListItemText inset primary={globalize.translate('HeaderLibraries')} />
|
||||||
|
</ListItemLink>
|
||||||
|
<ListItemLink to='/librarydisplay.html' sx={{ pl: 4 }}>
|
||||||
|
<ListItemText inset primary={globalize.translate('Display')} />
|
||||||
|
</ListItemLink>
|
||||||
|
<ListItemLink to='/metadataimages.html' sx={{ pl: 4 }}>
|
||||||
|
<ListItemText inset primary={globalize.translate('Metadata')} />
|
||||||
|
</ListItemLink>
|
||||||
|
<ListItemLink to='/metadatanfo.html' sx={{ pl: 4 }}>
|
||||||
|
<ListItemText inset primary={globalize.translate('TabNfoSettings')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</List>
|
||||||
|
</Collapse>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemLink to='/encodingsettings.html' selected={false}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<PlayCircle />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={globalize.translate('TitlePlayback')} />
|
||||||
|
{isPlaybackSectionOpen ? <ExpandLess /> : <ExpandMore />}
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItem>
|
||||||
|
<Collapse in={isPlaybackSectionOpen} timeout='auto' unmountOnExit>
|
||||||
|
<List component='div' disablePadding>
|
||||||
|
<ListItemLink to='/encodingsettings.html' sx={{ pl: 4 }}>
|
||||||
|
<ListItemText inset primary={globalize.translate('Transcoding')} />
|
||||||
|
</ListItemLink>
|
||||||
|
<ListItemLink to='/playbackconfiguration.html' sx={{ pl: 4 }}>
|
||||||
|
<ListItemText inset primary={globalize.translate('ButtonResume')} />
|
||||||
|
</ListItemLink>
|
||||||
|
<ListItemLink to='/streamingsettings.html' sx={{ pl: 4 }}>
|
||||||
|
<ListItemText inset primary={globalize.translate('TabStreaming')} />
|
||||||
|
</ListItemLink>
|
||||||
|
</List>
|
||||||
|
</Collapse>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerDrawerSection;
|
|
@ -2,12 +2,21 @@ import { Theme } from '@mui/material/styles';
|
||||||
import Tab from '@mui/material/Tab';
|
import Tab from '@mui/material/Tab';
|
||||||
import Tabs from '@mui/material/Tabs';
|
import Tabs from '@mui/material/Tabs';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import React, { FC, useCallback } from 'react';
|
import { debounce } from 'lodash-es';
|
||||||
|
import React, { FC, useCallback, useEffect } from 'react';
|
||||||
import { Route, Routes, useLocation, useSearchParams } from 'react-router-dom';
|
import { Route, Routes, useLocation, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import TabRoutes, { getDefaultTabIndex } from './tabRoutes';
|
import TabRoutes, { getDefaultTabIndex } from './tabRoutes';
|
||||||
|
|
||||||
const AppTabs: FC = () => {
|
interface AppTabsParams {
|
||||||
|
isDrawerOpen: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = debounce(() => window.dispatchEvent(new Event('resize')), 100);
|
||||||
|
|
||||||
|
const AppTabs: FC<AppTabsParams> = ({
|
||||||
|
isDrawerOpen
|
||||||
|
}) => {
|
||||||
const isBigScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
|
const isBigScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
|
@ -18,6 +27,12 @@ const AppTabs: FC = () => {
|
||||||
parseInt(searchParamsTab, 10) :
|
parseInt(searchParamsTab, 10) :
|
||||||
getDefaultTabIndex(location.pathname, libraryId);
|
getDefaultTabIndex(location.pathname, libraryId);
|
||||||
|
|
||||||
|
// HACK: Force resizing to workaround upstream bug with tab resizing
|
||||||
|
// https://github.com/mui/material-ui/issues/24011
|
||||||
|
useEffect(() => {
|
||||||
|
handleResize();
|
||||||
|
}, [ isDrawerOpen ]);
|
||||||
|
|
||||||
const onTabClick = useCallback((event: React.MouseEvent<HTMLElement>) => {
|
const onTabClick = useCallback((event: React.MouseEvent<HTMLElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
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 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 { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './asyncRoutes';
|
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './asyncRoutes';
|
||||||
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
|
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
|
||||||
|
|
||||||
|
|
16
src/types/collectionType.ts
Normal file
16
src/types/collectionType.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// NOTE: This should be included in the OpenAPI spec ideally
|
||||||
|
// https://github.com/jellyfin/jellyfin/blob/47290a8c3665f3adb859bda19deb66f438f2e5d0/MediaBrowser.Model/Entities/CollectionType.cs
|
||||||
|
export enum CollectionType {
|
||||||
|
Movies = 'movies',
|
||||||
|
TvShows = 'tvshows',
|
||||||
|
Music = 'music',
|
||||||
|
MusicVideos = 'musicvideos',
|
||||||
|
Trailers = 'trailers',
|
||||||
|
HomeVideos = 'homevideos',
|
||||||
|
BoxSets = 'boxsets',
|
||||||
|
Books = 'books',
|
||||||
|
Photos = 'photos',
|
||||||
|
LiveTv = 'livetv',
|
||||||
|
Playlists = 'playlists',
|
||||||
|
Folders = 'folders'
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue