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

Redesign library navigation in experimental layout

This commit is contained in:
Bill Thornton 2024-09-16 14:16:34 -04:00
parent 27e92802d5
commit 05c72ad521
22 changed files with 524 additions and 240 deletions

View file

@ -17,7 +17,7 @@ import { createRouterHistory } from 'components/router/routerHistory';
import UserThemeProvider from 'themes/UserThemeProvider';
const layoutMode = localStorage.getItem('layout');
const isExperimentalLayout = layoutMode === 'experimental';
const isExperimentalLayout = layoutMode === 'experimental' || !layoutMode;
const router = createHashRouter([
{

View file

@ -9,6 +9,7 @@ import { Outlet, useLocation } from 'react-router-dom';
import AppBody from 'components/AppBody';
import AppToolbar from 'components/toolbar/AppToolbar';
import ServerButton from 'components/toolbar/ServerButton';
import ElevationScroll from 'components/ElevationScroll';
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
import ThemeCss from 'components/ThemeCss';
@ -22,8 +23,6 @@ import { DASHBOARD_APP_PATHS } from './routes/routes';
import './AppOverrides.scss';
const DRAWERLESS_PATHS = [ DASHBOARD_APP_PATHS.MetadataManager ];
export const Component: FC = () => {
const [ isDrawerActive, setIsDrawerActive ] = useState(false);
const location = useLocation();
@ -31,8 +30,8 @@ export const Component: FC = () => {
const { dateFnsLocale } = useLocale();
const isMediumScreen = useMediaQuery((t: Theme) => t.breakpoints.up('md'));
const isDrawerAvailable = Boolean(user)
&& !DRAWERLESS_PATHS.some(path => location.pathname.startsWith(`/${path}`));
const isMetadataManager = location.pathname.startsWith(`/${DASHBOARD_APP_PATHS.MetadataManager}`);
const isDrawerAvailable = Boolean(user) && !isMetadataManager;
const isDrawerOpen = isDrawerActive && isDrawerAvailable;
const onToggleDrawer = useCallback(() => {
@ -74,6 +73,10 @@ export const Component: FC = () => {
<HelpButton />
}
>
{isMetadataManager && (
<ServerButton />
)}
<AppTabs isDrawerOpen={isDrawerOpen} />
</AppToolbar>
</AppBar>

View file

@ -8,7 +8,6 @@ import { Outlet, useLocation } from 'react-router-dom';
import AppBody from 'components/AppBody';
import CustomCss from 'components/CustomCss';
import ElevationScroll from 'components/ElevationScroll';
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
import ThemeCss from 'components/ThemeCss';
import { useApi } from 'hooks/useApi';
@ -23,7 +22,7 @@ export const Component = () => {
const location = useLocation();
const isMediumScreen = useMediaQuery((t: Theme) => t.breakpoints.up('md'));
const isDrawerAvailable = isDrawerPath(location.pathname) && Boolean(user);
const isDrawerAvailable = isDrawerPath(location.pathname) && Boolean(user) && !isMediumScreen;
const isDrawerOpen = isDrawerActive && isDrawerAvailable;
const onToggleDrawer = useCallback(() => {
@ -38,14 +37,8 @@ export const Component = () => {
<AppBar
position='fixed'
sx={{
width: {
xs: '100%',
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
},
ml: {
xs: 0,
md: isDrawerAvailable ? DRAWER_WIDTH : 0
}
width: '100%',
ml: 0
}}
>
<AppToolbar

View file

@ -5,23 +5,10 @@ $mui-bp-md: 900px;
$mui-bp-lg: 1200px;
$mui-bp-xl: 1536px;
$drawer-width: 240px;
#reactRoot {
height: 100%;
}
// Fix main pages layout to work with drawer
.mainAnimatedPage {
@media all and (min-width: $mui-bp-md) {
left: $drawer-width;
}
}
// The fallback page has no drawer
#fallbackPage {
left: 0;
}
// Hide some items from the user "settings" page that are in the drawer
#myPreferencesMenuPage {
.lnkQuickConnectPreferences,
@ -35,19 +22,10 @@ $drawer-width: 240px;
.homePage.libraryPage.withTabs, // Home page
// Library pages excluding the item details page and tabbed pages
.libraryPage:not(
.itemDetailPage,
.withTabs
.itemDetailPage
) {
padding-top: 3.25rem !important;
}
// Tabbed library pages
.libraryPage.withTabs {
padding-top: 6.5rem !important;
@media all and (min-width: $mui-bp-lg) {
padding-top: 3.25rem !important;
}
}
// Fix backdrop position on mobile item details page
.layout-mobile .itemBackdrop {

View file

@ -27,21 +27,17 @@ const getUrlParams = (searchParams: URLSearchParams) => {
return params;
};
interface SearchButtonProps {
isTabsAvailable: boolean;
}
const SearchButton: FC<SearchButtonProps> = ({ isTabsAvailable }) => {
const SearchButton: FC = () => {
const location = useLocation();
const [searchParams] = useSearchParams();
const isSearchPath = location.pathname === '/search';
const createSearchLink = isTabsAvailable ?
const search = createSearchParams(getUrlParams(searchParams));
const createSearchLink =
{
pathname: '/search',
search: `?${createSearchParams(getUrlParams(searchParams))}`
} :
'/search';
search: search ? `?${search}` : undefined
};
return (
<Tooltip title={globalize.translate('Search')}>

View file

@ -1,11 +1,14 @@
import Stack from '@mui/material/Stack';
import React, { type FC } from 'react';
import { useLocation } from 'react-router-dom';
import AppToolbar from 'components/toolbar/AppToolbar';
import AppTabs from '../tabs/AppTabs';
import ServerButton from 'components/toolbar/ServerButton';
import RemotePlayButton from './RemotePlayButton';
import SyncPlayButton from './SyncPlayButton';
import SearchButton from './SearchButton';
import { isTabPath } from '../tabs/tabRoutes';
import UserViewNav from './userViews/UserViewNav';
interface AppToolbarProps {
isDrawerAvailable: boolean
@ -31,7 +34,6 @@ const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
// The video osd does not show the standard toolbar
if (location.pathname === '/video') return null;
const isTabsAvailable = isTabPath(location.pathname);
const isPublicPath = PUBLIC_PATHS.includes(location.pathname);
return (
@ -40,7 +42,7 @@ const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
<>
<SyncPlayButton />
<RemotePlayButton />
<SearchButton isTabsAvailable={isTabsAvailable} />
<SearchButton />
</>
)}
isDrawerAvailable={isDrawerAvailable}
@ -48,7 +50,18 @@ const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
onDrawerButtonClick={onDrawerButtonClick}
isUserMenuAvailable={!isPublicPath}
>
{isTabsAvailable && (<AppTabs isDrawerOpen={isDrawerOpen} />)}
{!isDrawerAvailable && (
<Stack
direction='row'
spacing={0.5}
>
<ServerButton />
{!isPublicPath && (
<UserViewNav />
)}
</Stack>
)}
</AppToolbar>
);
};

View file

@ -0,0 +1,150 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import ArrowDropDown from '@mui/icons-material/ArrowDropDown';
import Favorite from '@mui/icons-material/Favorite';
import Button from '@mui/material/Button/Button';
import { Theme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import React, { useCallback, useMemo, useState } from 'react';
import { Link, useLocation, useSearchParams } from 'react-router-dom';
import LibraryIcon from 'apps/experimental/components/LibraryIcon';
import { isTabPath } from 'apps/experimental/components/tabs/tabRoutes';
import { PseudoUserViews } from 'apps/experimental/constants/PseudoUserViews';
import { appRouter } from 'components/router/appRouter';
import { useApi } from 'hooks/useApi';
import useCurrentTab from 'hooks/useCurrentTab';
import { useUserViews } from 'hooks/useUserViews';
import globalize from 'lib/globalize';
import UserViewsMenu from './UserViewsMenu';
const MAX_USER_VIEWS_MD = 3;
const MAX_USER_VIEWS_LG = 5;
const MAX_USER_VIEWS_XL = 8;
const OVERFLOW_MENU_ID = 'user-view-overflow-menu';
const HOME_PATH = '/home';
const LIST_PATH = '/list';
const getCurrentUserView = (
userViews: BaseItemDto[] | undefined,
pathname: string,
libraryId: string | null,
collectionType: string | null,
tab: number
) => {
const isUserViewPath = isTabPath(pathname) || [HOME_PATH, LIST_PATH].includes(pathname);
if (!isUserViewPath) return undefined;
if (collectionType === CollectionType.Livetv) {
return userViews?.find(({ CollectionType: type }) => type === CollectionType.Livetv);
}
if (pathname === HOME_PATH && tab === 1) {
return PseudoUserViews.Favorites;
}
// eslint-disable-next-line sonarjs/different-types-comparison
return userViews?.find(({ Id: id }) => id === libraryId);
};
const UserViewNav = () => {
const location = useLocation();
const [ searchParams ] = useSearchParams();
const libraryId = searchParams.get('topParentId') || searchParams.get('parentId');
const collectionType = searchParams.get('collectionType');
const { activeTab } = useCurrentTab();
const isExtraLargeScreen = useMediaQuery((t: Theme) => t.breakpoints.up('xl'));
const isLargeScreen = useMediaQuery((t: Theme) => t.breakpoints.up('lg'));
const maxViews = useMemo(() => {
if (isExtraLargeScreen) return MAX_USER_VIEWS_XL;
if (isLargeScreen) return MAX_USER_VIEWS_LG;
return MAX_USER_VIEWS_MD;
}, [ isExtraLargeScreen, isLargeScreen ]);
const { user } = useApi();
const {
data: userViews,
isPending
} = useUserViews(user?.Id);
const primaryViews = useMemo(() => (
userViews?.Items?.slice(0, maxViews)
), [ maxViews, userViews ]);
const overflowViews = useMemo(() => (
userViews?.Items?.slice(maxViews)
), [ maxViews, userViews ]);
const [ overflowAnchorEl, setOverflowAnchorEl ] = useState<null | HTMLElement>(null);
const isOverflowMenuOpen = Boolean(overflowAnchorEl);
const onOverflowButtonClick = useCallback((event: React.MouseEvent<HTMLElement>) => {
setOverflowAnchorEl(event.currentTarget);
}, []);
const onOverflowMenuClose = useCallback(() => {
setOverflowAnchorEl(null);
}, []);
const currentUserView = useMemo(() => (
getCurrentUserView(userViews?.Items, location.pathname, libraryId, collectionType, activeTab)
), [ activeTab, collectionType, libraryId, location.pathname, userViews ]);
if (isPending) return null;
return (
<>
<Button
variant='text'
color={(currentUserView?.Id === PseudoUserViews.Favorites.Id) ? 'primary' : 'inherit'}
startIcon={<Favorite />}
component={Link}
to='/home.html?tab=1'
>
{globalize.translate(PseudoUserViews.Favorites.Name)}
</Button>
{primaryViews?.map(view => (
<Button
key={view.Id}
variant='text'
color={(view.Id === currentUserView?.Id) ? 'primary' : 'inherit'}
startIcon={<LibraryIcon item={view} />}
component={Link}
to={appRouter.getRouteUrl(view, { context: view.CollectionType }).substring(1)}
>
{view.Name}
</Button>
))}
{overflowViews && overflowViews.length > 0 && (
<>
<Button
variant='text'
color='inherit'
endIcon={<ArrowDropDown />}
aria-controls={OVERFLOW_MENU_ID}
aria-haspopup='true'
onClick={onOverflowButtonClick}
>
{globalize.translate('ButtonMore')}
</Button>
<UserViewsMenu
anchorEl={overflowAnchorEl}
id={OVERFLOW_MENU_ID}
open={isOverflowMenuOpen}
onMenuClose={onOverflowMenuClose}
userViews={overflowViews}
selectedId={currentUserView?.Id}
/>
</>
)}
</>
);
};
export default UserViewNav;

View file

@ -0,0 +1,51 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import ListItemIcon from '@mui/material/ListItemIcon/ListItemIcon';
import ListItemText from '@mui/material/ListItemText/ListItemText';
import Menu, { type MenuProps } from '@mui/material/Menu/Menu';
import MenuItem from '@mui/material/MenuItem/MenuItem';
import React, { FC } from 'react';
import { Link } from 'react-router-dom';
import LibraryIcon from 'apps/experimental/components/LibraryIcon';
import { appRouter } from 'components/router/appRouter';
interface UserViewsMenuProps extends MenuProps {
userViews: BaseItemDto[]
selectedId?: string
includeGlobalViews?: boolean
onMenuClose: () => void
}
const UserViewsMenu: FC<UserViewsMenuProps> = ({
userViews,
selectedId,
onMenuClose,
...props
}) => {
return (
<Menu
{...props}
keepMounted
onClose={onMenuClose}
>
{userViews.map(view => (
<MenuItem
key={view.Id}
component={Link}
to={appRouter.getRouteUrl(view, { context: view.CollectionType }).substring(1)}
onClick={onMenuClose}
selected={view.Id === selectedId}
>
<ListItemIcon>
<LibraryIcon item={view} />
</ListItemIcon>
<ListItemText>
{view.Name}
</ListItemText>
</MenuItem>
))}
</Menu>
);
};
export default UserViewsMenu;

View file

@ -1,5 +1,6 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import Favorite from '@mui/icons-material/Favorite';
import Movie from '@mui/icons-material/Movie';
import MusicNote from '@mui/icons-material/MusicNote';
import Photo from '@mui/icons-material/Photo';
@ -14,6 +15,8 @@ import VideoLibrary from '@mui/icons-material/VideoLibrary';
import Folder from '@mui/icons-material/Folder';
import React, { FC } from 'react';
import { PseudoUserViews } from '../constants/PseudoUserViews';
interface LibraryIconProps {
item: BaseItemDto
}
@ -21,6 +24,10 @@ interface LibraryIconProps {
const LibraryIcon: FC<LibraryIconProps> = ({
item
}) => {
if (item.Id === PseudoUserViews.Favorites.Id) {
return <Favorite />;
}
switch (item.CollectionType) {
case CollectionType.Movies:
return <Movie />;

View file

@ -1,5 +1,3 @@
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 Divider from '@mui/material/Divider';
@ -111,38 +109,6 @@ const MainDrawerContent = () => {
</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'>
<ListItemIcon>
<Dashboard />
</ListItemIcon>
<ListItemText primary={globalize.translate('TabDashboard')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/metadata'>
<ListItemIcon>
<Edit />
</ListItemIcon>
<ListItemText primary={globalize.translate('MetadataManager')} />
</ListItemLink>
</ListItem>
</List>
</>
)}
</>
);
};

View file

@ -14,6 +14,15 @@ import { CardShape } from 'utils/card';
import Loading from 'components/loading/LoadingComponent';
import { playbackManager } from 'components/playback/playbackmanager';
import ItemsContainer from 'elements/emby-itemscontainer/ItemsContainer';
import NoItemsMessage from 'components/common/NoItemsMessage';
import Lists from 'components/listview/List/Lists';
import Cards from 'components/cardbuilder/Card/Cards';
import { LibraryTab } from 'types/libraryTab';
import { type LibraryViewSettings, type ParentId, ViewMode } from 'types/library';
import type { CardOptions } from 'types/cardOptions';
import type { ListOptions } from 'types/listOptions';
import { useItem } from 'hooks/useItem';
import AlphabetPicker from './AlphabetPicker';
import FilterButton from './filter/FilterButton';
import NewCollectionButton from './NewCollectionButton';
@ -23,14 +32,7 @@ import QueueButton from './QueueButton';
import ShuffleButton from './ShuffleButton';
import SortButton from './SortButton';
import GridListViewButton from './GridListViewButton';
import NoItemsMessage from 'components/common/NoItemsMessage';
import Lists from 'components/listview/List/Lists';
import Cards from 'components/cardbuilder/Card/Cards';
import { LibraryTab } from 'types/libraryTab';
import { type LibraryViewSettings, type ParentId, ViewMode } from 'types/library';
import type { CardOptions } from 'types/cardOptions';
import type { ListOptions } from 'types/listOptions';
import { useItem } from 'hooks/useItem';
import LibraryViewMenu from './LibraryViewMenu';
interface ItemsViewProps {
viewType: LibraryTab;
@ -225,17 +227,21 @@ const ItemsView: FC<ItemsViewProps> = ({
'vertical-list' :
'vertical-wrap'
);
return (
<Box>
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
{isPaginationEnabled && (
<Pagination
totalRecordCount={totalRecordCount}
libraryViewSettings={libraryViewSettings}
isPlaceholderData={isPlaceholderData}
setLibraryViewSettings={setLibraryViewSettings}
/>
<Box
className={classNames(
'padded-top padded-left padded-right padded-bottom',
{ 'padded-right-withalphapicker': isAlphabetPickerEnabled }
)}
sx={{
display: 'flex',
flexWrap: 'wrap'
}}
>
<LibraryViewMenu />
{isBtnPlayAllEnabled && (
<PlayAllButton
@ -246,6 +252,15 @@ const ItemsView: FC<ItemsViewProps> = ({
libraryViewSettings={libraryViewSettings}
/>
)}
{isBtnShuffleEnabled && totalRecordCount > 1 && (
<ShuffleButton
item={item}
items={items}
viewType={viewType}
hasFilters={hasFilters}
libraryViewSettings={libraryViewSettings}
/>
)}
{isBtnQueueEnabled
&& item
&& playbackManager.canQueue(item) && (
@ -255,15 +270,6 @@ const ItemsView: FC<ItemsViewProps> = ({
hasFilters={hasFilters}
/>
)}
{isBtnShuffleEnabled && totalRecordCount > 1 && (
<ShuffleButton
item={item}
items={items}
viewType={viewType}
hasFilters={hasFilters}
libraryViewSettings={libraryViewSettings}
/>
)}
{isBtnSortEnabled && (
<SortButton
viewType={viewType}
@ -289,6 +295,24 @@ const ItemsView: FC<ItemsViewProps> = ({
setLibraryViewSettings={setLibraryViewSettings}
/>
)}
{isPaginationEnabled && (
<Box
sx={{
display: 'flex',
justifyContent: 'flex-end',
flexGrow: 1,
order: 10
}}
>
<Pagination
totalRecordCount={totalRecordCount}
libraryViewSettings={libraryViewSettings}
isPlaceholderData={isPlaceholderData}
setLibraryViewSettings={setLibraryViewSettings}
/>
</Box>
)}
</Box>
{isAlphabetPickerEnabled && hasSortName && (
@ -312,7 +336,16 @@ const ItemsView: FC<ItemsViewProps> = ({
)}
{isPaginationEnabled && (
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
<Box
className={classNames(
'padded-top padded-left padded-right padded-bottom',
{ 'padded-right-withalphapicker': isAlphabetPickerEnabled }
)}
sx={{
display: 'flex',
justifyContent: 'flex-end'
}}
>
<Pagination
totalRecordCount={totalRecordCount}
libraryViewSettings={libraryViewSettings}

View file

@ -0,0 +1,77 @@
import ArrowDropDown from '@mui/icons-material/ArrowDropDown';
import Button from '@mui/material/Button/Button';
import Menu from '@mui/material/Menu/Menu';
import MenuItem from '@mui/material/MenuItem/MenuItem';
import React, { FC, useCallback, useState } from 'react';
import { useLocation, useSearchParams } from 'react-router-dom';
import useCurrentTab from 'hooks/useCurrentTab';
import globalize from 'lib/globalize';
import TabRoutes from '../tabs/tabRoutes';
const LIBRARY_VIEW_MENU_ID = 'library-view-menu';
const LibraryViewMenu: FC = () => {
const location = useLocation();
const [ searchParams, setSearchParams ] = useSearchParams();
const { activeTab } = useCurrentTab();
const [ menuAnchorEl, setMenuAnchorEl ] = useState<null | HTMLElement>(null);
const isMenuOpen = Boolean(menuAnchorEl);
const onMenuButtonClick = useCallback((event: React.MouseEvent<HTMLElement>) => {
setMenuAnchorEl(event.currentTarget);
}, []);
const onMenuClose = useCallback(() => {
setMenuAnchorEl(null);
}, []);
const currentRoute = TabRoutes.find(({ path }) => path === location.pathname);
const currentTab = currentRoute?.tabs.find(({ index }) => index === activeTab);
if (!currentTab) return null;
return (
<>
<Button
variant='text'
size='large'
color='inherit'
endIcon={<ArrowDropDown />}
aria-controls={LIBRARY_VIEW_MENU_ID}
aria-haspopup='true'
onClick={onMenuButtonClick}
>
{globalize.translate(currentTab.label)}
</Button>
<Menu
anchorEl={menuAnchorEl}
id={LIBRARY_VIEW_MENU_ID}
keepMounted
open={isMenuOpen}
onClose={onMenuClose}
>
{currentRoute?.tabs.map(tab => (
<MenuItem
key={tab.value}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => {
searchParams.set('tab', `${tab.index}`);
setSearchParams(searchParams);
onMenuClose();
}}
selected={tab.index === currentTab.index}
>
{globalize.translate(tab.label)}
</MenuItem>
))}
</Menu>
</>
);
};
export default LibraryViewMenu;

View file

@ -1,3 +1,4 @@
import Box from '@mui/material/Box/Box';
import React, { type FC } from 'react';
import SuggestionsSectionView from './SuggestionsSectionView';
import UpcomingView from './UpcomingView';
@ -8,6 +9,7 @@ import ProgramsSectionView from './ProgramsSectionView';
import { LibraryTab } from 'types/libraryTab';
import type { ParentId } from 'types/library';
import type { LibraryTabContent } from 'types/libraryTabContent';
import LibraryViewMenu from './LibraryViewMenu';
interface PageTabContentProps {
parentId: ParentId;
@ -17,46 +19,86 @@ interface PageTabContentProps {
const PageTabContent: FC<PageTabContentProps> = ({ parentId, currentTab }) => {
if (currentTab.viewType === LibraryTab.Suggestions) {
return (
<SuggestionsSectionView
parentId={parentId}
sectionType={
currentTab.sectionsView?.suggestionSections ?? []
}
isMovieRecommendationEnabled={
currentTab.sectionsView?.isMovieRecommendations
}
/>
<>
<Box className='padded-top padded-left padded-right padded-bottom'>
<LibraryViewMenu />
</Box>
<SuggestionsSectionView
parentId={parentId}
sectionType={
currentTab.sectionsView?.suggestionSections ?? []
}
isMovieRecommendationEnabled={
currentTab.sectionsView?.isMovieRecommendations
}
/>
</>
);
}
if (currentTab.viewType === LibraryTab.Programs || currentTab.viewType === LibraryTab.Recordings || currentTab.viewType === LibraryTab.Schedule) {
return (
<ProgramsSectionView
parentId={parentId}
sectionType={
currentTab.sectionsView?.programSections ?? []
}
isUpcomingRecordingsEnabled={currentTab.sectionsView?.isLiveTvUpcomingRecordings}
/>
<>
<Box className='padded-top padded-left padded-right padded-bottom'>
<LibraryViewMenu />
</Box>
<ProgramsSectionView
parentId={parentId}
sectionType={
currentTab.sectionsView?.programSections ?? []
}
isUpcomingRecordingsEnabled={currentTab.sectionsView?.isLiveTvUpcomingRecordings}
/>
</>
);
}
if (currentTab.viewType === LibraryTab.Upcoming) {
return <UpcomingView parentId={parentId} />;
return (
<>
<Box className='padded-top padded-left padded-right padded-bottom'>
<LibraryViewMenu />
</Box>
<UpcomingView parentId={parentId} />
</>
);
}
if (currentTab.viewType === LibraryTab.Genres) {
return (
<GenresView
parentId={parentId}
collectionType={currentTab.collectionType}
itemType={currentTab.itemType || []}
/>
<>
<Box className='padded-top padded-left padded-right padded-bottom'>
<LibraryViewMenu />
</Box>
<GenresView
parentId={parentId}
collectionType={currentTab.collectionType}
itemType={currentTab.itemType || []}
/>
</>
);
}
if (currentTab.viewType === LibraryTab.Guide) {
return <GuideView />;
return (
<>
<Box
className='padded-top padded-left padded-right padded-bottom'
sx={{
position: 'relative',
zIndex: 2
}}
>
<LibraryViewMenu />
</Box>
<GuideView />
</>
);
}
return (

View file

@ -47,10 +47,15 @@ const Pagination: FC<PaginationProps> = ({
}, [limit, setLibraryViewSettings, startIndex]);
return (
<Box className='paging'>
<Box
className='paging'
sx={{
display: 'flex'
}}
>
<Box
className='listPaging'
style={{ display: 'flex', alignItems: 'center' }}
sx={{ display: 'flex', alignItems: 'center' }}
>
<span>
{globalize.translate(

View file

@ -51,6 +51,12 @@ const PlayAllButton: FC<PlayAllButtonProps> = ({ item, items, viewType, hasFilte
title={globalize.translate('HeaderPlayAll')}
className='paper-icon-button-light btnPlay autoSize'
onClick={play}
sx={{
order: {
xs: 1,
sm: 'unset'
}
}}
>
<PlayArrowIcon />
</IconButton>

View file

@ -34,6 +34,12 @@ const QueueButton: FC<QueueButtonProps> = ({ item, items, hasFilters }) => {
title={globalize.translate('AddToPlayQueue')}
className='paper-icon-button-light btnQueue autoSize'
onClick={queue}
sx={{
order: {
xs: 3,
sm: 'unset'
}
}}
>
<QueueIcon />
</IconButton>

View file

@ -42,6 +42,12 @@ const ShuffleButton: FC<ShuffleButtonProps> = ({ item, items, viewType, hasFilte
title={globalize.translate('Shuffle')}
className='paper-icon-button-light btnShuffle autoSize'
onClick={shuffle}
sx={{
order: {
xs: 2,
sm: 'unset'
}
}}
>
<ShuffleIcon />
</IconButton>

View file

@ -1,85 +0,0 @@
import { Theme } from '@mui/material/styles';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import useMediaQuery from '@mui/material/useMediaQuery';
import { debounce } from 'lodash-es';
import React, { FC, useCallback, useEffect } from 'react';
import { Route, Routes } from 'react-router-dom';
import TabRoutes from './tabRoutes';
import useCurrentTab from 'hooks/useCurrentTab';
import globalize from 'lib/globalize';
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 { searchParams, setSearchParams, activeTab } = useCurrentTab();
// 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>) => {
event.preventDefault();
const tabIndex = event.currentTarget.dataset.tabIndex;
if (tabIndex) {
searchParams.set('tab', tabIndex);
setSearchParams(searchParams);
}
}, [ searchParams, setSearchParams ]);
return (
<Routes>
{
TabRoutes.map(route => (
<Route
key={route.path}
path={route.path}
element={
<Tabs
value={activeTab}
sx={{
width: '100%',
flexShrink: {
xs: 0,
lg: 'unset'
},
order: {
xs: 100,
lg: 'unset'
}
}}
variant={isBigScreen ? 'standard' : 'scrollable'}
centered={isBigScreen}
>
{
route.tabs.map(({ index, label }) => (
<Tab
key={`${route.path}-tab-${index}`}
label={globalize.translate(label)}
data-tab-index={`${index}`}
onClick={onTabClick}
/>
))
}
</Tabs>
}
/>
))
}
</Routes>
);
};
export default AppTabs;

View file

@ -0,0 +1,11 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
/**
* Views in the web app that we treat as UserViews.
*/
export const PseudoUserViews: Record<string, BaseItemDto> = {
Favorites: {
Id: 'favorites',
Name: 'Favorites'
}
};

View file

@ -46,7 +46,6 @@ const VideoPage: FC = () => {
<AppToolbar
isDrawerAvailable={false}
isDrawerOpen={false}
isFullscreen
isUserMenuAvailable={false}
buttons={
<>

View file

@ -16,8 +16,8 @@ interface AppToolbarProps {
buttons?: ReactNode
isDrawerAvailable: boolean
isDrawerOpen: boolean
onDrawerButtonClick?: (event: React.MouseEvent<HTMLElement>) => void,
isFullscreen?: boolean,
onDrawerButtonClick?: (event: React.MouseEvent<HTMLElement>) => void
isBackButtonAvailable?: boolean
isUserMenuAvailable?: boolean
}
@ -34,31 +34,21 @@ const AppToolbar: FC<PropsWithChildren<AppToolbarProps>> = ({
isDrawerAvailable,
isDrawerOpen,
onDrawerButtonClick = () => { /* no-op */ },
isFullscreen = false,
isBackButtonAvailable = false,
isUserMenuAvailable = true
}) => {
const { user } = useApi();
const isUserLoggedIn = Boolean(user);
const isBackButtonAvailable = appRouter.canGoBack();
// Only use the left safe area padding when the drawer is not pinned or in a fullscreen view
const useSafeAreaLeft = isDrawerAvailable || isFullscreen;
return (
<Toolbar
variant='dense'
sx={{
flexWrap: {
xs: 'wrap',
lg: 'nowrap'
flexWrap: 'wrap',
pl: {
xs: 'max(16px, env(safe-area-inset-left))',
sm: 'max(24px, env(safe-area-inset-left))'
},
...(useSafeAreaLeft && {
pl: {
xs: 'max(16px, env(safe-area-inset-left))',
sm: 'max(24px, env(safe-area-inset-left))'
}
}),
pr: {
xs: 'max(16px, env(safe-area-inset-left))',
sm: 'max(24px, env(safe-area-inset-left))'

View file

@ -0,0 +1,37 @@
import Button from '@mui/material/Button/Button';
import React, { FC } from 'react';
import { Link } from 'react-router-dom';
import { useSystemInfo } from 'hooks/useSystemInfo';
const ServerButton: FC = () => {
const {
data: systemInfo,
isPending
} = useSystemInfo();
return (
<Button
variant='text'
size='large'
color='inherit'
startIcon={
<img
src='assets/img/icon-transparent.png'
alt=''
aria-hidden
style={{
maxHeight: '1.25em',
maxWidth: '1.25em'
}}
/>
}
component={Link}
to='/'
>
{isPending ? '' : (systemInfo?.ServerName || 'Jellyfin')}
</Button>
);
};
export default ServerButton;