Add initial experimental UI based on MUI
This commit is contained in:
parent
6155524e2c
commit
69fe4f067a
14 changed files with 1793 additions and 233 deletions
1193
package-lock.json
generated
1193
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -65,6 +65,8 @@
|
|||
"worker-loader": "3.0.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.10.5",
|
||||
"@emotion/styled": "11.10.5",
|
||||
"@fontsource/noto-sans": "4.5.11",
|
||||
"@fontsource/noto-sans-hk": "4.5.12",
|
||||
"@fontsource/noto-sans-jp": "4.5.12",
|
||||
|
@ -73,6 +75,8 @@
|
|||
"@fontsource/noto-sans-tc": "4.5.12",
|
||||
"@jellyfin/sdk": "unstable",
|
||||
"@loadable/component": "5.15.3",
|
||||
"@mui/icons-material": "5.10.14",
|
||||
"@mui/material": "5.10.14",
|
||||
"blurhash": "2.0.5",
|
||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||
"classnames": "2.3.2",
|
||||
|
|
|
@ -1,19 +1,81 @@
|
|||
import React from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Box from '@mui/material/Box';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
|
||||
import AppHeader from '../../components/AppHeader';
|
||||
import Backdrop from '../../components/Backdrop';
|
||||
import AppHeader from 'components/AppHeader';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
|
||||
import AppToolbar from './components/AppToolbar';
|
||||
import AppUserMenu from './components/AppUserMenu';
|
||||
import ElevationScroll from './components/ElevationScroll';
|
||||
import { ExperimentalAppRoutes } from './routes/AppRoutes';
|
||||
import theme from './theme';
|
||||
|
||||
const ExperimentalApp = () => (
|
||||
<>
|
||||
import './AppOverrides.scss';
|
||||
|
||||
const ExperimentalApp = () => {
|
||||
const [ userMenuAnchorEl, setUserMenuAnchorEl ] = useState<null | HTMLElement>(null);
|
||||
const isUserMenuOpen = Boolean(userMenuAnchorEl);
|
||||
|
||||
const onUserButtonClick = useCallback((event) => {
|
||||
setUserMenuAnchorEl(event.currentTarget);
|
||||
}, [ setUserMenuAnchorEl ]);
|
||||
|
||||
const onUserMenuClose = useCallback(() => {
|
||||
setUserMenuAnchorEl(null);
|
||||
}, [ setUserMenuAnchorEl ]);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Backdrop />
|
||||
<AppHeader />
|
||||
|
||||
<div style={{ display: 'none' }}>
|
||||
{/*
|
||||
* TODO: These components are not used, but views interact with them directly so the need to be
|
||||
* present in the dom. We add them in a hidden element to prevent errors.
|
||||
*/}
|
||||
<AppHeader />
|
||||
</div>
|
||||
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<ElevationScroll>
|
||||
<AppBar
|
||||
position='fixed'
|
||||
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.drawer + 1 }}
|
||||
>
|
||||
<AppToolbar
|
||||
onUserButtonClick={onUserButtonClick}
|
||||
/>
|
||||
</AppBar>
|
||||
</ElevationScroll>
|
||||
|
||||
<Box
|
||||
component='main'
|
||||
sx={{
|
||||
width: '100%',
|
||||
flexGrow: 1,
|
||||
transition: theme.transitions.create('margin', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen
|
||||
}),
|
||||
marginLeft: 0
|
||||
}}
|
||||
>
|
||||
<div className='mainAnimatedPages skinBody' />
|
||||
<div className='skinBody'>
|
||||
<ExperimentalAppRoutes />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
</Box>
|
||||
|
||||
<AppUserMenu
|
||||
open={isUserMenuOpen}
|
||||
anchorEl={userMenuAnchorEl}
|
||||
onMenuClose={onUserMenuClose}
|
||||
/>
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExperimentalApp;
|
||||
|
|
40
src/apps/experimental/AppOverrides.scss
Normal file
40
src/apps/experimental/AppOverrides.scss
Normal file
|
@ -0,0 +1,40 @@
|
|||
// Default MUI breakpoints
|
||||
// https://mui.com/material-ui/customization/breakpoints/#default-breakpoints
|
||||
$mui-bp-sm: 600px;
|
||||
$mui-bp-md: 900px;
|
||||
$mui-bp-lg: 1200px;
|
||||
$mui-bp-xl: 1536px;
|
||||
|
||||
// Fix main pages layout to work with drawer
|
||||
.mainAnimatedPage {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Fix dashboard pages layout to work with drawer
|
||||
.dashboardDocument .skinBody {
|
||||
position: unset;
|
||||
}
|
||||
|
||||
// Hide some items from the user "settings" page that are in the drawer
|
||||
#myPreferencesMenuPage {
|
||||
.lnkQuickConnectPreferences,
|
||||
.adminSection,
|
||||
.userSection {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix the padding of some pages
|
||||
.homePage.libraryPage, // Home page
|
||||
.libraryPage:not(.withTabs), // Tabless library pages
|
||||
.content-primary.content-primary { // Dashboard pages
|
||||
padding-top: 3.25rem !important;
|
||||
}
|
||||
|
||||
.libraryPage.withTabs {
|
||||
padding-top: 6.5rem !important;
|
||||
|
||||
@media all and (min-width: $mui-bp-lg) {
|
||||
padding-top: 3.25rem !important;
|
||||
}
|
||||
}
|
118
src/apps/experimental/components/AppToolbar.tsx
Normal file
118
src/apps/experimental/components/AppToolbar.tsx
Normal file
|
@ -0,0 +1,118 @@
|
|||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
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 { useApi } from 'hooks/useApi';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import { ID as UserMenuId } from './AppUserMenu';
|
||||
import AppTabs from './tabs/AppTabs';
|
||||
|
||||
interface AppToolbarProps {
|
||||
onUserButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
||||
}
|
||||
|
||||
const AppToolbar: FC<AppToolbarProps> = ({
|
||||
onUserButtonClick
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { api, user } = useApi();
|
||||
const isUserLoggedIn = Boolean(user);
|
||||
|
||||
return (
|
||||
<Toolbar
|
||||
variant='dense'
|
||||
sx={{
|
||||
flexWrap: {
|
||||
xs: 'wrap',
|
||||
lg: 'nowrap'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component={Link}
|
||||
to='/'
|
||||
color='inherit'
|
||||
aria-label={globalize.translate('Home')}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
textDecoration: 'none'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component='img'
|
||||
src='/assets/img/icon-transparent.png'
|
||||
sx={{
|
||||
height: '2rem',
|
||||
marginInlineEnd: 1
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant='h6'
|
||||
noWrap
|
||||
component='div'
|
||||
sx={{ display: { xs: 'none', sm: 'inline-block' } }}
|
||||
>
|
||||
Jellyfin
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<AppTabs />
|
||||
|
||||
{isUserLoggedIn && (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'flex-end' }}>
|
||||
<Tooltip title={globalize.translate('Search')}>
|
||||
<IconButton
|
||||
size='large'
|
||||
aria-label={globalize.translate('Search')}
|
||||
color='inherit'
|
||||
component={Link}
|
||||
to='/search.html'
|
||||
>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flexGrow: 0 }}>
|
||||
<Tooltip title={globalize.translate('UserMenu')}>
|
||||
<IconButton
|
||||
size='large'
|
||||
edge='end'
|
||||
aria-label={globalize.translate('UserMenu')}
|
||||
aria-controls={UserMenuId}
|
||||
aria-haspopup='true'
|
||||
onClick={onUserButtonClick}
|
||||
color='inherit'
|
||||
sx={{ padding: 0 }}
|
||||
>
|
||||
<Avatar
|
||||
alt={user?.Name || undefined}
|
||||
src={
|
||||
api && user?.Id ?
|
||||
`${api.basePath}/Users/${user.Id}/Images/Primary?tag=${user.PrimaryImageTag}` :
|
||||
undefined
|
||||
}
|
||||
sx={{
|
||||
bgcolor: theme.palette.primary.dark,
|
||||
color: 'inherit'
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Toolbar>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppToolbar;
|
165
src/apps/experimental/components/AppUserMenu.tsx
Normal file
165
src/apps/experimental/components/AppUserMenu.tsx
Normal file
|
@ -0,0 +1,165 @@
|
|||
import { AppSettingsAlt, Close } from '@mui/icons-material';
|
||||
import AccountCircle from '@mui/icons-material/AccountCircle';
|
||||
import Logout from '@mui/icons-material/Logout';
|
||||
import PhonelinkLock from '@mui/icons-material/PhonelinkLock';
|
||||
import Settings from '@mui/icons-material/Settings';
|
||||
import Storage from '@mui/icons-material/Storage';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Menu, { MenuProps } from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { appHost } from 'components/apphost';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import globalize from 'scripts/globalize';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
|
||||
export const ID = 'app-user-menu';
|
||||
|
||||
interface AppUserMenuProps extends MenuProps {
|
||||
onMenuClose: () => void
|
||||
}
|
||||
|
||||
const AppUserMenu: FC<AppUserMenuProps> = ({
|
||||
anchorEl,
|
||||
open,
|
||||
onMenuClose
|
||||
}) => {
|
||||
const { user } = useApi();
|
||||
|
||||
const onClientSettingsClick = useCallback(() => {
|
||||
window.NativeShell?.openClientSettings();
|
||||
onMenuClose();
|
||||
}, [ onMenuClose ]);
|
||||
|
||||
const onExitAppClick = useCallback(() => {
|
||||
appHost.exit();
|
||||
onMenuClose();
|
||||
}, [ onMenuClose ]);
|
||||
|
||||
const onLogoutClick = useCallback(() => {
|
||||
Dashboard.logout();
|
||||
onMenuClose();
|
||||
}, [ onMenuClose ]);
|
||||
|
||||
const onSelectServerClick = useCallback(() => {
|
||||
Dashboard.selectServer();
|
||||
onMenuClose();
|
||||
}, [ onMenuClose ]);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
id={ID}
|
||||
keepMounted
|
||||
open={open}
|
||||
onClose={onMenuClose}
|
||||
>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={`/userprofile.html?userId=${user?.Id}`}
|
||||
onClick={onMenuClose}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<AccountCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('Profile')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to='/mypreferencesmenu.html'
|
||||
onClick={onMenuClose}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Settings />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('Settings')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
{appHost.supports('clientsettings') && ([
|
||||
<Divider key='client-settings-divider' />,
|
||||
<MenuItem
|
||||
key='client-settings-button'
|
||||
onClick={onClientSettingsClick}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<AppSettingsAlt />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('ClientSettings')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
])}
|
||||
|
||||
<Divider />
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to='/mypreferencesquickconnect.html'
|
||||
onClick={onMenuClose}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<PhonelinkLock />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('QuickConnect')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
{appHost.supports('multiserver') && (
|
||||
<MenuItem
|
||||
onClick={onSelectServerClick}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Storage />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('SelectServer')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
onClick={onLogoutClick}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Logout />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('ButtonSignOut')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
{appHost.supports('exitmenu') && ([
|
||||
<Divider key='exit-menu-divider' />,
|
||||
<MenuItem
|
||||
key='exit-menu-button'
|
||||
onClick={onExitAppClick}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Close />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('ButtonExitApp')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
])}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppUserMenu;
|
21
src/apps/experimental/components/ElevationScroll.tsx
Normal file
21
src/apps/experimental/components/ElevationScroll.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import useScrollTrigger from '@mui/material/useScrollTrigger';
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
/**
|
||||
* Component that changes the elevation of a child component when scrolled.
|
||||
*/
|
||||
const ElevationScroll = ({ children, elevate = false }: { children: ReactElement, elevate?: boolean }) => {
|
||||
const trigger = useScrollTrigger({
|
||||
disableHysteresis: true,
|
||||
threshold: 0
|
||||
});
|
||||
|
||||
const isElevated = elevate || trigger;
|
||||
|
||||
return React.cloneElement(children, {
|
||||
color: isElevated ? 'primary' : 'transparent',
|
||||
elevation: isElevated ? 4 : 0
|
||||
});
|
||||
};
|
||||
|
||||
export default ElevationScroll;
|
78
src/apps/experimental/components/tabs/AppTabs.tsx
Normal file
78
src/apps/experimental/components/tabs/AppTabs.tsx
Normal file
|
@ -0,0 +1,78 @@
|
|||
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 { Route, Routes, useLocation, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import TabRoutes, { getDefaultTabIndex } from './tabRoutes';
|
||||
|
||||
const AppTabs: FC = () => {
|
||||
const isBigScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
|
||||
const location = useLocation();
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
const searchParamsTab = searchParams.get('tab');
|
||||
const libraryId = location.pathname === '/livetv.html' ?
|
||||
'livetv' : searchParams.get('topParentId');
|
||||
const activeTab = searchParamsTab !== null ?
|
||||
parseInt(searchParamsTab, 10) :
|
||||
getDefaultTabIndex(location.pathname, libraryId);
|
||||
|
||||
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}-tab-${index}`}
|
||||
label={label}
|
||||
data-tab-index={`${index}`}
|
||||
onClick={onTabClick}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Tabs>
|
||||
}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
{/* Suppress warnings for unhandled routes */}
|
||||
<Route path='*' element={null} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppTabs;
|
190
src/apps/experimental/components/tabs/tabRoutes.ts
Normal file
190
src/apps/experimental/components/tabs/tabRoutes.ts
Normal file
|
@ -0,0 +1,190 @@
|
|||
import globalize from 'scripts/globalize';
|
||||
import * as userSettings from 'scripts/settings/userSettings';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
|
||||
interface TabDefinition {
|
||||
index: number
|
||||
label: string
|
||||
value: LibraryTab
|
||||
isDefault?: boolean
|
||||
}
|
||||
|
||||
interface TabRoute {
|
||||
path: string,
|
||||
tabs: TabDefinition[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to check if a path has tabs.
|
||||
*/
|
||||
export const isTabPath = (path: string) => (
|
||||
TabRoutes.some(route => route.path === path)
|
||||
);
|
||||
|
||||
/**
|
||||
* Utility function to get the default tab index for a specified URL path and library.
|
||||
*/
|
||||
export const getDefaultTabIndex = (path: string, libraryId?: string | null) => {
|
||||
if (!libraryId) return 0;
|
||||
|
||||
const tabs = TabRoutes.find(route => route.path === path)?.tabs ?? [];
|
||||
const defaultTab = userSettings.get('landing-' + libraryId, false);
|
||||
|
||||
return tabs.find(tab => tab.value === defaultTab)?.index
|
||||
?? tabs.find(tab => tab.isDefault)?.index
|
||||
?? 0;
|
||||
};
|
||||
|
||||
const TabRoutes: TabRoute[] = [
|
||||
{
|
||||
path: '/livetv.html',
|
||||
tabs: [
|
||||
{
|
||||
index: 0,
|
||||
label: globalize.translate('Programs'),
|
||||
value: LibraryTab.Programs,
|
||||
isDefault: true
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
label: globalize.translate('Guide'),
|
||||
value: LibraryTab.Guide
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
label: globalize.translate('Channels'),
|
||||
value: LibraryTab.Channels
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
label: globalize.translate('Recordings'),
|
||||
value: LibraryTab.Recordings
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
label: globalize.translate('Schedule'),
|
||||
value: LibraryTab.Schedule
|
||||
},
|
||||
{
|
||||
index: 5,
|
||||
label: globalize.translate('Series'),
|
||||
value: LibraryTab.Series
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/movies.html',
|
||||
tabs: [
|
||||
{
|
||||
index: 0,
|
||||
label: globalize.translate('Movies'),
|
||||
value: LibraryTab.Movies,
|
||||
isDefault: true
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
label: globalize.translate('Suggestions'),
|
||||
value: LibraryTab.Suggestions
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
label: globalize.translate('Trailers'),
|
||||
value: LibraryTab.Trailers
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
label: globalize.translate('Favorites'),
|
||||
value: LibraryTab.Favorites
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
label: globalize.translate('Collections'),
|
||||
value: LibraryTab.Collections
|
||||
},
|
||||
{
|
||||
index: 5,
|
||||
label: globalize.translate('Genres'),
|
||||
value: LibraryTab.Genres
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/music.html',
|
||||
tabs: [
|
||||
{
|
||||
index: 0,
|
||||
label: globalize.translate('Albums'),
|
||||
value: LibraryTab.Albums,
|
||||
isDefault: true
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
label: globalize.translate('Suggestions'),
|
||||
value: LibraryTab.Suggestions
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
label: globalize.translate('HeaderAlbumArtists'),
|
||||
value: LibraryTab.AlbumArtists
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
label: globalize.translate('Artists'),
|
||||
value: LibraryTab.Artists
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
label: globalize.translate('Playlists'),
|
||||
value: LibraryTab.Playlists
|
||||
},
|
||||
{
|
||||
index: 5,
|
||||
label: globalize.translate('Songs'),
|
||||
value: LibraryTab.Songs
|
||||
},
|
||||
{
|
||||
index: 6,
|
||||
label: globalize.translate('Genres'),
|
||||
value: LibraryTab.Genres
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/tv.html',
|
||||
tabs: [
|
||||
{
|
||||
index: 0,
|
||||
label: globalize.translate('Shows'),
|
||||
value: LibraryTab.Shows,
|
||||
isDefault: true
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
label: globalize.translate('Suggestions'),
|
||||
value: LibraryTab.Suggestions
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
label: globalize.translate('TabUpcoming'),
|
||||
value: LibraryTab.Upcoming
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
label: globalize.translate('Genres'),
|
||||
value: LibraryTab.Genres
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
label: globalize.translate('TabNetworks'),
|
||||
value: LibraryTab.Networks
|
||||
},
|
||||
{
|
||||
index: 5,
|
||||
label: globalize.translate('Episodes'),
|
||||
value: LibraryTab.Episodes
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export default TabRoutes;
|
|
@ -3,60 +3,26 @@ import '../../../../elements/emby-itemscontainer/emby-itemscontainer';
|
|||
import '../../../../elements/emby-tabs/emby-tabs';
|
||||
import '../../../../elements/emby-button/emby-button';
|
||||
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import React, { FC, useEffect, useRef } from 'react';
|
||||
import { useLocation, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import * as mainTabsManager from '../../../../components/maintabsmanager';
|
||||
import Page from '../../../../components/Page';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import libraryMenu from '../../../../scripts/libraryMenu';
|
||||
import * as userSettings from '../../../../scripts/settings/userSettings';
|
||||
import CollectionsView from './CollectionsView';
|
||||
import FavoritesView from './FavoritesView';
|
||||
import GenresView from './GenresView';
|
||||
import MoviesView from './MoviesView';
|
||||
import SuggestionsView from './SuggestionsView';
|
||||
import TrailersView from './TrailersView';
|
||||
|
||||
const getDefaultTabIndex = (folderId: string | null) => {
|
||||
switch (userSettings.get('landing-' + folderId, false)) {
|
||||
case 'suggestions':
|
||||
return 1;
|
||||
|
||||
case 'favorites':
|
||||
return 3;
|
||||
|
||||
case 'collections':
|
||||
return 4;
|
||||
|
||||
case 'genres':
|
||||
return 5;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const getTabs = () => {
|
||||
return [{
|
||||
name: globalize.translate('Movies')
|
||||
}, {
|
||||
name: globalize.translate('Suggestions')
|
||||
}, {
|
||||
name: globalize.translate('Trailers')
|
||||
}, {
|
||||
name: globalize.translate('Favorites')
|
||||
}, {
|
||||
name: globalize.translate('Collections')
|
||||
}, {
|
||||
name: globalize.translate('Genres')
|
||||
}];
|
||||
};
|
||||
import { getDefaultTabIndex } from '../../components/tabs/tabRoutes';
|
||||
|
||||
const Movies: FC = () => {
|
||||
const location = useLocation();
|
||||
const [ searchParams ] = useSearchParams();
|
||||
const currentTabIndex = parseInt(searchParams.get('tab') || getDefaultTabIndex(searchParams.get('topParentId')).toString(), 10);
|
||||
const [ selectedIndex, setSelectedIndex ] = useState(currentTabIndex);
|
||||
const searchParamsTab = searchParams.get('tab');
|
||||
const currentTabIndex = searchParamsTab !== null ? parseInt(searchParamsTab, 10) :
|
||||
getDefaultTabIndex(location.pathname, searchParams.get('topParentId'));
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const getTabComponent = (index: number) => {
|
||||
|
@ -94,11 +60,6 @@ const Movies: FC = () => {
|
|||
return component;
|
||||
};
|
||||
|
||||
const onTabChange = useCallback((e: { detail: { selectedTabIndex: string; }; }) => {
|
||||
const newIndex = parseInt(e.detail.selectedTabIndex, 10);
|
||||
setSelectedIndex(newIndex);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const page = element.current;
|
||||
|
||||
|
@ -106,7 +67,7 @@ const Movies: FC = () => {
|
|||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
mainTabsManager.setTabs(page, selectedIndex, getTabs, undefined, undefined, onTabChange);
|
||||
|
||||
if (!page.getAttribute('data-title')) {
|
||||
const parentId = searchParams.get('topParentId');
|
||||
|
||||
|
@ -116,13 +77,15 @@ const Movies: FC = () => {
|
|||
libraryMenu.setTitle(item.Name);
|
||||
}).catch(err => {
|
||||
console.error('[movies] failed to fetch library', err);
|
||||
page.setAttribute('data-title', globalize.translate('Movies'));
|
||||
libraryMenu.setTitle(globalize.translate('Movies'));
|
||||
});
|
||||
} else {
|
||||
page.setAttribute('data-title', globalize.translate('Movies'));
|
||||
libraryMenu.setTitle(globalize.translate('Movies'));
|
||||
}
|
||||
}
|
||||
}, [onTabChange, searchParams, selectedIndex]);
|
||||
}, [ searchParams ]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
|
@ -131,7 +94,7 @@ const Movies: FC = () => {
|
|||
className='mainAnimatedPage libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs'
|
||||
backDropType='movie'
|
||||
>
|
||||
{getTabComponent(selectedIndex)}
|
||||
{getTabComponent(currentTabIndex)}
|
||||
|
||||
</Page>
|
||||
</div>
|
||||
|
|
53
src/apps/experimental/theme.ts
Normal file
53
src/apps/experimental/theme.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { createTheme } from '@mui/material/styles';
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: {
|
||||
main: '#00a4dc'
|
||||
},
|
||||
secondary: {
|
||||
main: '#aa5cc3'
|
||||
},
|
||||
background: {
|
||||
default: '#101010',
|
||||
paper: '#202020'
|
||||
},
|
||||
action: {
|
||||
selectedOpacity: 0.2
|
||||
}
|
||||
},
|
||||
typography: {
|
||||
fontFamily: '"Noto Sans", sans-serif',
|
||||
button: {
|
||||
textTransform: 'none'
|
||||
}
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
defaultProps: {
|
||||
variant: 'contained'
|
||||
}
|
||||
},
|
||||
MuiFormControl: {
|
||||
defaultProps: {
|
||||
variant: 'filled'
|
||||
}
|
||||
},
|
||||
MuiTextField: {
|
||||
defaultProps: {
|
||||
variant: 'filled'
|
||||
}
|
||||
},
|
||||
MuiListSubheader: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
// NOTE: Added for drawer subheaders, but maybe it won't work in other cases?
|
||||
backgroundColor: 'inherit'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default theme;
|
1
src/global.d.ts
vendored
1
src/global.d.ts
vendored
|
@ -4,5 +4,6 @@ export declare global {
|
|||
interface Window {
|
||||
ApiClient: ApiClient;
|
||||
Events: Events;
|
||||
NativeShell: any;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1047,6 +1047,8 @@
|
|||
"MediaInfoVideoRange": "Video range",
|
||||
"MediaIsBeingConverted": "The media is being converted into a format that is compatible with the device that is playing the media.",
|
||||
"Menu": "Menu",
|
||||
"MenuOpen": "Open Menu",
|
||||
"MenuClose": "Close Menu",
|
||||
"MessageAddRepository": "If you wish to add a repository, click the button next to the header and fill out the requested information.",
|
||||
"MessageAlreadyInstalled": "This version is already installed.",
|
||||
"MessageAreYouSureDeleteSubtitles": "Are you sure you wish to delete this subtitle file?",
|
||||
|
@ -1560,6 +1562,7 @@
|
|||
"UseEpisodeImagesInNextUp": "Use episode images in 'Next Up' and 'Continue Watching' sections",
|
||||
"UseEpisodeImagesInNextUpHelp": "'Next Up' and 'Continue Watching' sections will use episode images as thumbnails instead of the primary thumbnail of the show.",
|
||||
"UserAgentHelp": "Supply a custom 'User-Agent' HTTP header.",
|
||||
"UserMenu": "User Menu",
|
||||
"UserProfilesIntro": "Jellyfin includes support for user profiles with granular display settings, play state, and parental controls.",
|
||||
"ValueAlbumCount": "{0} albums",
|
||||
"ValueAudioCodec": "Audio Codec: {0}",
|
||||
|
|
|
@ -33,7 +33,16 @@ const config = {
|
|||
modules: [
|
||||
path.resolve(__dirname, 'src'),
|
||||
path.resolve(__dirname, 'node_modules')
|
||||
]
|
||||
],
|
||||
alias: {
|
||||
'@mui/base': '@mui/base/legacy',
|
||||
'@mui/lab': '@mui/lab/legacy',
|
||||
'@mui/material': '@mui/material/legacy',
|
||||
'@mui/private-theming': '@mui/private-theming/legacy',
|
||||
'@mui/styled-engine': '@mui/styled-engine/legacy',
|
||||
'@mui/system': '@mui/system/legacy',
|
||||
'@mui/utils': '@mui/utils/legacy'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new DefinePlugin({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue