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:
parent
27e92802d5
commit
05c72ad521
22 changed files with 524 additions and 240 deletions
|
@ -17,7 +17,7 @@ import { createRouterHistory } from 'components/router/routerHistory';
|
||||||
import UserThemeProvider from 'themes/UserThemeProvider';
|
import UserThemeProvider from 'themes/UserThemeProvider';
|
||||||
|
|
||||||
const layoutMode = localStorage.getItem('layout');
|
const layoutMode = localStorage.getItem('layout');
|
||||||
const isExperimentalLayout = layoutMode === 'experimental';
|
const isExperimentalLayout = layoutMode === 'experimental' || !layoutMode;
|
||||||
|
|
||||||
const router = createHashRouter([
|
const router = createHashRouter([
|
||||||
{
|
{
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { Outlet, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import AppBody from 'components/AppBody';
|
import AppBody from 'components/AppBody';
|
||||||
import AppToolbar from 'components/toolbar/AppToolbar';
|
import AppToolbar from 'components/toolbar/AppToolbar';
|
||||||
|
import ServerButton from 'components/toolbar/ServerButton';
|
||||||
import ElevationScroll from 'components/ElevationScroll';
|
import ElevationScroll from 'components/ElevationScroll';
|
||||||
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
||||||
import ThemeCss from 'components/ThemeCss';
|
import ThemeCss from 'components/ThemeCss';
|
||||||
|
@ -22,8 +23,6 @@ import { DASHBOARD_APP_PATHS } from './routes/routes';
|
||||||
|
|
||||||
import './AppOverrides.scss';
|
import './AppOverrides.scss';
|
||||||
|
|
||||||
const DRAWERLESS_PATHS = [ DASHBOARD_APP_PATHS.MetadataManager ];
|
|
||||||
|
|
||||||
export const Component: FC = () => {
|
export const Component: FC = () => {
|
||||||
const [ isDrawerActive, setIsDrawerActive ] = useState(false);
|
const [ isDrawerActive, setIsDrawerActive ] = useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
@ -31,8 +30,8 @@ export const Component: FC = () => {
|
||||||
const { dateFnsLocale } = useLocale();
|
const { dateFnsLocale } = useLocale();
|
||||||
|
|
||||||
const isMediumScreen = useMediaQuery((t: Theme) => t.breakpoints.up('md'));
|
const isMediumScreen = useMediaQuery((t: Theme) => t.breakpoints.up('md'));
|
||||||
const isDrawerAvailable = Boolean(user)
|
const isMetadataManager = location.pathname.startsWith(`/${DASHBOARD_APP_PATHS.MetadataManager}`);
|
||||||
&& !DRAWERLESS_PATHS.some(path => location.pathname.startsWith(`/${path}`));
|
const isDrawerAvailable = Boolean(user) && !isMetadataManager;
|
||||||
const isDrawerOpen = isDrawerActive && isDrawerAvailable;
|
const isDrawerOpen = isDrawerActive && isDrawerAvailable;
|
||||||
|
|
||||||
const onToggleDrawer = useCallback(() => {
|
const onToggleDrawer = useCallback(() => {
|
||||||
|
@ -74,6 +73,10 @@ export const Component: FC = () => {
|
||||||
<HelpButton />
|
<HelpButton />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{isMetadataManager && (
|
||||||
|
<ServerButton />
|
||||||
|
)}
|
||||||
|
|
||||||
<AppTabs isDrawerOpen={isDrawerOpen} />
|
<AppTabs isDrawerOpen={isDrawerOpen} />
|
||||||
</AppToolbar>
|
</AppToolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { Outlet, useLocation } from 'react-router-dom';
|
||||||
import AppBody from 'components/AppBody';
|
import AppBody from 'components/AppBody';
|
||||||
import CustomCss from 'components/CustomCss';
|
import CustomCss from 'components/CustomCss';
|
||||||
import ElevationScroll from 'components/ElevationScroll';
|
import ElevationScroll from 'components/ElevationScroll';
|
||||||
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
|
||||||
import ThemeCss from 'components/ThemeCss';
|
import ThemeCss from 'components/ThemeCss';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
|
|
||||||
|
@ -23,7 +22,7 @@ export const Component = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const isMediumScreen = useMediaQuery((t: Theme) => t.breakpoints.up('md'));
|
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 isDrawerOpen = isDrawerActive && isDrawerAvailable;
|
||||||
|
|
||||||
const onToggleDrawer = useCallback(() => {
|
const onToggleDrawer = useCallback(() => {
|
||||||
|
@ -38,14 +37,8 @@ export const Component = () => {
|
||||||
<AppBar
|
<AppBar
|
||||||
position='fixed'
|
position='fixed'
|
||||||
sx={{
|
sx={{
|
||||||
width: {
|
width: '100%',
|
||||||
xs: '100%',
|
ml: 0
|
||||||
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
|
|
||||||
},
|
|
||||||
ml: {
|
|
||||||
xs: 0,
|
|
||||||
md: isDrawerAvailable ? DRAWER_WIDTH : 0
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AppToolbar
|
<AppToolbar
|
||||||
|
|
|
@ -5,23 +5,10 @@ $mui-bp-md: 900px;
|
||||||
$mui-bp-lg: 1200px;
|
$mui-bp-lg: 1200px;
|
||||||
$mui-bp-xl: 1536px;
|
$mui-bp-xl: 1536px;
|
||||||
|
|
||||||
$drawer-width: 240px;
|
|
||||||
|
|
||||||
#reactRoot {
|
#reactRoot {
|
||||||
height: 100%;
|
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
|
// Hide some items from the user "settings" page that are in the drawer
|
||||||
#myPreferencesMenuPage {
|
#myPreferencesMenuPage {
|
||||||
.lnkQuickConnectPreferences,
|
.lnkQuickConnectPreferences,
|
||||||
|
@ -35,19 +22,10 @@ $drawer-width: 240px;
|
||||||
.homePage.libraryPage.withTabs, // Home page
|
.homePage.libraryPage.withTabs, // Home page
|
||||||
// Library pages excluding the item details page and tabbed pages
|
// Library pages excluding the item details page and tabbed pages
|
||||||
.libraryPage:not(
|
.libraryPage:not(
|
||||||
.itemDetailPage,
|
.itemDetailPage
|
||||||
.withTabs
|
|
||||||
) {
|
) {
|
||||||
padding-top: 3.25rem !important;
|
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
|
// Fix backdrop position on mobile item details page
|
||||||
.layout-mobile .itemBackdrop {
|
.layout-mobile .itemBackdrop {
|
||||||
|
|
|
@ -27,21 +27,17 @@ const getUrlParams = (searchParams: URLSearchParams) => {
|
||||||
return params;
|
return params;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SearchButtonProps {
|
const SearchButton: FC = () => {
|
||||||
isTabsAvailable: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SearchButton: FC<SearchButtonProps> = ({ isTabsAvailable }) => {
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const isSearchPath = location.pathname === '/search';
|
const isSearchPath = location.pathname === '/search';
|
||||||
const createSearchLink = isTabsAvailable ?
|
const search = createSearchParams(getUrlParams(searchParams));
|
||||||
|
const createSearchLink =
|
||||||
{
|
{
|
||||||
pathname: '/search',
|
pathname: '/search',
|
||||||
search: `?${createSearchParams(getUrlParams(searchParams))}`
|
search: search ? `?${search}` : undefined
|
||||||
} :
|
};
|
||||||
'/search';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={globalize.translate('Search')}>
|
<Tooltip title={globalize.translate('Search')}>
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
import React, { type FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import AppToolbar from 'components/toolbar/AppToolbar';
|
import AppToolbar from 'components/toolbar/AppToolbar';
|
||||||
import AppTabs from '../tabs/AppTabs';
|
import ServerButton from 'components/toolbar/ServerButton';
|
||||||
|
|
||||||
import RemotePlayButton from './RemotePlayButton';
|
import RemotePlayButton from './RemotePlayButton';
|
||||||
import SyncPlayButton from './SyncPlayButton';
|
import SyncPlayButton from './SyncPlayButton';
|
||||||
import SearchButton from './SearchButton';
|
import SearchButton from './SearchButton';
|
||||||
import { isTabPath } from '../tabs/tabRoutes';
|
import UserViewNav from './userViews/UserViewNav';
|
||||||
|
|
||||||
interface AppToolbarProps {
|
interface AppToolbarProps {
|
||||||
isDrawerAvailable: boolean
|
isDrawerAvailable: boolean
|
||||||
|
@ -31,7 +34,6 @@ const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
|
||||||
// The video osd does not show the standard toolbar
|
// The video osd does not show the standard toolbar
|
||||||
if (location.pathname === '/video') return null;
|
if (location.pathname === '/video') return null;
|
||||||
|
|
||||||
const isTabsAvailable = isTabPath(location.pathname);
|
|
||||||
const isPublicPath = PUBLIC_PATHS.includes(location.pathname);
|
const isPublicPath = PUBLIC_PATHS.includes(location.pathname);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -40,7 +42,7 @@ const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
|
||||||
<>
|
<>
|
||||||
<SyncPlayButton />
|
<SyncPlayButton />
|
||||||
<RemotePlayButton />
|
<RemotePlayButton />
|
||||||
<SearchButton isTabsAvailable={isTabsAvailable} />
|
<SearchButton />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
isDrawerAvailable={isDrawerAvailable}
|
isDrawerAvailable={isDrawerAvailable}
|
||||||
|
@ -48,7 +50,18 @@ const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
|
||||||
onDrawerButtonClick={onDrawerButtonClick}
|
onDrawerButtonClick={onDrawerButtonClick}
|
||||||
isUserMenuAvailable={!isPublicPath}
|
isUserMenuAvailable={!isPublicPath}
|
||||||
>
|
>
|
||||||
{isTabsAvailable && (<AppTabs isDrawerOpen={isDrawerOpen} />)}
|
{!isDrawerAvailable && (
|
||||||
|
<Stack
|
||||||
|
direction='row'
|
||||||
|
spacing={0.5}
|
||||||
|
>
|
||||||
|
<ServerButton />
|
||||||
|
|
||||||
|
{!isPublicPath && (
|
||||||
|
<UserViewNav />
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
</AppToolbar>
|
</AppToolbar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,5 +1,6 @@
|
||||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
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 Movie from '@mui/icons-material/Movie';
|
||||||
import MusicNote from '@mui/icons-material/MusicNote';
|
import MusicNote from '@mui/icons-material/MusicNote';
|
||||||
import Photo from '@mui/icons-material/Photo';
|
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 Folder from '@mui/icons-material/Folder';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import { PseudoUserViews } from '../constants/PseudoUserViews';
|
||||||
|
|
||||||
interface LibraryIconProps {
|
interface LibraryIconProps {
|
||||||
item: BaseItemDto
|
item: BaseItemDto
|
||||||
}
|
}
|
||||||
|
@ -21,6 +24,10 @@ interface LibraryIconProps {
|
||||||
const LibraryIcon: FC<LibraryIconProps> = ({
|
const LibraryIcon: FC<LibraryIconProps> = ({
|
||||||
item
|
item
|
||||||
}) => {
|
}) => {
|
||||||
|
if (item.Id === PseudoUserViews.Favorites.Id) {
|
||||||
|
return <Favorite />;
|
||||||
|
}
|
||||||
|
|
||||||
switch (item.CollectionType) {
|
switch (item.CollectionType) {
|
||||||
case CollectionType.Movies:
|
case CollectionType.Movies:
|
||||||
return <Movie />;
|
return <Movie />;
|
||||||
|
|
|
@ -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 Favorite from '@mui/icons-material/Favorite';
|
||||||
import Home from '@mui/icons-material/Home';
|
import Home from '@mui/icons-material/Home';
|
||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
|
@ -111,38 +109,6 @@ const MainDrawerContent = () => {
|
||||||
</List>
|
</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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,6 +14,15 @@ import { CardShape } from 'utils/card';
|
||||||
import Loading from 'components/loading/LoadingComponent';
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
import { playbackManager } from 'components/playback/playbackmanager';
|
import { playbackManager } from 'components/playback/playbackmanager';
|
||||||
import ItemsContainer from 'elements/emby-itemscontainer/ItemsContainer';
|
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 AlphabetPicker from './AlphabetPicker';
|
||||||
import FilterButton from './filter/FilterButton';
|
import FilterButton from './filter/FilterButton';
|
||||||
import NewCollectionButton from './NewCollectionButton';
|
import NewCollectionButton from './NewCollectionButton';
|
||||||
|
@ -23,14 +32,7 @@ import QueueButton from './QueueButton';
|
||||||
import ShuffleButton from './ShuffleButton';
|
import ShuffleButton from './ShuffleButton';
|
||||||
import SortButton from './SortButton';
|
import SortButton from './SortButton';
|
||||||
import GridListViewButton from './GridListViewButton';
|
import GridListViewButton from './GridListViewButton';
|
||||||
import NoItemsMessage from 'components/common/NoItemsMessage';
|
import LibraryViewMenu from './LibraryViewMenu';
|
||||||
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';
|
|
||||||
|
|
||||||
interface ItemsViewProps {
|
interface ItemsViewProps {
|
||||||
viewType: LibraryTab;
|
viewType: LibraryTab;
|
||||||
|
@ -225,17 +227,21 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
'vertical-list' :
|
'vertical-list' :
|
||||||
'vertical-wrap'
|
'vertical-wrap'
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
<Box
|
||||||
{isPaginationEnabled && (
|
className={classNames(
|
||||||
<Pagination
|
'padded-top padded-left padded-right padded-bottom',
|
||||||
totalRecordCount={totalRecordCount}
|
{ 'padded-right-withalphapicker': isAlphabetPickerEnabled }
|
||||||
libraryViewSettings={libraryViewSettings}
|
|
||||||
isPlaceholderData={isPlaceholderData}
|
|
||||||
setLibraryViewSettings={setLibraryViewSettings}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
<LibraryViewMenu />
|
||||||
|
|
||||||
{isBtnPlayAllEnabled && (
|
{isBtnPlayAllEnabled && (
|
||||||
<PlayAllButton
|
<PlayAllButton
|
||||||
|
@ -246,6 +252,15 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
libraryViewSettings={libraryViewSettings}
|
libraryViewSettings={libraryViewSettings}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isBtnShuffleEnabled && totalRecordCount > 1 && (
|
||||||
|
<ShuffleButton
|
||||||
|
item={item}
|
||||||
|
items={items}
|
||||||
|
viewType={viewType}
|
||||||
|
hasFilters={hasFilters}
|
||||||
|
libraryViewSettings={libraryViewSettings}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isBtnQueueEnabled
|
{isBtnQueueEnabled
|
||||||
&& item
|
&& item
|
||||||
&& playbackManager.canQueue(item) && (
|
&& playbackManager.canQueue(item) && (
|
||||||
|
@ -255,15 +270,6 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
hasFilters={hasFilters}
|
hasFilters={hasFilters}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isBtnShuffleEnabled && totalRecordCount > 1 && (
|
|
||||||
<ShuffleButton
|
|
||||||
item={item}
|
|
||||||
items={items}
|
|
||||||
viewType={viewType}
|
|
||||||
hasFilters={hasFilters}
|
|
||||||
libraryViewSettings={libraryViewSettings}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isBtnSortEnabled && (
|
{isBtnSortEnabled && (
|
||||||
<SortButton
|
<SortButton
|
||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
|
@ -289,6 +295,24 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
setLibraryViewSettings={setLibraryViewSettings}
|
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>
|
</Box>
|
||||||
|
|
||||||
{isAlphabetPickerEnabled && hasSortName && (
|
{isAlphabetPickerEnabled && hasSortName && (
|
||||||
|
@ -312,7 +336,16 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isPaginationEnabled && (
|
{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
|
<Pagination
|
||||||
totalRecordCount={totalRecordCount}
|
totalRecordCount={totalRecordCount}
|
||||||
libraryViewSettings={libraryViewSettings}
|
libraryViewSettings={libraryViewSettings}
|
||||||
|
|
77
src/apps/experimental/components/library/LibraryViewMenu.tsx
Normal file
77
src/apps/experimental/components/library/LibraryViewMenu.tsx
Normal 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;
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Box from '@mui/material/Box/Box';
|
||||||
import React, { type FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
import SuggestionsSectionView from './SuggestionsSectionView';
|
import SuggestionsSectionView from './SuggestionsSectionView';
|
||||||
import UpcomingView from './UpcomingView';
|
import UpcomingView from './UpcomingView';
|
||||||
|
@ -8,6 +9,7 @@ import ProgramsSectionView from './ProgramsSectionView';
|
||||||
import { LibraryTab } from 'types/libraryTab';
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
import type { ParentId } from 'types/library';
|
import type { ParentId } from 'types/library';
|
||||||
import type { LibraryTabContent } from 'types/libraryTabContent';
|
import type { LibraryTabContent } from 'types/libraryTabContent';
|
||||||
|
import LibraryViewMenu from './LibraryViewMenu';
|
||||||
|
|
||||||
interface PageTabContentProps {
|
interface PageTabContentProps {
|
||||||
parentId: ParentId;
|
parentId: ParentId;
|
||||||
|
@ -17,46 +19,86 @@ interface PageTabContentProps {
|
||||||
const PageTabContent: FC<PageTabContentProps> = ({ parentId, currentTab }) => {
|
const PageTabContent: FC<PageTabContentProps> = ({ parentId, currentTab }) => {
|
||||||
if (currentTab.viewType === LibraryTab.Suggestions) {
|
if (currentTab.viewType === LibraryTab.Suggestions) {
|
||||||
return (
|
return (
|
||||||
<SuggestionsSectionView
|
<>
|
||||||
parentId={parentId}
|
<Box className='padded-top padded-left padded-right padded-bottom'>
|
||||||
sectionType={
|
<LibraryViewMenu />
|
||||||
currentTab.sectionsView?.suggestionSections ?? []
|
</Box>
|
||||||
}
|
|
||||||
isMovieRecommendationEnabled={
|
<SuggestionsSectionView
|
||||||
currentTab.sectionsView?.isMovieRecommendations
|
parentId={parentId}
|
||||||
}
|
sectionType={
|
||||||
/>
|
currentTab.sectionsView?.suggestionSections ?? []
|
||||||
|
}
|
||||||
|
isMovieRecommendationEnabled={
|
||||||
|
currentTab.sectionsView?.isMovieRecommendations
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentTab.viewType === LibraryTab.Programs || currentTab.viewType === LibraryTab.Recordings || currentTab.viewType === LibraryTab.Schedule) {
|
if (currentTab.viewType === LibraryTab.Programs || currentTab.viewType === LibraryTab.Recordings || currentTab.viewType === LibraryTab.Schedule) {
|
||||||
return (
|
return (
|
||||||
<ProgramsSectionView
|
<>
|
||||||
parentId={parentId}
|
<Box className='padded-top padded-left padded-right padded-bottom'>
|
||||||
sectionType={
|
<LibraryViewMenu />
|
||||||
currentTab.sectionsView?.programSections ?? []
|
</Box>
|
||||||
}
|
|
||||||
isUpcomingRecordingsEnabled={currentTab.sectionsView?.isLiveTvUpcomingRecordings}
|
<ProgramsSectionView
|
||||||
/>
|
parentId={parentId}
|
||||||
|
sectionType={
|
||||||
|
currentTab.sectionsView?.programSections ?? []
|
||||||
|
}
|
||||||
|
isUpcomingRecordingsEnabled={currentTab.sectionsView?.isLiveTvUpcomingRecordings}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentTab.viewType === LibraryTab.Upcoming) {
|
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) {
|
if (currentTab.viewType === LibraryTab.Genres) {
|
||||||
return (
|
return (
|
||||||
<GenresView
|
<>
|
||||||
parentId={parentId}
|
<Box className='padded-top padded-left padded-right padded-bottom'>
|
||||||
collectionType={currentTab.collectionType}
|
<LibraryViewMenu />
|
||||||
itemType={currentTab.itemType || []}
|
</Box>
|
||||||
/>
|
|
||||||
|
<GenresView
|
||||||
|
parentId={parentId}
|
||||||
|
collectionType={currentTab.collectionType}
|
||||||
|
itemType={currentTab.itemType || []}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentTab.viewType === LibraryTab.Guide) {
|
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 (
|
return (
|
||||||
|
|
|
@ -47,10 +47,15 @@ const Pagination: FC<PaginationProps> = ({
|
||||||
}, [limit, setLibraryViewSettings, startIndex]);
|
}, [limit, setLibraryViewSettings, startIndex]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className='paging'>
|
<Box
|
||||||
|
className='paging'
|
||||||
|
sx={{
|
||||||
|
display: 'flex'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
className='listPaging'
|
className='listPaging'
|
||||||
style={{ display: 'flex', alignItems: 'center' }}
|
sx={{ display: 'flex', alignItems: 'center' }}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{globalize.translate(
|
{globalize.translate(
|
||||||
|
|
|
@ -51,6 +51,12 @@ const PlayAllButton: FC<PlayAllButtonProps> = ({ item, items, viewType, hasFilte
|
||||||
title={globalize.translate('HeaderPlayAll')}
|
title={globalize.translate('HeaderPlayAll')}
|
||||||
className='paper-icon-button-light btnPlay autoSize'
|
className='paper-icon-button-light btnPlay autoSize'
|
||||||
onClick={play}
|
onClick={play}
|
||||||
|
sx={{
|
||||||
|
order: {
|
||||||
|
xs: 1,
|
||||||
|
sm: 'unset'
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PlayArrowIcon />
|
<PlayArrowIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
|
@ -34,6 +34,12 @@ const QueueButton: FC<QueueButtonProps> = ({ item, items, hasFilters }) => {
|
||||||
title={globalize.translate('AddToPlayQueue')}
|
title={globalize.translate('AddToPlayQueue')}
|
||||||
className='paper-icon-button-light btnQueue autoSize'
|
className='paper-icon-button-light btnQueue autoSize'
|
||||||
onClick={queue}
|
onClick={queue}
|
||||||
|
sx={{
|
||||||
|
order: {
|
||||||
|
xs: 3,
|
||||||
|
sm: 'unset'
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<QueueIcon />
|
<QueueIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
|
@ -42,6 +42,12 @@ const ShuffleButton: FC<ShuffleButtonProps> = ({ item, items, viewType, hasFilte
|
||||||
title={globalize.translate('Shuffle')}
|
title={globalize.translate('Shuffle')}
|
||||||
className='paper-icon-button-light btnShuffle autoSize'
|
className='paper-icon-button-light btnShuffle autoSize'
|
||||||
onClick={shuffle}
|
onClick={shuffle}
|
||||||
|
sx={{
|
||||||
|
order: {
|
||||||
|
xs: 2,
|
||||||
|
sm: 'unset'
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ShuffleIcon />
|
<ShuffleIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
|
@ -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;
|
|
11
src/apps/experimental/constants/PseudoUserViews.ts
Normal file
11
src/apps/experimental/constants/PseudoUserViews.ts
Normal 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'
|
||||||
|
}
|
||||||
|
};
|
|
@ -46,7 +46,6 @@ const VideoPage: FC = () => {
|
||||||
<AppToolbar
|
<AppToolbar
|
||||||
isDrawerAvailable={false}
|
isDrawerAvailable={false}
|
||||||
isDrawerOpen={false}
|
isDrawerOpen={false}
|
||||||
isFullscreen
|
|
||||||
isUserMenuAvailable={false}
|
isUserMenuAvailable={false}
|
||||||
buttons={
|
buttons={
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -16,8 +16,8 @@ interface AppToolbarProps {
|
||||||
buttons?: ReactNode
|
buttons?: ReactNode
|
||||||
isDrawerAvailable: boolean
|
isDrawerAvailable: boolean
|
||||||
isDrawerOpen: boolean
|
isDrawerOpen: boolean
|
||||||
onDrawerButtonClick?: (event: React.MouseEvent<HTMLElement>) => void,
|
onDrawerButtonClick?: (event: React.MouseEvent<HTMLElement>) => void
|
||||||
isFullscreen?: boolean,
|
isBackButtonAvailable?: boolean
|
||||||
isUserMenuAvailable?: boolean
|
isUserMenuAvailable?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,31 +34,21 @@ const AppToolbar: FC<PropsWithChildren<AppToolbarProps>> = ({
|
||||||
isDrawerAvailable,
|
isDrawerAvailable,
|
||||||
isDrawerOpen,
|
isDrawerOpen,
|
||||||
onDrawerButtonClick = () => { /* no-op */ },
|
onDrawerButtonClick = () => { /* no-op */ },
|
||||||
isFullscreen = false,
|
isBackButtonAvailable = false,
|
||||||
isUserMenuAvailable = true
|
isUserMenuAvailable = true
|
||||||
}) => {
|
}) => {
|
||||||
const { user } = useApi();
|
const { user } = useApi();
|
||||||
const isUserLoggedIn = Boolean(user);
|
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 (
|
return (
|
||||||
<Toolbar
|
<Toolbar
|
||||||
variant='dense'
|
variant='dense'
|
||||||
sx={{
|
sx={{
|
||||||
flexWrap: {
|
flexWrap: 'wrap',
|
||||||
xs: 'wrap',
|
pl: {
|
||||||
lg: 'nowrap'
|
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: {
|
pr: {
|
||||||
xs: 'max(16px, env(safe-area-inset-left))',
|
xs: 'max(16px, env(safe-area-inset-left))',
|
||||||
sm: 'max(24px, env(safe-area-inset-left))'
|
sm: 'max(24px, env(safe-area-inset-left))'
|
||||||
|
|
37
src/components/toolbar/ServerButton.tsx
Normal file
37
src/components/toolbar/ServerButton.tsx
Normal 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;
|
Loading…
Add table
Add a link
Reference in a new issue