diff --git a/src/apps/experimental/App.tsx b/src/apps/experimental/App.tsx index dfb97e1b7e..4c2dbbe75b 100644 --- a/src/apps/experimental/App.tsx +++ b/src/apps/experimental/App.tsx @@ -1,23 +1,56 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import AppBar from '@mui/material/AppBar'; import Box from '@mui/material/Box'; import { ThemeProvider } from '@mui/material/styles'; +import { useLocation } from 'react-router-dom'; import AppHeader from 'components/AppHeader'; import Backdrop from 'components/Backdrop'; +import { useApi } from 'hooks/useApi'; +import { useLocalStorage } from 'hooks/useLocalStorage'; import AppToolbar from './components/AppToolbar'; import AppUserMenu from './components/AppUserMenu'; +import AppDrawer, { DRAWER_WIDTH, isDrawerPath } from './components/drawers/AppDrawer'; import ElevationScroll from './components/ElevationScroll'; import { ExperimentalAppRoutes } from './routes/AppRoutes'; import theme from './theme'; import './AppOverrides.scss'; +interface ExperimentalAppSettings { + isDrawerPinned: boolean +} + +const DEFAULT_EXPERIMENTAL_APP_SETTINGS: ExperimentalAppSettings = { + isDrawerPinned: false +}; + const ExperimentalApp = () => { + const [ appSettings, setAppSettings ] = useLocalStorage('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); 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) => { setUserMenuAnchorEl(event.currentTarget); }, [ setUserMenuAnchorEl ]); @@ -39,17 +72,25 @@ const ExperimentalApp = () => { - + muiTheme.zIndex.drawer + 1 }} > + + { easing: theme.transitions.easing.sharp, 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 + }) }} >
diff --git a/src/apps/experimental/components/AppToolbar.tsx b/src/apps/experimental/components/AppToolbar.tsx index d53920d96d..e62c5585c5 100644 --- a/src/apps/experimental/components/AppToolbar.tsx +++ b/src/apps/experimental/components/AppToolbar.tsx @@ -1,3 +1,4 @@ +import MenuIcon from '@mui/icons-material/Menu'; import SearchIcon from '@mui/icons-material/Search'; import Avatar from '@mui/material/Avatar'; import Box from '@mui/material/Box'; @@ -7,24 +8,32 @@ import Toolbar from '@mui/material/Toolbar'; import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import React, { FC } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import { useApi } from 'hooks/useApi'; import globalize from 'scripts/globalize'; import { ID as UserMenuId } from './AppUserMenu'; import AppTabs from './tabs/AppTabs'; +import { isDrawerPath } from './drawers/AppDrawer'; interface AppToolbarProps { + isDrawerOpen: boolean + onDrawerButtonClick: (event: React.MouseEvent) => void onUserButtonClick: (event: React.MouseEvent) => void } const AppToolbar: FC = ({ + isDrawerOpen, + onDrawerButtonClick, onUserButtonClick }) => { const theme = useTheme(); const { api, user } = useApi(); const isUserLoggedIn = Boolean(user); + const location = useLocation(); + + const isDrawerAvailable = isDrawerPath(location.pathname); return ( = ({ } }} > + {isUserLoggedIn && isDrawerAvailable && ( + + + + + + )} + = ({ - + {isUserLoggedIn && ( <> diff --git a/src/apps/experimental/components/LibraryIcon.tsx b/src/apps/experimental/components/LibraryIcon.tsx new file mode 100644 index 0000000000..220682bc90 --- /dev/null +++ b/src/apps/experimental/components/LibraryIcon.tsx @@ -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 = ({ + item +}) => { + switch (item.CollectionType) { + case CollectionType.Movies: + return ; + case CollectionType.Music: + return ; + case CollectionType.HomeVideos: + case CollectionType.Photos: + return ; + case CollectionType.LiveTv: + return ; + case CollectionType.TvShows: + return ; + case CollectionType.Trailers: + return ; + case CollectionType.MusicVideos: + return ; + case CollectionType.Books: + return ; + case CollectionType.BoxSets: + return ; + case CollectionType.Playlists: + return ; + default: + return ; + } +}; + +export default LibraryIcon; diff --git a/src/apps/experimental/components/drawers/AppDrawer.tsx b/src/apps/experimental/components/drawers/AppDrawer.tsx new file mode 100644 index 0000000000..49ccab09fd --- /dev/null +++ b/src/apps/experimental/components/drawers/AppDrawer.tsx @@ -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 = ({ + open = false, + onClose, + onOpen +}) => ( + + { + MAIN_DRAWER_ROUTES.map(route => ( + + + + } + /> + )) + } + { + ADMIN_DRAWER_ROUTES.map(route => ( + + + + + + + + } + /> + )) + } + + {/* Suppress warnings for unhandled routes */} + + +); + +export default AppDrawer; diff --git a/src/apps/experimental/components/drawers/ListItemLink.tsx b/src/apps/experimental/components/drawers/ListItemLink.tsx new file mode 100644 index 0000000000..0e0206a4d0 --- /dev/null +++ b/src/apps/experimental/components/drawers/ListItemLink.tsx @@ -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 = ({ + 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 ( + + {children} + + ); +}; + +export default ListItemLink; diff --git a/src/apps/experimental/components/drawers/MainDrawerContent.tsx b/src/apps/experimental/components/drawers/MainDrawerContent.tsx new file mode 100644 index 0000000000..f402189cf7 --- /dev/null +++ b/src/apps/experimental/components/drawers/MainDrawerContent.tsx @@ -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(); + const [ userViews, setUserViews ] = useState([]); + 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 */} + + + + + + + + + + + + + + + + + + + + {/* CUSTOM LINKS */} + {(!!webConfig.menuLinks && webConfig.menuLinks.length > 0) && ( + <> + + + {webConfig.menuLinks.map(menuLink => ( + + + + {/* TODO: Support custom icons */} + + + + + + ))} + + + )} + + {/* LIBRARY LINKS */} + {userViews.length > 0 && ( + <> + + + {globalize.translate('HeaderLibraries')} + + } + > + {userViews.map(view => ( + + + + + + + + + ))} + + + )} + + {/* ADMIN LINKS */} + {user?.Policy?.IsAdministrator && ( + <> + + + {globalize.translate('HeaderAdmin')} + + } + > + + + + + + + + + + + + + + + + + + + )} + + {/* FOOTER */} + + + + + + + + ); +}; + +export default MainDrawerContent; diff --git a/src/apps/experimental/components/drawers/ResponsiveDrawer.tsx b/src/apps/experimental/components/drawers/ResponsiveDrawer.tsx new file mode 100644 index 0000000000..1f7a554cf4 --- /dev/null +++ b/src/apps/experimental/components/drawers/ResponsiveDrawer.tsx @@ -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 = ({ + 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 */ + + + {children} + + ) : ( + /* MOBILE DRAWER */ + + + + {children} + + + )); +}; + +export default ResponsiveDrawer; diff --git a/src/apps/experimental/components/drawers/dashboard/AdvancedDrawerSection.tsx b/src/apps/experimental/components/drawers/dashboard/AdvancedDrawerSection.tsx new file mode 100644 index 0000000000..71e2c0d95f --- /dev/null +++ b/src/apps/experimental/components/drawers/dashboard/AdvancedDrawerSection.tsx @@ -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 ( + + {globalize.translate('TabAdvanced')} + + } + > + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + {isPluginSectionOpen ? : } + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default AdvancedDrawerSection; diff --git a/src/apps/experimental/components/drawers/dashboard/DevicesDrawerSection.tsx b/src/apps/experimental/components/drawers/dashboard/DevicesDrawerSection.tsx new file mode 100644 index 0000000000..ae1e7d4b85 --- /dev/null +++ b/src/apps/experimental/components/drawers/dashboard/DevicesDrawerSection.tsx @@ -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 ( + + {globalize.translate('HeaderDevices')} + + } + > + + + + + + + + + + + + + + + + + + + + + + + {isDlnaSectionOpen ? : } + + + + + + + + + + + + + + ); +}; + +export default DevicesDrawerSection; diff --git a/src/apps/experimental/components/drawers/dashboard/LiveTvDrawerSection.tsx b/src/apps/experimental/components/drawers/dashboard/LiveTvDrawerSection.tsx new file mode 100644 index 0000000000..505973b82c --- /dev/null +++ b/src/apps/experimental/components/drawers/dashboard/LiveTvDrawerSection.tsx @@ -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 ( + + {globalize.translate('LiveTV')} + + } + > + + + + + + + + + + + + + + + + + + ); +}; + +export default LiveTvDrawerSection; diff --git a/src/apps/experimental/components/drawers/dashboard/PluginDrawerSection.tsx b/src/apps/experimental/components/drawers/dashboard/PluginDrawerSection.tsx new file mode 100644 index 0000000000..8a368af8d5 --- /dev/null +++ b/src/apps/experimental/components/drawers/dashboard/PluginDrawerSection.tsx @@ -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([]); + + 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 ( + + {globalize.translate('TabPlugins')} + + } + > + { + pagesInfo.map(pageInfo => ( + + + + {/* TODO: Support different icons? */} + + + + + + )) + } + + ); +}; + +export default PluginDrawerSection; diff --git a/src/apps/experimental/components/drawers/dashboard/ServerDrawerSection.tsx b/src/apps/experimental/components/drawers/dashboard/ServerDrawerSection.tsx new file mode 100644 index 0000000000..388c1feeeb --- /dev/null +++ b/src/apps/experimental/components/drawers/dashboard/ServerDrawerSection.tsx @@ -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 ( + + {globalize.translate('TabServer')} + + } + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {isLibrarySectionOpen ? : } + + + + + + + + + + + + + + + + + + + + + + + + + {isPlaybackSectionOpen ? : } + + + + + + + + + + + + + + + + + ); +}; + +export default ServerDrawerSection; diff --git a/src/apps/experimental/components/tabs/AppTabs.tsx b/src/apps/experimental/components/tabs/AppTabs.tsx index 263e16bc15..383bf8ab21 100644 --- a/src/apps/experimental/components/tabs/AppTabs.tsx +++ b/src/apps/experimental/components/tabs/AppTabs.tsx @@ -2,12 +2,21 @@ 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 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 TabRoutes, { getDefaultTabIndex } from './tabRoutes'; -const AppTabs: FC = () => { +interface AppTabsParams { + isDrawerOpen: boolean +} + +const handleResize = debounce(() => window.dispatchEvent(new Event('resize')), 100); + +const AppTabs: FC = ({ + isDrawerOpen +}) => { const isBigScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm')); const location = useLocation(); const [ searchParams, setSearchParams ] = useSearchParams(); @@ -18,6 +27,12 @@ const AppTabs: FC = () => { parseInt(searchParamsTab, 10) : 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) => { event.preventDefault(); diff --git a/src/apps/experimental/routes/AppRoutes.tsx b/src/apps/experimental/routes/AppRoutes.tsx index e87476c554..8c01a02675 100644 --- a/src/apps/experimental/routes/AppRoutes.tsx +++ b/src/apps/experimental/routes/AppRoutes.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; -import ConnectionRequired from '../../../components/ConnectionRequired'; -import ServerContentPage from '../../../components/ServerContentPage'; -import { toAsyncPageRoute } from '../../../components/router/AsyncRoute'; -import { toViewManagerPageRoute } from '../../../components/router/LegacyRoute'; +import ConnectionRequired from 'components/ConnectionRequired'; +import ServerContentPage from 'components/ServerContentPage'; +import { toAsyncPageRoute } from 'components/router/AsyncRoute'; +import { toViewManagerPageRoute } from 'components/router/LegacyRoute'; + import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './asyncRoutes'; import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes'; diff --git a/src/types/collectionType.ts b/src/types/collectionType.ts new file mode 100644 index 0000000000..c2e3b9d9ff --- /dev/null +++ b/src/types/collectionType.ts @@ -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' +}