Merge branch 'master' into segment-deletion
This commit is contained in:
commit
128184cc72
497 changed files with 70077 additions and 54756 deletions
30
src/RootApp.tsx
Normal file
30
src/RootApp.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import loadable from '@loadable/component';
|
||||
import { History } from '@remix-run/router';
|
||||
import React from 'react';
|
||||
|
||||
import StableApp from './apps/stable/App';
|
||||
import { HistoryRouter } from './components/router/HistoryRouter';
|
||||
import { ApiProvider } from './hooks/useApi';
|
||||
import { WebConfigProvider } from './hooks/useWebConfig';
|
||||
|
||||
const ExperimentalApp = loadable(() => import('./apps/experimental/App'));
|
||||
|
||||
const RootApp = ({ history }: { history: History }) => {
|
||||
const layoutMode = localStorage.getItem('layout');
|
||||
|
||||
return (
|
||||
<ApiProvider>
|
||||
<WebConfigProvider>
|
||||
<HistoryRouter history={history}>
|
||||
{
|
||||
layoutMode === 'experimental' ?
|
||||
<ExperimentalApp /> :
|
||||
<StableApp />
|
||||
}
|
||||
</HistoryRouter>
|
||||
</WebConfigProvider>
|
||||
</ApiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default RootApp;
|
3
src/apiclient.d.ts
vendored
3
src/apiclient.d.ts
vendored
|
@ -117,6 +117,7 @@ declare module 'jellyfin-apiclient' {
|
|||
getCountries(): Promise<CountryInfo[]>;
|
||||
getCriticReviews(itemId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getCultures(): Promise<CultureDto[]>;
|
||||
getCurrentUser(cache?: boolean): Promise<UserDto>;
|
||||
getCurrentUserId(): string;
|
||||
getDateParamValue(date: Date): string;
|
||||
getDefaultImageQuality(imageType: ImageType): number;
|
||||
|
@ -267,7 +268,7 @@ declare module 'jellyfin-apiclient' {
|
|||
sendWebSocketMessage(name: string, data: any): void;
|
||||
serverAddress(val?: string): string;
|
||||
serverId(): string;
|
||||
serverVersion(): string
|
||||
serverVersion(): string;
|
||||
setAuthenticationInfo(accessKey?: string, userId?: string): void;
|
||||
setRequestHeaders(headers: any): void;
|
||||
setSystemInfo(info: SystemInfo): void;
|
||||
|
|
44
src/apps/experimental/App.tsx
Normal file
44
src/apps/experimental/App.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
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 AppLayout from './AppLayout';
|
||||
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
||||
|
||||
const ExperimentalApp = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path='/*' element={<AppLayout />}>
|
||||
{/* User routes */}
|
||||
<Route element={<ConnectionRequired />}>
|
||||
{ASYNC_USER_ROUTES.map(toAsyncPageRoute)}
|
||||
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
|
||||
</Route>
|
||||
|
||||
{/* Admin routes */}
|
||||
<Route element={<ConnectionRequired isAdminRequired />}>
|
||||
{ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)}
|
||||
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
|
||||
|
||||
<Route path='configurationpage' element={
|
||||
<ServerContentPage view='/web/configurationpage' />
|
||||
} />
|
||||
</Route>
|
||||
|
||||
{/* Public routes */}
|
||||
<Route element={<ConnectionRequired isUserRequired={false} />}>
|
||||
<Route index element={<Navigate replace to='/home.html' />} />
|
||||
|
||||
{LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)}
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExperimentalApp;
|
114
src/apps/experimental/AppLayout.tsx
Normal file
114
src/apps/experimental/AppLayout.tsx
Normal file
|
@ -0,0 +1,114 @@
|
|||
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 { Outlet, 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 AppDrawer, { DRAWER_WIDTH, isDrawerPath } from './components/drawers/AppDrawer';
|
||||
import ElevationScroll from './components/ElevationScroll';
|
||||
import theme from './theme';
|
||||
|
||||
import './AppOverrides.scss';
|
||||
|
||||
interface ExperimentalAppSettings {
|
||||
isDrawerPinned: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_EXPERIMENTAL_APP_SETTINGS: ExperimentalAppSettings = {
|
||||
isDrawerPinned: false
|
||||
};
|
||||
|
||||
const AppLayout = () => {
|
||||
const [ appSettings, setAppSettings ] = useLocalStorage<ExperimentalAppSettings>('ExperimentalAppSettings', DEFAULT_EXPERIMENTAL_APP_SETTINGS);
|
||||
const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned);
|
||||
const { user } = useApi();
|
||||
const location = useLocation();
|
||||
|
||||
const isDrawerAvailable = isDrawerPath(location.pathname);
|
||||
const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDrawerActive !== appSettings.isDrawerPinned) {
|
||||
setAppSettings({
|
||||
...appSettings,
|
||||
isDrawerPinned: isDrawerActive
|
||||
});
|
||||
}
|
||||
}, [ appSettings, isDrawerActive, setAppSettings ]);
|
||||
|
||||
const onToggleDrawer = useCallback(() => {
|
||||
setIsDrawerActive(!isDrawerActive);
|
||||
}, [ isDrawerActive, setIsDrawerActive ]);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Backdrop />
|
||||
|
||||
<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 elevate={isDrawerOpen}>
|
||||
<AppBar
|
||||
position='fixed'
|
||||
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.drawer + 1 }}
|
||||
>
|
||||
<AppToolbar
|
||||
isDrawerOpen={isDrawerOpen}
|
||||
onDrawerButtonClick={onToggleDrawer}
|
||||
/>
|
||||
</AppBar>
|
||||
</ElevationScroll>
|
||||
|
||||
<AppDrawer
|
||||
open={isDrawerOpen}
|
||||
onClose={onToggleDrawer}
|
||||
onOpen={onToggleDrawer}
|
||||
/>
|
||||
|
||||
<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,
|
||||
...(isDrawerAvailable && {
|
||||
marginLeft: {
|
||||
sm: `-${DRAWER_WIDTH}px`
|
||||
}
|
||||
}),
|
||||
...(isDrawerActive && {
|
||||
transition: theme.transitions.create('margin', {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen
|
||||
}),
|
||||
marginLeft: 0
|
||||
})
|
||||
}}
|
||||
>
|
||||
<div className='mainAnimatedPages skinBody' />
|
||||
<div className='skinBody'>
|
||||
<Outlet />
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
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;
|
||||
}
|
||||
}
|
112
src/apps/experimental/components/AppToolbar/RemotePlayButton.tsx
Normal file
112
src/apps/experimental/components/AppToolbar/RemotePlayButton.tsx
Normal file
|
@ -0,0 +1,112 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import CastConnected from '@mui/icons-material/CastConnected';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Cast from '@mui/icons-material/Cast';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import globalize from 'scripts/globalize';
|
||||
import Events from 'utils/events';
|
||||
|
||||
import RemotePlayMenu, { ID } from './menus/RemotePlayMenu';
|
||||
import RemotePlayActiveMenu, { ID as ACTIVE_ID } from './menus/RemotePlayActiveMenu';
|
||||
|
||||
const RemotePlayButton = () => {
|
||||
const theme = useTheme();
|
||||
const [ playerInfo, setPlayerInfo ] = useState(playbackManager.getPlayerInfo());
|
||||
|
||||
const updatePlayerInfo = useCallback(() => {
|
||||
setPlayerInfo(playbackManager.getPlayerInfo());
|
||||
}, [ setPlayerInfo ]);
|
||||
|
||||
useEffect(() => {
|
||||
Events.on(playbackManager, 'playerchange', updatePlayerInfo);
|
||||
|
||||
return () => {
|
||||
Events.off(playbackManager, 'playerchange', updatePlayerInfo);
|
||||
};
|
||||
}, [ updatePlayerInfo ]);
|
||||
|
||||
const [ remotePlayMenuAnchorEl, setRemotePlayMenuAnchorEl ] = useState<null | HTMLElement>(null);
|
||||
const isRemotePlayMenuOpen = Boolean(remotePlayMenuAnchorEl);
|
||||
|
||||
const onRemotePlayButtonClick = useCallback((event) => {
|
||||
setRemotePlayMenuAnchorEl(event.currentTarget);
|
||||
}, [ setRemotePlayMenuAnchorEl ]);
|
||||
|
||||
const onRemotePlayMenuClose = useCallback(() => {
|
||||
setRemotePlayMenuAnchorEl(null);
|
||||
}, [ setRemotePlayMenuAnchorEl ]);
|
||||
|
||||
const [ remotePlayActiveMenuAnchorEl, setRemotePlayActiveMenuAnchorEl ] = useState<null | HTMLElement>(null);
|
||||
const isRemotePlayActiveMenuOpen = Boolean(remotePlayActiveMenuAnchorEl);
|
||||
|
||||
const onRemotePlayActiveButtonClick = useCallback((event) => {
|
||||
setRemotePlayActiveMenuAnchorEl(event.currentTarget);
|
||||
}, [ setRemotePlayActiveMenuAnchorEl ]);
|
||||
|
||||
const onRemotePlayActiveMenuClose = useCallback(() => {
|
||||
setRemotePlayActiveMenuAnchorEl(null);
|
||||
}, [ setRemotePlayActiveMenuAnchorEl ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(playerInfo && !playerInfo.isLocalPlayer) ? (
|
||||
<Box
|
||||
sx={{
|
||||
alignSelf: 'center'
|
||||
}}
|
||||
>
|
||||
<Tooltip title={globalize.translate('ButtonCast')}>
|
||||
<Button
|
||||
variant='text'
|
||||
size='large'
|
||||
startIcon={<CastConnected />}
|
||||
aria-label={globalize.translate('ButtonCast')}
|
||||
aria-controls={ACTIVE_ID}
|
||||
aria-haspopup='true'
|
||||
onClick={onRemotePlayActiveButtonClick}
|
||||
color='inherit'
|
||||
sx={{
|
||||
color: theme.palette.primary.main
|
||||
}}
|
||||
>
|
||||
{playerInfo.deviceName || playerInfo.name}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
) : (
|
||||
<Tooltip title={globalize.translate('ButtonCast')}>
|
||||
<IconButton
|
||||
size='large'
|
||||
aria-label={globalize.translate('ButtonCast')}
|
||||
aria-controls={ID}
|
||||
aria-haspopup='true'
|
||||
onClick={onRemotePlayButtonClick}
|
||||
color='inherit'
|
||||
>
|
||||
<Cast />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<RemotePlayMenu
|
||||
open={isRemotePlayMenuOpen}
|
||||
anchorEl={remotePlayMenuAnchorEl}
|
||||
onMenuClose={onRemotePlayMenuClose}
|
||||
/>
|
||||
|
||||
<RemotePlayActiveMenu
|
||||
open={isRemotePlayActiveMenuOpen}
|
||||
anchorEl={remotePlayActiveMenuAnchorEl}
|
||||
onMenuClose={onRemotePlayActiveMenuClose}
|
||||
playerInfo={playerInfo}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemotePlayButton;
|
|
@ -0,0 +1,64 @@
|
|||
import Avatar from '@mui/material/Avatar';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import AppUserMenu, { ID } from './menus/AppUserMenu';
|
||||
|
||||
const UserMenuButton = () => {
|
||||
const theme = useTheme();
|
||||
const { api, user } = useApi();
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Tooltip title={globalize.translate('UserMenu')}>
|
||||
<IconButton
|
||||
size='large'
|
||||
edge='end'
|
||||
aria-label={globalize.translate('UserMenu')}
|
||||
aria-controls={ID}
|
||||
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>
|
||||
|
||||
<AppUserMenu
|
||||
open={isUserMenuOpen}
|
||||
anchorEl={userMenuAnchorEl}
|
||||
onMenuClose={onUserMenuClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserMenuButton;
|
117
src/apps/experimental/components/AppToolbar/index.tsx
Normal file
117
src/apps/experimental/components/AppToolbar/index.tsx
Normal file
|
@ -0,0 +1,117 @@
|
|||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import Box from '@mui/material/Box';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
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, useLocation } from 'react-router-dom';
|
||||
|
||||
import appIcon from 'assets/img/icon-transparent.png';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import AppTabs from '../tabs/AppTabs';
|
||||
import { isDrawerPath } from '../drawers/AppDrawer';
|
||||
import UserMenuButton from './UserMenuButton';
|
||||
import RemotePlayButton from './RemotePlayButton';
|
||||
|
||||
interface AppToolbarProps {
|
||||
isDrawerOpen: boolean
|
||||
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
||||
}
|
||||
|
||||
const AppToolbar: FC<AppToolbarProps> = ({
|
||||
isDrawerOpen,
|
||||
onDrawerButtonClick
|
||||
}) => {
|
||||
const { user } = useApi();
|
||||
const isUserLoggedIn = Boolean(user);
|
||||
const location = useLocation();
|
||||
|
||||
const isDrawerAvailable = isDrawerPath(location.pathname);
|
||||
|
||||
return (
|
||||
<Toolbar
|
||||
variant='dense'
|
||||
sx={{
|
||||
flexWrap: {
|
||||
xs: 'wrap',
|
||||
lg: 'nowrap'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isUserLoggedIn && isDrawerAvailable && (
|
||||
<Tooltip title={globalize.translate(isDrawerOpen ? 'MenuClose' : 'MenuOpen')}>
|
||||
<IconButton
|
||||
size='large'
|
||||
edge='start'
|
||||
color='inherit'
|
||||
aria-label={globalize.translate(isDrawerOpen ? 'MenuClose' : 'MenuOpen')}
|
||||
sx={{ mr: 2 }}
|
||||
onClick={onDrawerButtonClick}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Box
|
||||
component={Link}
|
||||
to='/'
|
||||
color='inherit'
|
||||
aria-label={globalize.translate('Home')}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
textDecoration: 'none'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component='img'
|
||||
src={appIcon}
|
||||
sx={{
|
||||
height: '2rem',
|
||||
marginInlineEnd: 1
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant='h6'
|
||||
noWrap
|
||||
component='div'
|
||||
sx={{ display: { xs: 'none', sm: 'inline-block' } }}
|
||||
>
|
||||
Jellyfin
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<AppTabs isDrawerOpen={isDrawerOpen} />
|
||||
|
||||
{isUserLoggedIn && (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'flex-end' }}>
|
||||
<RemotePlayButton />
|
||||
|
||||
<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 }}>
|
||||
<UserMenuButton />
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Toolbar>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppToolbar;
|
|
@ -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;
|
|
@ -0,0 +1,160 @@
|
|||
import Check from '@mui/icons-material/Check';
|
||||
import Close from '@mui/icons-material/Close';
|
||||
import SettingsRemote from '@mui/icons-material/SettingsRemote';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import Menu, { MenuProps } from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import dialog from 'components/dialog/dialog';
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { enable, isEnabled, supported } from 'scripts/autocast';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
interface RemotePlayActiveMenuProps extends MenuProps {
|
||||
onMenuClose: () => void
|
||||
playerInfo: {
|
||||
name: string
|
||||
isLocalPlayer: boolean
|
||||
id?: string
|
||||
deviceName?: string
|
||||
playableMediaTypes?: string[]
|
||||
supportedCommands?: string[]
|
||||
} | null
|
||||
}
|
||||
|
||||
export const ID = 'app-remote-play-active-menu';
|
||||
|
||||
const RemotePlayActiveMenu: FC<RemotePlayActiveMenuProps> = ({
|
||||
anchorEl,
|
||||
open,
|
||||
onMenuClose,
|
||||
playerInfo
|
||||
}) => {
|
||||
const [ isDisplayMirrorEnabled, setIsDisplayMirrorEnabled ] = useState(playbackManager.enableDisplayMirroring());
|
||||
const isDisplayMirrorSupported = playerInfo?.supportedCommands && playerInfo.supportedCommands.indexOf('DisplayContent') !== -1;
|
||||
const toggleDisplayMirror = useCallback(() => {
|
||||
playbackManager.enableDisplayMirroring(!isDisplayMirrorEnabled);
|
||||
setIsDisplayMirrorEnabled(!isDisplayMirrorEnabled);
|
||||
}, [ isDisplayMirrorEnabled, setIsDisplayMirrorEnabled ]);
|
||||
|
||||
const [ isAutoCastEnabled, setIsAutoCastEnabled ] = useState(isEnabled());
|
||||
const isAutoCastSupported = supported();
|
||||
const toggleAutoCast = useCallback(() => {
|
||||
enable(!isAutoCastEnabled);
|
||||
setIsAutoCastEnabled(!isAutoCastEnabled);
|
||||
}, [ isAutoCastEnabled, setIsAutoCastEnabled ]);
|
||||
|
||||
const remotePlayerName = playerInfo?.deviceName || playerInfo?.name;
|
||||
|
||||
const disconnectRemotePlayer = useCallback(() => {
|
||||
if (playbackManager.getSupportedCommands().indexOf('EndSession') !== -1) {
|
||||
dialog.show({
|
||||
buttons: [
|
||||
{
|
||||
name: globalize.translate('Yes'),
|
||||
id: 'yes'
|
||||
}, {
|
||||
name: globalize.translate('No'),
|
||||
id: 'no'
|
||||
}
|
||||
],
|
||||
text: globalize.translate('ConfirmEndPlayerSession', remotePlayerName)
|
||||
}).then(id => {
|
||||
onMenuClose();
|
||||
|
||||
if (id === 'yes') {
|
||||
playbackManager.getCurrentPlayer().endSession();
|
||||
}
|
||||
playbackManager.setDefaultPlayerActive();
|
||||
}).catch(() => {
|
||||
// Dialog closed
|
||||
});
|
||||
} else {
|
||||
onMenuClose();
|
||||
playbackManager.setDefaultPlayerActive();
|
||||
}
|
||||
}, [ onMenuClose, remotePlayerName ]);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
id={ID}
|
||||
keepMounted
|
||||
open={open}
|
||||
onClose={onMenuClose}
|
||||
MenuListProps={{
|
||||
'aria-labelledby': 'remote-play-active-subheader',
|
||||
subheader: (
|
||||
<ListSubheader component='div' id='remote-play-active-subheader'>
|
||||
{remotePlayerName}
|
||||
</ListSubheader>
|
||||
)
|
||||
}}
|
||||
>
|
||||
{isDisplayMirrorSupported && (
|
||||
<MenuItem onClick={toggleDisplayMirror}>
|
||||
{isDisplayMirrorEnabled && (
|
||||
<ListItemIcon>
|
||||
<Check />
|
||||
</ListItemIcon>
|
||||
)}
|
||||
<ListItemText inset={!isDisplayMirrorEnabled}>
|
||||
{globalize.translate('EnableDisplayMirroring')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{isAutoCastSupported && (
|
||||
<MenuItem onClick={toggleAutoCast}>
|
||||
{isAutoCastEnabled && (
|
||||
<ListItemIcon>
|
||||
<Check />
|
||||
</ListItemIcon>
|
||||
)}
|
||||
<ListItemText inset={!isAutoCastEnabled}>
|
||||
{globalize.translate('EnableAutoCast')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{(isDisplayMirrorSupported || isAutoCastSupported) && <Divider />}
|
||||
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to='/queue'
|
||||
onClick={onMenuClose}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<SettingsRemote />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('HeaderRemoteControl')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem onClick={disconnectRemotePlayer}>
|
||||
<ListItemIcon>
|
||||
<Close />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('Disconnect')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemotePlayActiveMenu;
|
|
@ -0,0 +1,100 @@
|
|||
import Warning from '@mui/icons-material/Warning';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Menu, { type MenuProps } from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
|
||||
import globalize from 'scripts/globalize';
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import { pluginManager } from 'components/pluginManager';
|
||||
import type { PlayTarget } from 'types/playTarget';
|
||||
|
||||
import PlayTargetIcon from '../../PlayTargetIcon';
|
||||
|
||||
interface RemotePlayMenuProps extends MenuProps {
|
||||
onMenuClose: () => void
|
||||
}
|
||||
|
||||
export const ID = 'app-remote-play-menu';
|
||||
|
||||
const RemotePlayMenu: FC<RemotePlayMenuProps> = ({
|
||||
anchorEl,
|
||||
open,
|
||||
onMenuClose
|
||||
}) => {
|
||||
// TODO: Add other checks for support (Android app, secure context, etc)
|
||||
const isChromecastPluginLoaded = !!pluginManager.plugins.find(plugin => plugin.id === 'chromecast');
|
||||
|
||||
const [ playbackTargets, setPlaybackTargets ] = useState<PlayTarget[]>([]);
|
||||
|
||||
const onPlayTargetClick = (target: PlayTarget) => {
|
||||
playbackManager.trySetActivePlayer(target.playerName, target);
|
||||
onMenuClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlaybackTargets = async () => {
|
||||
setPlaybackTargets(
|
||||
await playbackManager.getTargets()
|
||||
);
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchPlaybackTargets()
|
||||
.catch(err => {
|
||||
console.error('[AppRemotePlayMenu] unable to get playback targets', err);
|
||||
});
|
||||
}
|
||||
}, [ open, setPlaybackTargets ]);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
id={ID}
|
||||
keepMounted
|
||||
open={open}
|
||||
onClose={onMenuClose}
|
||||
>
|
||||
{!isChromecastPluginLoaded && ([
|
||||
<MenuItem key='cast-unsupported-item' disabled>
|
||||
<ListItemIcon>
|
||||
<Warning />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('GoogleCastUnsupported')}
|
||||
</ListItemText>
|
||||
</MenuItem>,
|
||||
<Divider key='cast-unsupported-divider' />
|
||||
])}
|
||||
|
||||
{playbackTargets.map(target => (
|
||||
<MenuItem
|
||||
key={target.id}
|
||||
// Since we are looping over targets there is no good way to avoid creating a new function here
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onPlayTargetClick(target)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<PlayTargetIcon target={target} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={ target.appName ? `${target.name} - ${target.appName}` : target.name }
|
||||
secondary={ target.user?.Name }
|
||||
/>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemotePlayMenu;
|
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;
|
50
src/apps/experimental/components/LibraryIcon.tsx
Normal file
50
src/apps/experimental/components/LibraryIcon.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import Movie from '@mui/icons-material/Movie';
|
||||
import MusicNote from '@mui/icons-material/MusicNote';
|
||||
import Photo from '@mui/icons-material/Photo';
|
||||
import LiveTv from '@mui/icons-material/LiveTv';
|
||||
import Tv from '@mui/icons-material/Tv';
|
||||
import Theaters from '@mui/icons-material/Theaters';
|
||||
import MusicVideo from '@mui/icons-material/MusicVideo';
|
||||
import Book from '@mui/icons-material/Book';
|
||||
import Collections from '@mui/icons-material/Collections';
|
||||
import Queue from '@mui/icons-material/Queue';
|
||||
import Folder from '@mui/icons-material/Folder';
|
||||
import React, { FC } from 'react';
|
||||
import { CollectionType } from 'types/collectionType';
|
||||
|
||||
interface LibraryIconProps {
|
||||
item: BaseItemDto
|
||||
}
|
||||
|
||||
const LibraryIcon: FC<LibraryIconProps> = ({
|
||||
item
|
||||
}) => {
|
||||
switch (item.CollectionType) {
|
||||
case CollectionType.Movies:
|
||||
return <Movie />;
|
||||
case CollectionType.Music:
|
||||
return <MusicNote />;
|
||||
case CollectionType.HomeVideos:
|
||||
case CollectionType.Photos:
|
||||
return <Photo />;
|
||||
case CollectionType.LiveTv:
|
||||
return <LiveTv />;
|
||||
case CollectionType.TvShows:
|
||||
return <Tv />;
|
||||
case CollectionType.Trailers:
|
||||
return <Theaters />;
|
||||
case CollectionType.MusicVideos:
|
||||
return <MusicVideo />;
|
||||
case CollectionType.Books:
|
||||
return <Book />;
|
||||
case CollectionType.BoxSets:
|
||||
return <Collections />;
|
||||
case CollectionType.Playlists:
|
||||
return <Queue />;
|
||||
default:
|
||||
return <Folder />;
|
||||
}
|
||||
};
|
||||
|
||||
export default LibraryIcon;
|
38
src/apps/experimental/components/PlayTargetIcon.tsx
Normal file
38
src/apps/experimental/components/PlayTargetIcon.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import Cast from '@mui/icons-material/Cast';
|
||||
import Computer from '@mui/icons-material/Computer';
|
||||
import Devices from '@mui/icons-material/Devices';
|
||||
import Smartphone from '@mui/icons-material/Smartphone';
|
||||
import Tablet from '@mui/icons-material/Tablet';
|
||||
import Tv from '@mui/icons-material/Tv';
|
||||
|
||||
import browser from 'scripts/browser';
|
||||
import type { PlayTarget } from 'types/playTarget';
|
||||
|
||||
const PlayTargetIcon = ({ target }: { target: PlayTarget }) => {
|
||||
if (!target.deviceType && target.isLocalPlayer) {
|
||||
if (browser.tv) {
|
||||
return <Tv />;
|
||||
} else if (browser.mobile) {
|
||||
return <Smartphone />;
|
||||
}
|
||||
return <Computer />;
|
||||
}
|
||||
|
||||
switch (target.deviceType) {
|
||||
case 'smartphone':
|
||||
return <Smartphone />;
|
||||
case 'tablet':
|
||||
return <Tablet />;
|
||||
case 'desktop':
|
||||
return <Computer />;
|
||||
case 'cast':
|
||||
return <Cast />;
|
||||
case 'tv':
|
||||
return <Tv />;
|
||||
default:
|
||||
return <Devices />;
|
||||
}
|
||||
};
|
||||
|
||||
export default PlayTargetIcon;
|
86
src/apps/experimental/components/drawers/AppDrawer.tsx
Normal file
86
src/apps/experimental/components/drawers/AppDrawer.tsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from '../../routes/asyncRoutes';
|
||||
import { LEGACY_ADMIN_ROUTES, LEGACY_USER_ROUTES } from '../../routes/legacyRoutes';
|
||||
|
||||
import AdvancedDrawerSection from './dashboard/AdvancedDrawerSection';
|
||||
import DevicesDrawerSection from './dashboard/DevicesDrawerSection';
|
||||
import LiveTvDrawerSection from './dashboard/LiveTvDrawerSection';
|
||||
import PluginDrawerSection from './dashboard/PluginDrawerSection';
|
||||
import ServerDrawerSection from './dashboard/ServerDrawerSection';
|
||||
import MainDrawerContent from './MainDrawerContent';
|
||||
import ResponsiveDrawer, { ResponsiveDrawerProps } from './ResponsiveDrawer';
|
||||
|
||||
export const DRAWER_WIDTH = 240;
|
||||
|
||||
const DRAWERLESS_ROUTES = [
|
||||
'edititemmetadata.html', // metadata manager
|
||||
'video' // video player
|
||||
];
|
||||
|
||||
const MAIN_DRAWER_ROUTES = [
|
||||
...ASYNC_USER_ROUTES,
|
||||
...LEGACY_USER_ROUTES
|
||||
].filter(route => !DRAWERLESS_ROUTES.includes(route.path));
|
||||
|
||||
const ADMIN_DRAWER_ROUTES = [
|
||||
...ASYNC_ADMIN_ROUTES,
|
||||
...LEGACY_ADMIN_ROUTES,
|
||||
{ path: '/configurationpage' } // Plugin configuration page
|
||||
].filter(route => !DRAWERLESS_ROUTES.includes(route.path));
|
||||
|
||||
/** Utility function to check if a path has a drawer. */
|
||||
export const isDrawerPath = (path: string) => (
|
||||
MAIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path)
|
||||
|| ADMIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path)
|
||||
);
|
||||
|
||||
const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
||||
open = false,
|
||||
onClose,
|
||||
onOpen
|
||||
}) => (
|
||||
<Routes>
|
||||
{
|
||||
MAIN_DRAWER_ROUTES.map(route => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={
|
||||
<ResponsiveDrawer
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
<MainDrawerContent />
|
||||
</ResponsiveDrawer>
|
||||
}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{
|
||||
ADMIN_DRAWER_ROUTES.map(route => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={
|
||||
<ResponsiveDrawer
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
<ServerDrawerSection />
|
||||
<DevicesDrawerSection />
|
||||
<LiveTvDrawerSection />
|
||||
<AdvancedDrawerSection />
|
||||
<PluginDrawerSection />
|
||||
</ResponsiveDrawer>
|
||||
}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Routes>
|
||||
);
|
||||
|
||||
export default AppDrawer;
|
45
src/apps/experimental/components/drawers/ListItemLink.tsx
Normal file
45
src/apps/experimental/components/drawers/ListItemLink.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import ListItemButton, { ListItemButtonBaseProps } from '@mui/material/ListItemButton';
|
||||
import React, { FC } from 'react';
|
||||
import { Link, useLocation, useSearchParams } from 'react-router-dom';
|
||||
|
||||
interface ListItemLinkProps extends ListItemButtonBaseProps {
|
||||
to: string
|
||||
}
|
||||
|
||||
const isMatchingParams = (routeParams: URLSearchParams, currentParams: URLSearchParams) => {
|
||||
for (const param of routeParams) {
|
||||
if (currentParams.get(param[0]) !== param[1]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const ListItemLink: FC<ListItemLinkProps> = ({
|
||||
children,
|
||||
to,
|
||||
...params
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const [ searchParams ] = useSearchParams();
|
||||
|
||||
const [ toPath, toParams ] = to.split('?');
|
||||
// eslint-disable-next-line compat/compat
|
||||
const toSearchParams = new URLSearchParams(`?${toParams}`);
|
||||
|
||||
const selected = location.pathname === toPath && (!toParams || isMatchingParams(toSearchParams, searchParams));
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
component={Link}
|
||||
to={to}
|
||||
selected={selected}
|
||||
{...params}
|
||||
>
|
||||
{children}
|
||||
</ListItemButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListItemLink;
|
186
src/apps/experimental/components/drawers/MainDrawerContent.tsx
Normal file
186
src/apps/experimental/components/drawers/MainDrawerContent.tsx
Normal file
|
@ -0,0 +1,186 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
import type { SystemInfo } from '@jellyfin/sdk/lib/generated-client/models/system-info';
|
||||
import { getUserViewsApi } from '@jellyfin/sdk/lib/utils/api/user-views-api';
|
||||
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
|
||||
import Dashboard from '@mui/icons-material/Dashboard';
|
||||
import Edit from '@mui/icons-material/Edit';
|
||||
import Favorite from '@mui/icons-material/Favorite';
|
||||
import Home from '@mui/icons-material/Home';
|
||||
import Link from '@mui/icons-material/Link';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { useWebConfig } from 'hooks/useWebConfig';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
|
||||
import ListItemLink from './ListItemLink';
|
||||
import LibraryIcon from '../LibraryIcon';
|
||||
|
||||
const MainDrawerContent = () => {
|
||||
const { api, user } = useApi();
|
||||
const location = useLocation();
|
||||
const [ systemInfo, setSystemInfo ] = useState<SystemInfo>();
|
||||
const [ userViews, setUserViews ] = useState<BaseItemDto[]>([]);
|
||||
const webConfig = useWebConfig();
|
||||
|
||||
const isHomeSelected = location.pathname === '/home.html' && (!location.search || location.search === '?tab=0');
|
||||
|
||||
useEffect(() => {
|
||||
if (api && user?.Id) {
|
||||
getUserViewsApi(api)
|
||||
.getUserViews({ userId: user.Id })
|
||||
.then(({ data }) => {
|
||||
setUserViews(data.Items || []);
|
||||
})
|
||||
.catch(err => {
|
||||
console.warn('[MainDrawer] failed to fetch user views', err);
|
||||
setUserViews([]);
|
||||
});
|
||||
|
||||
getSystemApi(api)
|
||||
.getSystemInfo()
|
||||
.then(({ data }) => {
|
||||
setSystemInfo(data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.warn('[MainDrawer] failed to fetch system info', err);
|
||||
});
|
||||
} else {
|
||||
setUserViews([]);
|
||||
}
|
||||
}, [ api, user?.Id ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* MAIN LINKS */}
|
||||
<List>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/home.html' selected={isHomeSelected}>
|
||||
<ListItemIcon>
|
||||
<Home />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('Home')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/home.html?tab=1'>
|
||||
<ListItemIcon>
|
||||
<Favorite />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('Favorites')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
{/* CUSTOM LINKS */}
|
||||
{(!!webConfig.menuLinks && webConfig.menuLinks.length > 0) && (
|
||||
<>
|
||||
<Divider />
|
||||
<List>
|
||||
{webConfig.menuLinks.map(menuLink => (
|
||||
<ListItem
|
||||
key={`${menuLink.name}_${menuLink.url}`}
|
||||
disablePadding
|
||||
>
|
||||
<ListItemButton
|
||||
component='a'
|
||||
href={menuLink.url}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<ListItemIcon>
|
||||
{/* TODO: Support custom icons */}
|
||||
<Link />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={menuLink.name} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* LIBRARY LINKS */}
|
||||
{userViews.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<List
|
||||
aria-labelledby='libraries-subheader'
|
||||
subheader={
|
||||
<ListSubheader component='div' id='libraries-subheader'>
|
||||
{globalize.translate('HeaderLibraries')}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
{userViews.map(view => (
|
||||
<ListItem key={view.Id} disablePadding>
|
||||
<ListItemLink
|
||||
to={appRouter.getRouteUrl(view, { context: view.CollectionType }).substring(1)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<LibraryIcon item={view} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={view.Name} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ADMIN LINKS */}
|
||||
{user?.Policy?.IsAdministrator && (
|
||||
<>
|
||||
<Divider />
|
||||
<List
|
||||
aria-labelledby='admin-subheader'
|
||||
subheader={
|
||||
<ListSubheader component='div' id='admin-subheader'>
|
||||
{globalize.translate('HeaderAdmin')}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard.html'>
|
||||
<ListItemIcon>
|
||||
<Dashboard />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabDashboard')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/edititemmetadata.html'>
|
||||
<ListItemIcon>
|
||||
<Edit />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('MetadataManager')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
</List>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* FOOTER */}
|
||||
<Divider style={{ marginTop: 'auto' }} />
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={systemInfo?.ServerName ? systemInfo.ServerName : 'Jellyfin'}
|
||||
secondary={systemInfo?.Version ? `v${systemInfo.Version}` : ''}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainDrawerContent;
|
|
@ -0,0 +1,86 @@
|
|||
import { Theme } from '@mui/material/styles';
|
||||
import Box from '@mui/material/Box';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import browser from 'scripts/browser';
|
||||
|
||||
import { DRAWER_WIDTH } from './AppDrawer';
|
||||
import { isTabPath } from '../tabs/tabRoutes';
|
||||
|
||||
export interface ResponsiveDrawerProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onOpen: () => void
|
||||
}
|
||||
|
||||
const ResponsiveDrawer: FC<ResponsiveDrawerProps> = ({
|
||||
children,
|
||||
open = false,
|
||||
onClose,
|
||||
onOpen
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const isSmallScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
|
||||
const isLargeScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('lg'));
|
||||
const isTallToolbar = isTabPath(location.pathname) && !isLargeScreen;
|
||||
|
||||
const getToolbarStyles = useCallback((theme: Theme) => ({
|
||||
marginBottom: isTallToolbar ? theme.spacing(6) : 0
|
||||
}), [ isTallToolbar ]);
|
||||
|
||||
return ( isSmallScreen ? (
|
||||
/* DESKTOP DRAWER */
|
||||
<Drawer
|
||||
sx={{
|
||||
width: DRAWER_WIDTH,
|
||||
flexShrink: 0,
|
||||
'& .MuiDrawer-paper': {
|
||||
width: DRAWER_WIDTH,
|
||||
boxSizing: 'border-box'
|
||||
}
|
||||
}}
|
||||
variant='persistent'
|
||||
anchor='left'
|
||||
open={open}
|
||||
>
|
||||
<Toolbar
|
||||
variant='dense'
|
||||
sx={getToolbarStyles}
|
||||
/>
|
||||
{children}
|
||||
</Drawer>
|
||||
) : (
|
||||
/* MOBILE DRAWER */
|
||||
<SwipeableDrawer
|
||||
anchor='left'
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onOpen={onOpen}
|
||||
// Disable swipe to open on iOS since it interferes with back navigation
|
||||
disableDiscovery={browser.iOS}
|
||||
ModalProps={{
|
||||
keepMounted: true // Better open performance on mobile.
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
variant='dense'
|
||||
sx={getToolbarStyles}
|
||||
/>
|
||||
<Box
|
||||
role='presentation'
|
||||
// Close the drawer when the content is clicked
|
||||
onClick={onClose}
|
||||
onKeyDown={onClose}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</SwipeableDrawer>
|
||||
));
|
||||
};
|
||||
|
||||
export default ResponsiveDrawer;
|
|
@ -0,0 +1,110 @@
|
|||
import Article from '@mui/icons-material/Article';
|
||||
import EditNotifications from '@mui/icons-material/EditNotifications';
|
||||
import ExpandLess from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
import Extension from '@mui/icons-material/Extension';
|
||||
import Lan from '@mui/icons-material/Lan';
|
||||
import Schedule from '@mui/icons-material/Schedule';
|
||||
import VpnKey from '@mui/icons-material/VpnKey';
|
||||
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',
|
||||
'/addplugin.html',
|
||||
'/configurationpage'
|
||||
];
|
||||
|
||||
const AdvancedDrawerSection = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const isPluginSectionOpen = PLUGIN_PATHS.includes(location.pathname);
|
||||
|
||||
return (
|
||||
<List
|
||||
aria-labelledby='advanced-subheader'
|
||||
subheader={
|
||||
<ListSubheader component='div' id='advanced-subheader'>
|
||||
{globalize.translate('TabAdvanced')}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/networking.html'>
|
||||
<ListItemIcon>
|
||||
<Lan />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabNetworking')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/apikeys.html'>
|
||||
<ListItemIcon>
|
||||
<VpnKey />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderApiKeys')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/log.html'>
|
||||
<ListItemIcon>
|
||||
<Article />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabLogs')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/notificationsettings.html'>
|
||||
<ListItemIcon>
|
||||
<EditNotifications />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('Notifications')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/installedplugins.html' selected={false}>
|
||||
<ListItemIcon>
|
||||
<Extension />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabPlugins')} />
|
||||
{isPluginSectionOpen ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<Collapse in={isPluginSectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/installedplugins.html' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabMyPlugins')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/availableplugins.html' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabCatalog')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/repositories.html' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabRepositories')} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
</Collapse>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/scheduledtasks.html'>
|
||||
<ListItemIcon>
|
||||
<Schedule />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabScheduledTasks')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedDrawerSection;
|
|
@ -0,0 +1,73 @@
|
|||
import { Devices, Analytics, Input, ExpandLess, ExpandMore } from '@mui/icons-material';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import ListItemLink from '../ListItemLink';
|
||||
|
||||
const DLNA_PATHS = [
|
||||
'/dlnasettings.html',
|
||||
'/dlnaprofiles.html'
|
||||
];
|
||||
|
||||
const DevicesDrawerSection = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const isDlnaSectionOpen = DLNA_PATHS.includes(location.pathname);
|
||||
|
||||
return (
|
||||
<List
|
||||
aria-labelledby='devices-subheader'
|
||||
subheader={
|
||||
<ListSubheader component='div' id='devices-subheader'>
|
||||
{globalize.translate('HeaderDevices')}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/devices.html'>
|
||||
<ListItemIcon>
|
||||
<Devices />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderDevices')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/serveractivity.html'>
|
||||
<ListItemIcon>
|
||||
<Analytics />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderActivity')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dlnasettings.html' selected={false}>
|
||||
<ListItemIcon>
|
||||
<Input />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={'DLNA'} />
|
||||
{isDlnaSectionOpen ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<Collapse in={isDlnaSectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/dlnasettings.html' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Settings')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dlnaprofiles.html' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabProfiles')} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
</Collapse>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default DevicesDrawerSection;
|
|
@ -0,0 +1,43 @@
|
|||
import { Dvr, LiveTv } from '@mui/icons-material';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React from 'react';
|
||||
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import ListItemLink from '../ListItemLink';
|
||||
|
||||
const LiveTvDrawerSection = () => {
|
||||
return (
|
||||
<List
|
||||
aria-labelledby='livetv-subheader'
|
||||
subheader={
|
||||
<ListSubheader component='div' id='livetv-subheader'>
|
||||
{globalize.translate('LiveTV')}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/livetvstatus.html'>
|
||||
<ListItemIcon>
|
||||
<LiveTv />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('LiveTV')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/livetvsettings.html'>
|
||||
<ListItemIcon>
|
||||
<Dvr />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderDVR')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default LiveTvDrawerSection;
|
|
@ -0,0 +1,67 @@
|
|||
import { ConfigurationPageInfo } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { getDashboardApi } from '@jellyfin/sdk/lib/utils/api/dashboard-api';
|
||||
import { Folder } from '@mui/icons-material';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import globalize from 'scripts/globalize';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
|
||||
import ListItemLink from '../ListItemLink';
|
||||
|
||||
const PluginDrawerSection = () => {
|
||||
const { api } = useApi();
|
||||
const [ pagesInfo, setPagesInfo ] = useState<ConfigurationPageInfo[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPluginPages = async () => {
|
||||
if (!api) return;
|
||||
|
||||
const pagesResponse = await getDashboardApi(api)
|
||||
.getConfigurationPages({ enableInMainMenu: true });
|
||||
|
||||
setPagesInfo(pagesResponse.data);
|
||||
};
|
||||
|
||||
fetchPluginPages()
|
||||
.catch(err => {
|
||||
console.error('[PluginDrawerSection] unable to fetch plugin config pages', err);
|
||||
});
|
||||
}, [ api ]);
|
||||
|
||||
if (!api || pagesInfo.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<List
|
||||
aria-labelledby='plugins-subheader'
|
||||
subheader={
|
||||
<ListSubheader component='div' id='plugins-subheader'>
|
||||
{globalize.translate('TabPlugins')}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
{
|
||||
pagesInfo.map(pageInfo => (
|
||||
<ListItem key={pageInfo.PluginId} disablePadding>
|
||||
<ListItemLink to={`/${Dashboard.getPluginUrl(pageInfo.Name)}`}>
|
||||
<ListItemIcon>
|
||||
{/* TODO: Support different icons? */}
|
||||
<Folder />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={pageInfo.DisplayName} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
))
|
||||
}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginDrawerSection;
|
|
@ -0,0 +1,118 @@
|
|||
import { Dashboard, ExpandLess, ExpandMore, LibraryAdd, People, PlayCircle, Settings } from '@mui/icons-material';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import ListItemLink from '../ListItemLink';
|
||||
|
||||
const LIBRARY_PATHS = [
|
||||
'/library.html',
|
||||
'/librarydisplay.html',
|
||||
'/metadataimages.html',
|
||||
'/metadatanfo.html'
|
||||
];
|
||||
|
||||
const PLAYBACK_PATHS = [
|
||||
'/encodingsettings.html',
|
||||
'/playbackconfiguration.html',
|
||||
'/streamingsettings.html'
|
||||
];
|
||||
|
||||
const ServerDrawerSection = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const isLibrarySectionOpen = LIBRARY_PATHS.includes(location.pathname);
|
||||
const isPlaybackSectionOpen = PLAYBACK_PATHS.includes(location.pathname);
|
||||
|
||||
return (
|
||||
<List
|
||||
aria-labelledby='server-subheader'
|
||||
subheader={
|
||||
<ListSubheader component='div' id='server-subheader'>
|
||||
{globalize.translate('TabServer')}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard.html'>
|
||||
<ListItemIcon>
|
||||
<Dashboard />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabDashboard')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboardgeneral.html'>
|
||||
<ListItemIcon>
|
||||
<Settings />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('General')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/userprofiles.html'>
|
||||
<ListItemIcon>
|
||||
<People />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderUsers')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/library.html' selected={false}>
|
||||
<ListItemIcon>
|
||||
<LibraryAdd />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderLibraries')} />
|
||||
{isLibrarySectionOpen ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<Collapse in={isLibrarySectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/library.html' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('HeaderLibraries')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/librarydisplay.html' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Display')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/metadataimages.html' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Metadata')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/metadatanfo.html' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabNfoSettings')} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
</Collapse>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/encodingsettings.html' selected={false}>
|
||||
<ListItemIcon>
|
||||
<PlayCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TitlePlayback')} />
|
||||
{isPlaybackSectionOpen ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<Collapse in={isPlaybackSectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/encodingsettings.html' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Transcoding')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/playbackconfiguration.html' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('ButtonResume')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/streamingsettings.html' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabStreaming')} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
</Collapse>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerDrawerSection;
|
90
src/apps/experimental/components/tabs/AppTabs.tsx
Normal file
90
src/apps/experimental/components/tabs/AppTabs.tsx
Normal file
|
@ -0,0 +1,90 @@
|
|||
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, useLocation, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import TabRoutes, { getDefaultTabIndex } from './tabRoutes';
|
||||
|
||||
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 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);
|
||||
|
||||
// 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}-tab-${index}`}
|
||||
label={label}
|
||||
data-tab-index={`${index}`}
|
||||
onClick={onTabClick}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Tabs>
|
||||
}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</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;
|
11
src/apps/experimental/routes/asyncRoutes/admin.ts
Normal file
11
src/apps/experimental/routes/asyncRoutes/admin.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'notificationsettings.html', page: 'dashboard/notifications' },
|
||||
{ path: 'usernew.html', page: 'user/usernew' },
|
||||
{ path: 'userprofiles.html', page: 'user/userprofiles' },
|
||||
{ path: 'useredit.html', page: 'user/useredit' },
|
||||
{ path: 'userlibraryaccess.html', page: 'user/userlibraryaccess' },
|
||||
{ path: 'userparentalcontrol.html', page: 'user/userparentalcontrol' },
|
||||
{ path: 'userpassword.html', page: 'user/userpassword' }
|
||||
];
|
2
src/apps/experimental/routes/asyncRoutes/index.ts
Normal file
2
src/apps/experimental/routes/asyncRoutes/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './admin';
|
||||
export * from './user';
|
8
src/apps/experimental/routes/asyncRoutes/user.ts
Normal file
8
src/apps/experimental/routes/asyncRoutes/user.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { AsyncRoute, AsyncRouteType } from '../../../../components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_USER_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'search.html', page: 'search' },
|
||||
{ path: 'userprofile.html', page: 'user/userprofile' },
|
||||
{ path: 'home.html', page: 'home', type: AsyncRouteType.Experimental },
|
||||
{ path: 'movies.html', page: 'movies', type: AsyncRouteType.Experimental }
|
||||
];
|
|
@ -1,22 +1,20 @@
|
|||
import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import globalize from '../scripts/globalize';
|
||||
import LibraryMenu from '../scripts/libraryMenu';
|
||||
import { clearBackdrop } from '../components/backdrop/backdrop';
|
||||
import layoutManager from '../components/layoutManager';
|
||||
import * as mainTabsManager from '../components/maintabsmanager';
|
||||
import '../elements/emby-tabs/emby-tabs';
|
||||
import '../elements/emby-button/emby-button';
|
||||
import '../elements/emby-scroller/emby-scroller';
|
||||
import Page from '../components/Page';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
type IProps = {
|
||||
tab?: string;
|
||||
}
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import LibraryMenu from '../../../scripts/libraryMenu';
|
||||
import { clearBackdrop } from '../../../components/backdrop/backdrop';
|
||||
import layoutManager from '../../../components/layoutManager';
|
||||
import * as mainTabsManager from '../../../components/maintabsmanager';
|
||||
import '../../../elements/emby-tabs/emby-tabs';
|
||||
import '../../../elements/emby-button/emby-button';
|
||||
import '../../../elements/emby-scroller/emby-scroller';
|
||||
import Page from '../../../components/Page';
|
||||
|
||||
type OnResumeOptions = {
|
||||
autoFocus?: boolean;
|
||||
refresh?: boolean
|
||||
}
|
||||
};
|
||||
|
||||
type ControllerProps = {
|
||||
onResume: (
|
||||
|
@ -25,17 +23,14 @@ type ControllerProps = {
|
|||
refreshed: boolean;
|
||||
onPause: () => void;
|
||||
destroy: () => void;
|
||||
}
|
||||
};
|
||||
|
||||
const Home: FunctionComponent<IProps> = (props: IProps) => {
|
||||
const getDefaultTabIndex = () => {
|
||||
return 0;
|
||||
};
|
||||
const Home: FunctionComponent = () => {
|
||||
const [ searchParams ] = useSearchParams();
|
||||
const initialTabIndex = parseInt(searchParams.get('tab') || '0', 10);
|
||||
|
||||
const tabController = useRef<ControllerProps | null>();
|
||||
const currentTabIndex = useRef(parseInt(props.tab || getDefaultTabIndex().toString()));
|
||||
const tabControllers = useMemo<ControllerProps[]>(() => [], []);
|
||||
const initialTabIndex = useRef<number | null>(currentTabIndex.current);
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const setTitle = () => {
|
||||
|
@ -70,18 +65,18 @@ const Home: FunctionComponent<IProps> = (props: IProps) => {
|
|||
depends = 'favorites';
|
||||
}
|
||||
|
||||
return import(/* webpackChunkName: "[request]" */ `../controllers/${depends}`).then(({ default: controllerFactory }) => {
|
||||
return import(/* webpackChunkName: "[request]" */ `../../../controllers/${depends}`).then(({ default: controllerFactory }) => {
|
||||
let controller = tabControllers[index];
|
||||
|
||||
if (!controller) {
|
||||
const tabContent = element.current?.querySelector(".tabContent[data-index='" + index + "']");
|
||||
controller = new controllerFactory(tabContent, props);
|
||||
controller = new controllerFactory(tabContent, null);
|
||||
tabControllers[index] = controller;
|
||||
}
|
||||
|
||||
return controller;
|
||||
});
|
||||
}, [props, tabControllers]);
|
||||
}, [ tabControllers ]);
|
||||
|
||||
const onViewDestroy = useCallback(() => {
|
||||
if (tabControllers) {
|
||||
|
@ -93,8 +88,7 @@ const Home: FunctionComponent<IProps> = (props: IProps) => {
|
|||
}
|
||||
|
||||
tabController.current = null;
|
||||
initialTabIndex.current = null;
|
||||
}, [tabControllers]);
|
||||
}, [ tabControllers ]);
|
||||
|
||||
const loadTab = useCallback((index: number, previousIndex: number | null) => {
|
||||
getTabController(index).then((controller) => {
|
||||
|
@ -106,22 +100,23 @@ const Home: FunctionComponent<IProps> = (props: IProps) => {
|
|||
});
|
||||
|
||||
controller.refreshed = true;
|
||||
currentTabIndex.current = index;
|
||||
tabController.current = controller;
|
||||
}).catch(err => {
|
||||
console.error('[Home] failed to get tab controller', err);
|
||||
});
|
||||
}, [getTabController]);
|
||||
}, [ getTabController ]);
|
||||
|
||||
const onTabChange = useCallback((e: { detail: { selectedTabIndex: string; previousIndex: number | null }; }) => {
|
||||
const newIndex = parseInt(e.detail.selectedTabIndex);
|
||||
const newIndex = parseInt(e.detail.selectedTabIndex, 10);
|
||||
const previousIndex = e.detail.previousIndex;
|
||||
|
||||
const previousTabController = previousIndex == null ? null : tabControllers[previousIndex];
|
||||
if (previousTabController && previousTabController.onPause) {
|
||||
if (previousTabController?.onPause) {
|
||||
previousTabController.onPause();
|
||||
}
|
||||
|
||||
loadTab(newIndex, previousIndex);
|
||||
}, [loadTab, tabControllers]);
|
||||
}, [ loadTab, tabControllers ]);
|
||||
|
||||
const onResume = useCallback(() => {
|
||||
setTitle();
|
||||
|
@ -130,30 +125,29 @@ const Home: FunctionComponent<IProps> = (props: IProps) => {
|
|||
const currentTabController = tabController.current;
|
||||
|
||||
if (!currentTabController) {
|
||||
mainTabsManager.selectedTabIndex(initialTabIndex.current);
|
||||
} else if (currentTabController && currentTabController.onResume) {
|
||||
mainTabsManager.selectedTabIndex(initialTabIndex);
|
||||
} else if (currentTabController?.onResume) {
|
||||
currentTabController.onResume({});
|
||||
}
|
||||
(document.querySelector('.skinHeader') as HTMLDivElement).classList.add('noHomeButtonHeader');
|
||||
}, []);
|
||||
}, [ initialTabIndex ]);
|
||||
|
||||
const onPause = useCallback(() => {
|
||||
const currentTabController = tabController.current;
|
||||
if (currentTabController && currentTabController.onPause) {
|
||||
if (currentTabController?.onPause) {
|
||||
currentTabController.onPause();
|
||||
}
|
||||
(document.querySelector('.skinHeader') as HTMLDivElement).classList.remove('noHomeButtonHeader');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
mainTabsManager.setTabs(element.current, currentTabIndex.current, getTabs, getTabContainers, null, onTabChange, false);
|
||||
mainTabsManager.setTabs(element.current, initialTabIndex, getTabs, getTabContainers, null, onTabChange, false);
|
||||
|
||||
onResume();
|
||||
return () => {
|
||||
onPause();
|
||||
onViewDestroy();
|
||||
};
|
||||
}, [onPause, onResume, onTabChange, onViewDestroy]);
|
||||
}, [ initialTabIndex, onPause, onResume, onTabChange, onViewDestroy ]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
|
@ -1,4 +1,4 @@
|
|||
import { LegacyRoute } from '.';
|
||||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||
{
|
||||
|
@ -103,18 +103,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
|||
controller: 'dashboard/metadatanfo',
|
||||
view: 'dashboard/metadatanfo.html'
|
||||
}
|
||||
}, {
|
||||
path: 'notificationsetting.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/notifications/notification/index',
|
||||
view: 'dashboard/notifications/notification/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'notificationsettings.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/notifications/notifications/index',
|
||||
view: 'dashboard/notifications/notifications/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'playbackconfiguration.html',
|
||||
pageProps: {
|
3
src/apps/experimental/routes/legacyRoutes/index.ts
Normal file
3
src/apps/experimental/routes/legacyRoutes/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './admin';
|
||||
export * from './public';
|
||||
export * from './user';
|
81
src/apps/experimental/routes/legacyRoutes/public.ts
Normal file
81
src/apps/experimental/routes/legacyRoutes/public.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_PUBLIC_ROUTES: LegacyRoute[] = [
|
||||
{
|
||||
path: 'addserver.html',
|
||||
pageProps: {
|
||||
controller: 'session/addServer/index',
|
||||
view: 'session/addServer/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'selectserver.html',
|
||||
pageProps: {
|
||||
controller: 'session/selectServer/index',
|
||||
view: 'session/selectServer/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'login.html',
|
||||
pageProps: {
|
||||
controller: 'session/login/index',
|
||||
view: 'session/login/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'forgotpassword.html',
|
||||
pageProps: {
|
||||
controller: 'session/forgotPassword/index',
|
||||
view: 'session/forgotPassword/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'forgotpasswordpin.html',
|
||||
pageProps: {
|
||||
controller: 'session/resetPassword/index',
|
||||
view: 'session/resetPassword/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardremoteaccess.html',
|
||||
pageProps: {
|
||||
controller: 'wizard/remote/index',
|
||||
view: 'wizard/remote/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardfinish.html',
|
||||
pageProps: {
|
||||
controller: 'wizard/finish/index',
|
||||
view: 'wizard/finish/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardlibrary.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/library',
|
||||
view: 'wizard/library.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardsettings.html',
|
||||
pageProps: {
|
||||
controller: 'wizard/settings/index',
|
||||
view: 'wizard/settings/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardstart.html',
|
||||
pageProps: {
|
||||
controller: 'wizard/start/index',
|
||||
view: 'wizard/start/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizarduser.html',
|
||||
pageProps: {
|
||||
controller: 'wizard/user/index',
|
||||
view: 'wizard/user/index.html'
|
||||
}
|
||||
}
|
||||
];
|
|
@ -1,4 +1,4 @@
|
|||
import { LegacyRoute } from '.';
|
||||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_USER_ROUTES: LegacyRoute[] = [
|
||||
{
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import ViewItemsContainer from '../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../types/interface';
|
||||
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../../../types/interface';
|
||||
|
||||
const CollectionsView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const getBasekey = useCallback(() => {
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import ViewItemsContainer from '../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../types/interface';
|
||||
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../../../types/interface';
|
||||
|
||||
const FavoritesView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const getBasekey = useCallback(() => {
|
|
@ -1,9 +1,9 @@
|
|||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import loading from '../../components/loading/loading';
|
||||
import GenresItemsContainer from '../../components/common/GenresItemsContainer';
|
||||
import { LibraryViewProps } from '../../types/interface';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import GenresItemsContainer from '../../../../components/common/GenresItemsContainer';
|
||||
import { LibraryViewProps } from '../../../../types/interface';
|
||||
|
||||
const GenresView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const [ itemsResult, setItemsResult ] = useState<BaseItemDtoQueryResult>({});
|
||||
|
@ -23,6 +23,8 @@ const GenresView: FC<LibraryViewProps> = ({ topParentId }) => {
|
|||
).then((result) => {
|
||||
setItemsResult(result);
|
||||
loading.hide();
|
||||
}).catch(err => {
|
||||
console.error('[GenresView] failed to fetch genres', err);
|
||||
});
|
||||
}, [topParentId]);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import ViewItemsContainer from '../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../types/interface';
|
||||
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../../../types/interface';
|
||||
|
||||
const MoviesView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const getBasekey = useCallback(() => {
|
|
@ -1,15 +1,15 @@
|
|||
import type { BaseItemDto, BaseItemDtoQueryResult, RecommendationDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import layoutManager from '../../components/layoutManager';
|
||||
import loading from '../../components/loading/loading';
|
||||
import dom from '../../scripts/dom';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import RecommendationContainer from '../../components/common/RecommendationContainer';
|
||||
import SectionContainer from '../../components/common/SectionContainer';
|
||||
import { LibraryViewProps } from '../../types/interface';
|
||||
import layoutManager from '../../../../components/layoutManager';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import dom from '../../../../scripts/dom';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import RecommendationContainer from '../../../../components/common/RecommendationContainer';
|
||||
import SectionContainer from '../../../../components/common/SectionContainer';
|
||||
import { LibraryViewProps } from '../../../../types/interface';
|
||||
|
||||
const SuggestionsView: FC<LibraryViewProps> = ({topParentId}) => {
|
||||
const SuggestionsView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const [ latestItems, setLatestItems ] = useState<BaseItemDto[]>([]);
|
||||
const [ resumeResult, setResumeResult ] = useState<BaseItemDtoQueryResult>({});
|
||||
const [ recommendations, setRecommendations ] = useState<RecommendationDto[]>([]);
|
||||
|
@ -28,8 +28,10 @@ const SuggestionsView: FC<LibraryViewProps> = ({topParentId}) => {
|
|||
}, [enableScrollX]);
|
||||
|
||||
const autoFocus = useCallback((page) => {
|
||||
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
|
||||
import('../../../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
}).catch(err => {
|
||||
console.error('[SuggestionsView] failed to load data', err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
@ -55,6 +57,8 @@ const SuggestionsView: FC<LibraryViewProps> = ({topParentId}) => {
|
|||
|
||||
loading.hide();
|
||||
autoFocus(page);
|
||||
}).catch(err => {
|
||||
console.error('[SuggestionsView] failed to fetch items', err);
|
||||
});
|
||||
}, [autoFocus]);
|
||||
|
||||
|
@ -72,6 +76,8 @@ const SuggestionsView: FC<LibraryViewProps> = ({topParentId}) => {
|
|||
setLatestItems(items);
|
||||
|
||||
autoFocus(page);
|
||||
}).catch(err => {
|
||||
console.error('[SuggestionsView] failed to fetch latest items', err);
|
||||
});
|
||||
}, [autoFocus]);
|
||||
|
||||
|
@ -95,6 +101,8 @@ const SuggestionsView: FC<LibraryViewProps> = ({topParentId}) => {
|
|||
setRecommendations(result);
|
||||
|
||||
autoFocus(page);
|
||||
}).catch(err => {
|
||||
console.error('[SuggestionsView] failed to fetch recommendations', err);
|
||||
});
|
||||
}, [autoFocus]);
|
||||
|
||||
|
@ -143,8 +151,8 @@ const SuggestionsView: FC<LibraryViewProps> = ({topParentId}) => {
|
|||
{!recommendations.length ? <div className='noItemsMessage centerMessage'>
|
||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||
<p>{globalize.translate('MessageNoMovieSuggestionsAvailable')}</p>
|
||||
</div> : recommendations.map((recommendation, index) => {
|
||||
return <RecommendationContainer key={index} getPortraitShape={getPortraitShape} enableScrollX={enableScrollX} recommendation={recommendation} />;
|
||||
</div> : recommendations.map(recommendation => {
|
||||
return <RecommendationContainer key={recommendation.CategoryId} getPortraitShape={getPortraitShape} enableScrollX={enableScrollX} recommendation={recommendation} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
|
@ -1,8 +1,8 @@
|
|||
|
||||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import ViewItemsContainer from '../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../types/interface';
|
||||
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../../../types/interface';
|
||||
|
||||
const TrailersView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const getBasekey = useCallback(() => {
|
|
@ -1,62 +1,28 @@
|
|||
import '../../elements/emby-scroller/emby-scroller';
|
||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import '../../elements/emby-tabs/emby-tabs';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../../../elements/emby-scroller/emby-scroller';
|
||||
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 Page from '../../../../components/Page';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import libraryMenu from '../../../../scripts/libraryMenu';
|
||||
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());
|
||||
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);
|
||||
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');
|
||||
|
||||
|
@ -114,13 +75,17 @@ const Movies: FC = () => {
|
|||
window.ApiClient.getItem(window.ApiClient.getCurrentUserId(), parentId).then((item) => {
|
||||
page.setAttribute('data-title', item.Name as string);
|
||||
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}>
|
||||
|
@ -129,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;
|
58
src/apps/stable/App.tsx
Normal file
58
src/apps/stable/App.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import AppHeader from 'components/AppHeader';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
import ServerContentPage from 'components/ServerContentPage';
|
||||
import ConnectionRequired from 'components/ConnectionRequired';
|
||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||
|
||||
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
||||
|
||||
const Layout = () => (
|
||||
<>
|
||||
<Backdrop />
|
||||
<AppHeader />
|
||||
|
||||
<div className='mainAnimatedPages skinBody' />
|
||||
<div className='skinBody'>
|
||||
<Outlet />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const StableApp = () => (
|
||||
<Routes>
|
||||
<Route element={<Layout />}>
|
||||
{/* User routes */}
|
||||
<Route path='/' element={<ConnectionRequired />}>
|
||||
{ASYNC_USER_ROUTES.map(toAsyncPageRoute)}
|
||||
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
|
||||
</Route>
|
||||
|
||||
{/* Admin routes */}
|
||||
<Route path='/' element={<ConnectionRequired isAdminRequired />}>
|
||||
{ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)}
|
||||
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
|
||||
|
||||
<Route path='configurationpage' element={
|
||||
<ServerContentPage view='/web/configurationpage' />
|
||||
} />
|
||||
</Route>
|
||||
|
||||
{/* Public routes */}
|
||||
<Route path='/' element={<ConnectionRequired isUserRequired={false} />}>
|
||||
<Route index element={<Navigate replace to='/home.html' />} />
|
||||
|
||||
{LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)}
|
||||
</Route>
|
||||
|
||||
{/* Suppress warnings for unhandled routes */}
|
||||
<Route path='*' element={null} />
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
|
||||
export default StableApp;
|
11
src/apps/stable/routes/asyncRoutes/admin.ts
Normal file
11
src/apps/stable/routes/asyncRoutes/admin.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'notificationsettings.html', page: 'dashboard/notifications' },
|
||||
{ path: 'usernew.html', page: 'user/usernew' },
|
||||
{ path: 'userprofiles.html', page: 'user/userprofiles' },
|
||||
{ path: 'useredit.html', page: 'user/useredit' },
|
||||
{ path: 'userlibraryaccess.html', page: 'user/userlibraryaccess' },
|
||||
{ path: 'userparentalcontrol.html', page: 'user/userparentalcontrol' },
|
||||
{ path: 'userpassword.html', page: 'user/userpassword' }
|
||||
];
|
2
src/apps/stable/routes/asyncRoutes/index.ts
Normal file
2
src/apps/stable/routes/asyncRoutes/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './admin';
|
||||
export * from './user';
|
6
src/apps/stable/routes/asyncRoutes/user.ts
Normal file
6
src/apps/stable/routes/asyncRoutes/user.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_USER_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'search.html', page: 'search' },
|
||||
{ path: 'userprofile.html', page: 'user/userprofile' }
|
||||
];
|
36
src/apps/stable/routes/dashboard/notifications.tsx
Normal file
36
src/apps/stable/routes/dashboard/notifications.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
|
||||
import Page from 'components/Page';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
const PluginLink = () => (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `<a
|
||||
is='emby-linkbutton'
|
||||
class='button-link'
|
||||
href='#/addplugin.html?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173'
|
||||
>
|
||||
${globalize.translate('GetThePlugin')}
|
||||
</a>`
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const Notifications = () => (
|
||||
<Page
|
||||
id='notificationSettingPage'
|
||||
title={globalize.translate('Notifications')}
|
||||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<div className='content-primary'>
|
||||
<h2>{globalize.translate('Notifications')}</h2>
|
||||
<p>
|
||||
{globalize.translate('NotificationsMovedMessage')}
|
||||
</p>
|
||||
<PluginLink />
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
|
||||
export default Notifications;
|
185
src/apps/stable/routes/legacyRoutes/admin.ts
Normal file
185
src/apps/stable/routes/legacyRoutes/admin.ts
Normal file
|
@ -0,0 +1,185 @@
|
|||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||
{
|
||||
path: 'dashboard.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dashboard',
|
||||
view: 'dashboard/dashboard.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dashboardgeneral.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/general',
|
||||
view: 'dashboard/general.html'
|
||||
}
|
||||
}, {
|
||||
path: 'networking.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/networking',
|
||||
view: 'dashboard/networking.html'
|
||||
}
|
||||
}, {
|
||||
path: 'devices.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/devices/devices',
|
||||
view: 'dashboard/devices/devices.html'
|
||||
}
|
||||
}, {
|
||||
path: 'device.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/devices/device',
|
||||
view: 'dashboard/devices/device.html'
|
||||
}
|
||||
}, {
|
||||
path: 'quickConnect.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/quickConnect',
|
||||
view: 'dashboard/quickConnect.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dlnaprofile.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dlna/profile',
|
||||
view: 'dashboard/dlna/profile.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dlnaprofiles.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dlna/profiles',
|
||||
view: 'dashboard/dlna/profiles.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dlnasettings.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dlna/settings',
|
||||
view: 'dashboard/dlna/settings.html'
|
||||
}
|
||||
}, {
|
||||
path: 'addplugin.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/add/index',
|
||||
view: 'dashboard/plugins/add/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'library.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/library',
|
||||
view: 'dashboard/library.html'
|
||||
}
|
||||
}, {
|
||||
path: 'librarydisplay.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/librarydisplay',
|
||||
view: 'dashboard/librarydisplay.html'
|
||||
}
|
||||
}, {
|
||||
path: 'edititemmetadata.html',
|
||||
pageProps: {
|
||||
controller: 'edititemmetadata',
|
||||
view: 'edititemmetadata.html'
|
||||
}
|
||||
}, {
|
||||
path: 'encodingsettings.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/encodingsettings',
|
||||
view: 'dashboard/encodingsettings.html'
|
||||
}
|
||||
}, {
|
||||
path: 'log.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/logs',
|
||||
view: 'dashboard/logs.html'
|
||||
}
|
||||
}, {
|
||||
path: 'metadataimages.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/metadataImages',
|
||||
view: 'dashboard/metadataimages.html'
|
||||
}
|
||||
}, {
|
||||
path: 'metadatanfo.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/metadatanfo',
|
||||
view: 'dashboard/metadatanfo.html'
|
||||
}
|
||||
}, {
|
||||
path: 'playbackconfiguration.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/playback',
|
||||
view: 'dashboard/playback.html'
|
||||
}
|
||||
}, {
|
||||
path: 'availableplugins.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/available/index',
|
||||
view: 'dashboard/plugins/available/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'repositories.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/repositories/index',
|
||||
view: 'dashboard/plugins/repositories/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvguideprovider.html',
|
||||
pageProps: {
|
||||
controller: 'livetvguideprovider',
|
||||
view: 'livetvguideprovider.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvsettings.html',
|
||||
pageProps: {
|
||||
controller: 'livetvsettings',
|
||||
view: 'livetvsettings.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvstatus.html',
|
||||
pageProps: {
|
||||
controller: 'livetvstatus',
|
||||
view: 'livetvstatus.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvtuner.html',
|
||||
pageProps: {
|
||||
controller: 'livetvtuner',
|
||||
view: 'livetvtuner.html'
|
||||
}
|
||||
}, {
|
||||
path: 'installedplugins.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/installed/index',
|
||||
view: 'dashboard/plugins/installed/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'scheduledtask.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/scheduledtasks/scheduledtask',
|
||||
view: 'dashboard/scheduledtasks/scheduledtask.html'
|
||||
}
|
||||
}, {
|
||||
path: 'scheduledtasks.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/scheduledtasks/scheduledtasks',
|
||||
view: 'dashboard/scheduledtasks/scheduledtasks.html'
|
||||
}
|
||||
}, {
|
||||
path: 'serveractivity.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/serveractivity',
|
||||
view: 'dashboard/serveractivity.html'
|
||||
}
|
||||
}, {
|
||||
path: 'apikeys.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/apikeys',
|
||||
view: 'dashboard/apikeys.html'
|
||||
}
|
||||
}, {
|
||||
path: 'streamingsettings.html',
|
||||
pageProps: {
|
||||
view: 'dashboard/streaming.html',
|
||||
controller: 'dashboard/streaming'
|
||||
}
|
||||
}
|
||||
];
|
3
src/apps/stable/routes/legacyRoutes/index.ts
Normal file
3
src/apps/stable/routes/legacyRoutes/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './admin';
|
||||
export * from './public';
|
||||
export * from './user';
|
81
src/apps/stable/routes/legacyRoutes/public.ts
Normal file
81
src/apps/stable/routes/legacyRoutes/public.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_PUBLIC_ROUTES: LegacyRoute[] = [
|
||||
{
|
||||
path: 'addserver.html',
|
||||
pageProps: {
|
||||
controller: 'session/addServer/index',
|
||||
view: 'session/addServer/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'selectserver.html',
|
||||
pageProps: {
|
||||
controller: 'session/selectServer/index',
|
||||
view: 'session/selectServer/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'login.html',
|
||||
pageProps: {
|
||||
controller: 'session/login/index',
|
||||
view: 'session/login/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'forgotpassword.html',
|
||||
pageProps: {
|
||||
controller: 'session/forgotPassword/index',
|
||||
view: 'session/forgotPassword/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'forgotpasswordpin.html',
|
||||
pageProps: {
|
||||
controller: 'session/resetPassword/index',
|
||||
view: 'session/resetPassword/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardremoteaccess.html',
|
||||
pageProps: {
|
||||
controller: 'wizard/remote/index',
|
||||
view: 'wizard/remote/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardfinish.html',
|
||||
pageProps: {
|
||||
controller: 'wizard/finish/index',
|
||||
view: 'wizard/finish/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardlibrary.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/library',
|
||||
view: 'wizard/library.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardsettings.html',
|
||||
pageProps: {
|
||||
controller: 'wizard/settings/index',
|
||||
view: 'wizard/settings/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardstart.html',
|
||||
pageProps: {
|
||||
controller: 'wizard/start/index',
|
||||
view: 'wizard/start/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizarduser.html',
|
||||
pageProps: {
|
||||
controller: 'wizard/user/index',
|
||||
view: 'wizard/user/index.html'
|
||||
}
|
||||
}
|
||||
];
|
108
src/apps/stable/routes/legacyRoutes/user.ts
Normal file
108
src/apps/stable/routes/legacyRoutes/user.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_USER_ROUTES: LegacyRoute[] = [
|
||||
{
|
||||
path: 'details',
|
||||
pageProps: {
|
||||
controller: 'itemDetails/index',
|
||||
view: 'itemDetails/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'list.html',
|
||||
pageProps: {
|
||||
controller: 'list',
|
||||
view: 'list.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetv.html',
|
||||
pageProps: {
|
||||
controller: 'livetv/livetvsuggested',
|
||||
view: 'livetv.html'
|
||||
}
|
||||
}, {
|
||||
path: 'music.html',
|
||||
pageProps: {
|
||||
controller: 'music/musicrecommended',
|
||||
view: 'music/music.html'
|
||||
}
|
||||
}, {
|
||||
path: 'mypreferencesmenu.html',
|
||||
pageProps: {
|
||||
controller: 'user/menu/index',
|
||||
view: 'user/menu/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'mypreferencescontrols.html',
|
||||
pageProps: {
|
||||
controller: 'user/controls/index',
|
||||
view: 'user/controls/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'mypreferencesdisplay.html',
|
||||
pageProps: {
|
||||
controller: 'user/display/index',
|
||||
view: 'user/display/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'mypreferenceshome.html',
|
||||
pageProps: {
|
||||
controller: 'user/home/index',
|
||||
view: 'user/home/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'mypreferencesquickconnect.html',
|
||||
pageProps: {
|
||||
controller: 'user/quickConnect/index',
|
||||
view: 'user/quickConnect/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'mypreferencesplayback.html',
|
||||
pageProps: {
|
||||
controller: 'user/playback/index',
|
||||
view: 'user/playback/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'mypreferencessubtitles.html',
|
||||
pageProps: {
|
||||
controller: 'user/subtitles/index',
|
||||
view: 'user/subtitles/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'tv.html',
|
||||
pageProps: {
|
||||
controller: 'shows/tvrecommended',
|
||||
view: 'shows/tvrecommended.html'
|
||||
}
|
||||
}, {
|
||||
path: 'video',
|
||||
pageProps: {
|
||||
controller: 'playback/video/index',
|
||||
view: 'playback/video/index.html',
|
||||
type: 'video-osd',
|
||||
isFullscreen: true,
|
||||
isNowPlayingBarEnabled: false,
|
||||
isThemeMediaSupported: true
|
||||
}
|
||||
}, {
|
||||
path: 'queue',
|
||||
pageProps: {
|
||||
controller: 'playback/queue/index',
|
||||
view: 'playback/queue/index.html',
|
||||
isFullscreen: true,
|
||||
isNowPlayingBarEnabled: false,
|
||||
isThemeMediaSupported: true
|
||||
}
|
||||
}, {
|
||||
path: 'home.html',
|
||||
pageProps: {
|
||||
controller: 'home',
|
||||
view: 'home.html'
|
||||
}
|
||||
}, {
|
||||
path: 'movies.html',
|
||||
pageProps: {
|
||||
controller: 'movies/moviesrecommended',
|
||||
view: 'movies/movies.html'
|
||||
}
|
||||
}
|
||||
];
|
|
@ -1,12 +1,12 @@
|
|||
import React, { FunctionComponent, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import Page from '../components/Page';
|
||||
import SearchFields from '../components/search/SearchFields';
|
||||
import SearchResults from '../components/search/SearchResults';
|
||||
import SearchSuggestions from '../components/search/SearchSuggestions';
|
||||
import LiveTVSearchResults from '../components/search/LiveTVSearchResults';
|
||||
import globalize from '../scripts/globalize';
|
||||
import Page from '../../../components/Page';
|
||||
import SearchFields from '../../../components/search/SearchFields';
|
||||
import SearchResults from '../../../components/search/SearchResults';
|
||||
import SearchSuggestions from '../../../components/search/SearchSuggestions';
|
||||
import LiveTVSearchResults from '../../../components/search/LiveTVSearchResults';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const Search: FunctionComponent = () => {
|
||||
const [ query, setQuery ] = useState<string>();
|
||||
|
@ -19,9 +19,8 @@ const Search: FunctionComponent = () => {
|
|||
className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage'
|
||||
>
|
||||
<SearchFields onSearch={setQuery} />
|
||||
{!query &&
|
||||
<SearchSuggestions
|
||||
serverId={searchParams.get('serverId') || window.ApiClient.serverId()}
|
||||
{!query
|
||||
&& <SearchSuggestions
|
||||
parentId={searchParams.get('parentId')}
|
||||
/>
|
||||
}
|
|
@ -1,28 +1,43 @@
|
|||
import type { SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import LibraryMenu from '../../scripts/libraryMenu';
|
||||
import ButtonElement from '../../elements/ButtonElement';
|
||||
import CheckBoxElement from '../../elements/CheckBoxElement';
|
||||
import InputElement from '../../elements/InputElement';
|
||||
import LinkEditUserPreferences from '../../components/dashboard/users/LinkEditUserPreferences';
|
||||
import SectionTitleContainer from '../../elements/SectionTitleContainer';
|
||||
import SectionTabs from '../../components/dashboard/users/SectionTabs';
|
||||
import loading from '../../components/loading/loading';
|
||||
import toast from '../../components/toast/toast';
|
||||
import { getParameterByName } from '../../utils/url';
|
||||
import escapeHTML from 'escape-html';
|
||||
import SelectElement from '../../elements/SelectElement';
|
||||
import Page from '../../components/Page';
|
||||
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import LibraryMenu from '../../../../scripts/libraryMenu';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||
import InputElement from '../../../../elements/InputElement';
|
||||
import LinkEditUserPreferences from '../../../../components/dashboard/users/LinkEditUserPreferences';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import SelectElement from '../../../../elements/SelectElement';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type ResetProvider = AuthProvider & {
|
||||
checkedAttribute: string
|
||||
}
|
||||
};
|
||||
|
||||
type AuthProvider = {
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
};
|
||||
|
||||
const getCheckedElementDataIds = (elements: NodeListOf<Element>) => (
|
||||
Array.prototype.filter.call(elements, e => e.checked)
|
||||
.map(e => e.getAttribute('data-id'))
|
||||
);
|
||||
|
||||
function onSaveComplete() {
|
||||
Dashboard.navigate('userprofiles.html')
|
||||
.catch(err => {
|
||||
console.error('[useredit] failed to navigate to user profile', err);
|
||||
});
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
const UserEdit: FunctionComponent = () => {
|
||||
|
@ -56,7 +71,7 @@ const UserEdit: FunctionComponent = () => {
|
|||
}
|
||||
|
||||
const fldSelectLoginProvider = page.querySelector('.fldSelectLoginProvider') as HTMLDivElement;
|
||||
providers.length > 1 ? fldSelectLoginProvider.classList.remove('hide') : fldSelectLoginProvider.classList.add('hide');
|
||||
fldSelectLoginProvider.classList.toggle('hide', providers.length <= 1);
|
||||
|
||||
setAuthProviders(providers);
|
||||
|
||||
|
@ -73,7 +88,7 @@ const UserEdit: FunctionComponent = () => {
|
|||
}
|
||||
|
||||
const fldSelectPasswordResetProvider = page.querySelector('.fldSelectPasswordResetProvider') as HTMLDivElement;
|
||||
providers.length > 1 ? fldSelectPasswordResetProvider.classList.remove('hide') : fldSelectPasswordResetProvider.classList.add('hide');
|
||||
fldSelectPasswordResetProvider.classList.toggle('hide', providers.length <= 1);
|
||||
|
||||
setPasswordResetProviders(providers);
|
||||
|
||||
|
@ -121,6 +136,8 @@ const UserEdit: FunctionComponent = () => {
|
|||
const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement;
|
||||
chkEnableDeleteAllFolders.checked = user.Policy.EnableContentDeletion;
|
||||
triggerChange(chkEnableDeleteAllFolders);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch channels', err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
@ -134,18 +151,24 @@ const UserEdit: FunctionComponent = () => {
|
|||
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/Providers')).then(function (providers) {
|
||||
loadAuthProviders(user, providers);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch auth providers', err);
|
||||
});
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) {
|
||||
loadPasswordResetProviders(user, providers);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch password reset providers', err);
|
||||
});
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
|
||||
IsHidden: false
|
||||
})).then(function (folders) {
|
||||
loadDeleteFolders(user, folders.Items);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch media folders', err);
|
||||
});
|
||||
|
||||
const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement;
|
||||
user.Policy.IsDisabled ? disabledUserBanner.classList.remove('hide') : disabledUserBanner.classList.add('hide');
|
||||
disabledUserBanner.classList.toggle('hide', !user.Policy.IsDisabled);
|
||||
|
||||
const txtUserName = page.querySelector('#txtUserName') as HTMLInputElement;
|
||||
txtUserName.disabled = false;
|
||||
|
@ -159,6 +182,7 @@ const UserEdit: FunctionComponent = () => {
|
|||
(page.querySelector('.chkIsAdmin') as HTMLInputElement).checked = user.Policy.IsAdministrator;
|
||||
(page.querySelector('.chkDisabled') as HTMLInputElement).checked = user.Policy.IsDisabled;
|
||||
(page.querySelector('.chkIsHidden') as HTMLInputElement).checked = user.Policy.IsHidden;
|
||||
(page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked = user.Policy.EnableCollectionManagement;
|
||||
(page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = user.Policy.EnableSharedDeviceControl;
|
||||
(page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = user.Policy.EnableRemoteControlOfOtherUsers;
|
||||
(page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = user.Policy.EnableContentDownloading;
|
||||
|
@ -171,7 +195,7 @@ const UserEdit: FunctionComponent = () => {
|
|||
(page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked = user.Policy.ForceRemoteSourceTranscoding;
|
||||
(page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked = user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess;
|
||||
(page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value = user.Policy.RemoteClientBitrateLimit > 0 ?
|
||||
(user.Policy.RemoteClientBitrateLimit / 1e6).toLocaleString(undefined, {maximumFractionDigits: 6}) : '';
|
||||
(user.Policy.RemoteClientBitrateLimit / 1e6).toLocaleString(undefined, { maximumFractionDigits: 6 }) : '';
|
||||
(page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value = user.Policy.LoginAttemptsBeforeLockout || '0';
|
||||
(page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = user.Policy.MaxActiveSessions || '0';
|
||||
if (window.ApiClient.isMinServerVersion('10.6.0')) {
|
||||
|
@ -184,6 +208,8 @@ const UserEdit: FunctionComponent = () => {
|
|||
loading.show();
|
||||
getUser().then(function (user) {
|
||||
loadUser(user);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to load data', err);
|
||||
});
|
||||
}, [loadUser]);
|
||||
|
||||
|
@ -197,19 +223,9 @@ const UserEdit: FunctionComponent = () => {
|
|||
|
||||
loadData();
|
||||
|
||||
function onSaveComplete() {
|
||||
Dashboard.navigate('userprofiles.html');
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
const saveUser = (user: UserDto) => {
|
||||
if (!user.Id) {
|
||||
throw new Error('Unexpected null user.Id');
|
||||
}
|
||||
|
||||
if (!user.Policy) {
|
||||
throw new Error('Unexpected null user.Policy');
|
||||
if (!user.Id || !user.Policy) {
|
||||
throw new Error('Unexpected null user id or policy');
|
||||
}
|
||||
|
||||
user.Name = (page.querySelector('#txtUserName') as HTMLInputElement).value;
|
||||
|
@ -224,27 +240,25 @@ const UserEdit: FunctionComponent = () => {
|
|||
user.Policy.EnableAudioPlaybackTranscoding = (page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked;
|
||||
user.Policy.EnableVideoPlaybackTranscoding = (page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked;
|
||||
user.Policy.EnablePlaybackRemuxing = (page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked;
|
||||
user.Policy.EnableCollectionManagement = (page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked;
|
||||
user.Policy.ForceRemoteSourceTranscoding = (page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked;
|
||||
user.Policy.EnableContentDownloading = (page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked;
|
||||
user.Policy.EnableRemoteAccess = (page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked;
|
||||
user.Policy.RemoteClientBitrateLimit = Math.floor(1e6 * parseFloat((page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value || '0'));
|
||||
user.Policy.LoginAttemptsBeforeLockout = parseInt((page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value || '0');
|
||||
user.Policy.MaxActiveSessions = parseInt((page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value || '0');
|
||||
user.Policy.LoginAttemptsBeforeLockout = parseInt((page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value || '0', 10);
|
||||
user.Policy.MaxActiveSessions = parseInt((page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value || '0', 10);
|
||||
user.Policy.AuthenticationProviderId = (page.querySelector('#selectLoginProvider') as HTMLSelectElement).value;
|
||||
user.Policy.PasswordResetProviderId = (page.querySelector('#selectPasswordResetProvider') as HTMLSelectElement).value;
|
||||
user.Policy.EnableContentDeletion = (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).checked;
|
||||
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
if (window.ApiClient.isMinServerVersion('10.6.0')) {
|
||||
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType;
|
||||
}
|
||||
window.ApiClient.updateUser(user).then(function () {
|
||||
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {}).then(function () {
|
||||
onSaveComplete();
|
||||
});
|
||||
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : getCheckedElementDataIds(page.querySelectorAll('.chkFolder'));
|
||||
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType;
|
||||
|
||||
window.ApiClient.updateUser(user).then(() => (
|
||||
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {})
|
||||
)).then(() => {
|
||||
onSaveComplete();
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to update user', err);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -252,6 +266,8 @@ const UserEdit: FunctionComponent = () => {
|
|||
loading.show();
|
||||
getUser().then(function (result) {
|
||||
saveUser(result);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch user', err);
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -259,16 +275,13 @@ const UserEdit: FunctionComponent = () => {
|
|||
};
|
||||
|
||||
(page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
|
||||
if (this.checked) {
|
||||
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.add('hide');
|
||||
} else {
|
||||
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.remove('hide');
|
||||
}
|
||||
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.toggle('hide', this.checked);
|
||||
});
|
||||
|
||||
window.ApiClient.getNamedConfiguration('network').then(function (config) {
|
||||
const fldRemoteAccess = page.querySelector('.fldRemoteAccess') as HTMLDivElement;
|
||||
config.EnableRemoteAccess ? fldRemoteAccess.classList.remove('hide') : fldRemoteAccess.classList.add('hide');
|
||||
(page.querySelector('.fldRemoteAccess') as HTMLDivElement).classList.toggle('hide', !config.EnableRemoteAccess);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to load network config', err);
|
||||
});
|
||||
|
||||
(page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
|
||||
|
@ -312,7 +325,7 @@ const UserEdit: FunctionComponent = () => {
|
|||
<SectionTabs activeTab='useredit'/>
|
||||
<div
|
||||
className='lnkEditUserPreferencesContainer'
|
||||
style={{paddingBottom: '1em'}}
|
||||
style={{ paddingBottom: '1em' }}
|
||||
>
|
||||
<LinkEditUserPreferences
|
||||
className= 'lnkEditUserPreferences button-link'
|
||||
|
@ -325,7 +338,7 @@ const UserEdit: FunctionComponent = () => {
|
|||
<div>
|
||||
{globalize.translate('HeaderThisUserIsCurrentlyDisabled')}
|
||||
</div>
|
||||
<div style={{marginTop: 5}}>
|
||||
<div style={{ marginTop: 5 }}>
|
||||
{globalize.translate('MessageReenableUser')}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -375,11 +388,16 @@ const UserEdit: FunctionComponent = () => {
|
|||
className='chkIsAdmin'
|
||||
title='OptionAllowUserToManageServer'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
className='chkEnableCollectionManagement'
|
||||
title='AllowCollectionManagement'
|
||||
/>
|
||||
<div id='featureAccessFields' className='verticalSection'>
|
||||
<h2 className='paperListLabel'>
|
||||
{globalize.translate('HeaderFeatureAccess')}
|
||||
</h2>
|
||||
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
|
||||
<CheckBoxElement
|
||||
className='chkEnableLiveTvAccess'
|
||||
title='OptionAllowBrowsingLiveTv'
|
||||
|
@ -394,7 +412,7 @@ const UserEdit: FunctionComponent = () => {
|
|||
<h2 className='paperListLabel'>
|
||||
{globalize.translate('HeaderPlayback')}
|
||||
</h2>
|
||||
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
|
||||
<CheckBoxElement
|
||||
className='chkEnableMediaPlayback'
|
||||
title='OptionAllowMediaPlayback'
|
||||
|
@ -451,7 +469,7 @@ const UserEdit: FunctionComponent = () => {
|
|||
</div>
|
||||
</div>
|
||||
<div className='verticalSection'>
|
||||
<h2 className='checkboxListLabel' style={{marginBottom: '1em'}}>
|
||||
<h2 className='checkboxListLabel' style={{ marginBottom: '1em' }}>
|
||||
{globalize.translate('HeaderAllowMediaDeletionFrom')}
|
||||
</h2>
|
||||
<div className='checkboxList paperList checkboxList-paperList'>
|
||||
|
@ -477,7 +495,7 @@ const UserEdit: FunctionComponent = () => {
|
|||
<h2 className='checkboxListLabel'>
|
||||
{globalize.translate('HeaderRemoteControl')}
|
||||
</h2>
|
||||
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
|
||||
<CheckBoxElement
|
||||
className='chkEnableRemoteControlOtherUsers'
|
||||
title='OptionAllowRemoteControlOthers'
|
|
@ -1,24 +1,24 @@
|
|||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
|
||||
import loading from '../../components/loading/loading';
|
||||
import libraryMenu from '../../scripts/libraryMenu';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import toast from '../../components/toast/toast';
|
||||
import SectionTabs from '../../components/dashboard/users/SectionTabs';
|
||||
import ButtonElement from '../../elements/ButtonElement';
|
||||
import { getParameterByName } from '../../utils/url';
|
||||
import SectionTitleContainer from '../../elements/SectionTitleContainer';
|
||||
import AccessContainer from '../../components/dashboard/users/AccessContainer';
|
||||
import CheckBoxElement from '../../elements/CheckBoxElement';
|
||||
import Page from '../../components/Page';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import libraryMenu from '../../../../scripts/libraryMenu';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import AccessContainer from '../../../../components/dashboard/users/AccessContainer';
|
||||
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type ItemsArr = {
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
AppName?: string;
|
||||
checkedAttribute?: string
|
||||
}
|
||||
};
|
||||
|
||||
const UserLibraryAccess: FunctionComponent = () => {
|
||||
const [ userName, setUserName ] = useState('');
|
||||
|
@ -148,6 +148,8 @@ const UserLibraryAccess: FunctionComponent = () => {
|
|||
const promise4 = window.ApiClient.getJSON(window.ApiClient.getUrl('Devices'));
|
||||
Promise.all([promise1, promise2, promise3, promise4]).then(function (responses) {
|
||||
loadUser(responses[0], responses[1].Items, responses[2].Items, responses[3].Items);
|
||||
}).catch(err => {
|
||||
console.error('[userlibraryaccess] failed to load data', err);
|
||||
});
|
||||
}, [loadUser]);
|
||||
|
||||
|
@ -166,6 +168,8 @@ const UserLibraryAccess: FunctionComponent = () => {
|
|||
const userId = getParameterByName('userId');
|
||||
window.ApiClient.getUser(userId).then(function (result) {
|
||||
saveUser(result);
|
||||
}).catch(err => {
|
||||
console.error('[userlibraryaccess] failed to fetch user', err);
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -203,6 +207,8 @@ const UserLibraryAccess: FunctionComponent = () => {
|
|||
user.Policy.BlockedMediaFolders = null;
|
||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
onSaveComplete();
|
||||
}).catch(err => {
|
||||
console.error('[userlibraryaccess] failed to update user policy', err);
|
||||
});
|
||||
};
|
||||
|
|
@ -1,25 +1,25 @@
|
|||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import loading from '../../components/loading/loading';
|
||||
import toast from '../../components/toast/toast';
|
||||
import SectionTitleContainer from '../../elements/SectionTitleContainer';
|
||||
import InputElement from '../../elements/InputElement';
|
||||
import ButtonElement from '../../elements/ButtonElement';
|
||||
import AccessContainer from '../../components/dashboard/users/AccessContainer';
|
||||
import CheckBoxElement from '../../elements/CheckBoxElement';
|
||||
import Page from '../../components/Page';
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import InputElement from '../../../../elements/InputElement';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import AccessContainer from '../../../../components/dashboard/users/AccessContainer';
|
||||
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type userInput = {
|
||||
Name?: string;
|
||||
Password?: string;
|
||||
}
|
||||
};
|
||||
|
||||
type ItemsArr = {
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
}
|
||||
};
|
||||
|
||||
const UserNew: FunctionComponent = () => {
|
||||
const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]);
|
||||
|
@ -93,6 +93,8 @@ const UserNew: FunctionComponent = () => {
|
|||
loadMediaFolders(responses[0].Items);
|
||||
loadChannels(responses[1].Items);
|
||||
loading.hide();
|
||||
}).catch(err => {
|
||||
console.error('[usernew] failed to load data', err);
|
||||
});
|
||||
}, [loadChannels, loadMediaFolders]);
|
||||
|
||||
|
@ -111,12 +113,8 @@ const UserNew: FunctionComponent = () => {
|
|||
userInput.Name = (page.querySelector('#txtUsername') as HTMLInputElement).value;
|
||||
userInput.Password = (page.querySelector('#txtPassword') as HTMLInputElement).value;
|
||||
window.ApiClient.createUser(userInput).then(function (user) {
|
||||
if (!user.Id) {
|
||||
throw new Error('Unexpected null user.Id');
|
||||
}
|
||||
|
||||
if (!user.Policy) {
|
||||
throw new Error('Unexpected null user.Policy');
|
||||
if (!user.Id || !user.Policy) {
|
||||
throw new Error('Unexpected null user id or policy');
|
||||
}
|
||||
|
||||
user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked;
|
||||
|
@ -142,7 +140,12 @@ const UserNew: FunctionComponent = () => {
|
|||
}
|
||||
|
||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
Dashboard.navigate('useredit.html?userId=' + user.Id);
|
||||
Dashboard.navigate('useredit.html?userId=' + user.Id)
|
||||
.catch(err => {
|
||||
console.error('[usernew] failed to navigate to edit user page', err);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[usernew] failed to update user policy', err);
|
||||
});
|
||||
}, function () {
|
||||
toast(globalize.translate('ErrorDefault'));
|
|
@ -1,26 +1,27 @@
|
|||
import type { AccessSchedule, ParentalRating, UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { DynamicDayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/dynamic-day-of-week';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import LibraryMenu from '../../scripts/libraryMenu';
|
||||
import AccessScheduleList from '../../components/dashboard/users/AccessScheduleList';
|
||||
import BlockedTagList from '../../components/dashboard/users/BlockedTagList';
|
||||
import ButtonElement from '../../elements/ButtonElement';
|
||||
import SectionTitleContainer from '../../elements/SectionTitleContainer';
|
||||
import SectionTabs from '../../components/dashboard/users/SectionTabs';
|
||||
import loading from '../../components/loading/loading';
|
||||
import toast from '../../components/toast/toast';
|
||||
import { getParameterByName } from '../../utils/url';
|
||||
import CheckBoxElement from '../../elements/CheckBoxElement';
|
||||
import escapeHTML from 'escape-html';
|
||||
import SelectElement from '../../elements/SelectElement';
|
||||
import Page from '../../components/Page';
|
||||
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import LibraryMenu from '../../../../scripts/libraryMenu';
|
||||
import AccessScheduleList from '../../../../components/dashboard/users/AccessScheduleList';
|
||||
import BlockedTagList from '../../../../components/dashboard/users/BlockedTagList';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||
import SelectElement from '../../../../elements/SelectElement';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type UnratedItem = {
|
||||
name: string;
|
||||
value: string;
|
||||
checkedAttribute: string
|
||||
}
|
||||
};
|
||||
|
||||
const UserParentalControl: FunctionComponent = () => {
|
||||
const [ userName, setUserName ] = useState('');
|
||||
|
@ -142,7 +143,7 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
|
||||
for (const btnDelete of accessScheduleList.querySelectorAll('.btnDelete')) {
|
||||
btnDelete.addEventListener('click', function () {
|
||||
const index = parseInt(btnDelete.getAttribute('data-index') || '0', 10);
|
||||
const index = parseInt(btnDelete.getAttribute('data-index') ?? '0', 10);
|
||||
schedules.splice(index, 1);
|
||||
const newindex = schedules.filter(function (i: number) {
|
||||
return i != index;
|
||||
|
@ -196,6 +197,8 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
const promise2 = window.ApiClient.getParentalRatings();
|
||||
Promise.all([promise1, promise2]).then(function (responses) {
|
||||
loadUser(responses[0], responses[1]);
|
||||
}).catch(err => {
|
||||
console.error('[userparentalcontrol] failed to load data', err);
|
||||
});
|
||||
}, [loadUser]);
|
||||
|
||||
|
@ -215,15 +218,11 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
};
|
||||
|
||||
const saveUser = (user: UserDto) => {
|
||||
if (!user.Id) {
|
||||
throw new Error('Unexpected null user.Id');
|
||||
if (!user.Id || !user.Policy) {
|
||||
throw new Error('Unexpected null user id or policy');
|
||||
}
|
||||
|
||||
if (!user.Policy) {
|
||||
throw new Error('Unexpected null user.Policy');
|
||||
}
|
||||
|
||||
const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value || '0', 10);
|
||||
const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10);
|
||||
user.Policy.MaxParentalRating = Number.isNaN(parentalRating) ? null : parentalRating;
|
||||
user.Policy.BlockUnratedItems = Array.prototype.filter.call(page.querySelectorAll('.chkUnratedItem'), function (i) {
|
||||
return i.checked;
|
||||
|
@ -234,12 +233,14 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
user.Policy.BlockedTags = getBlockedTagsFromPage();
|
||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
onSaveComplete();
|
||||
}).catch(err => {
|
||||
console.error('[userparentalcontrol] failed to update user policy', err);
|
||||
});
|
||||
};
|
||||
|
||||
const showSchedulePopup = (schedule: AccessSchedule, index: number) => {
|
||||
schedule = schedule || {};
|
||||
import('../../components/accessSchedule/accessSchedule').then(({default: accessschedule}) => {
|
||||
import('../../../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => {
|
||||
accessschedule.show({
|
||||
schedule: schedule
|
||||
}).then(function (updatedSchedule) {
|
||||
|
@ -251,7 +252,11 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
|
||||
schedules[index] = updatedSchedule;
|
||||
renderAccessSchedule(schedules);
|
||||
}).catch(() => {
|
||||
// access schedule closed
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[userparentalcontrol] failed to load access schedule', err);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -272,7 +277,7 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
};
|
||||
|
||||
const showBlockedTagPopup = () => {
|
||||
import('../../components/prompt/prompt').then(({default: prompt}) => {
|
||||
import('../../../../components/prompt/prompt').then(({ default: prompt }) => {
|
||||
prompt({
|
||||
label: globalize.translate('LabelTag')
|
||||
}).then(function (value) {
|
||||
|
@ -282,7 +287,11 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
tags.push(value);
|
||||
loadBlockedTags(tags);
|
||||
}
|
||||
}).catch(() => {
|
||||
// prompt closed
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[userparentalcontrol] failed to load prompt', err);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -291,6 +300,8 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
const userId = getParameterByName('userId');
|
||||
window.ApiClient.getUser(userId).then(function (result) {
|
||||
saveUser(result);
|
||||
}).catch(err => {
|
||||
console.error('[userparentalcontrol] failed to fetch user', err);
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -367,7 +378,7 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div className='verticalSection' style={{marginBottom: '2em'}}>
|
||||
<div className='verticalSection' style={{ marginBottom: '2em' }}>
|
||||
<SectionTitleContainer
|
||||
SectionClassName='detailSectionHeader'
|
||||
title={globalize.translate('LabelBlockContentWithTags')}
|
||||
|
@ -378,16 +389,16 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
btnIcon='add'
|
||||
isLinkVisible={false}
|
||||
/>
|
||||
<div className='blockedTags' style={{marginTop: '.5em'}}>
|
||||
{blockedTags.map((tag, index) => {
|
||||
<div className='blockedTags' style={{ marginTop: '.5em' }}>
|
||||
{blockedTags.map(tag => {
|
||||
return <BlockedTagList
|
||||
key={index}
|
||||
key={tag}
|
||||
tag={tag}
|
||||
/>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className='accessScheduleSection verticalSection' style={{marginBottom: '2em'}}>
|
||||
<div className='accessScheduleSection verticalSection' style={{ marginBottom: '2em' }}>
|
||||
<SectionTitleContainer
|
||||
title={globalize.translate('HeaderAccessSchedule')}
|
||||
isBtnVisible={true}
|
||||
|
@ -401,7 +412,7 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
<div className='accessScheduleList paperList'>
|
||||
{accessSchedules.map((accessSchedule, index) => {
|
||||
return <AccessScheduleList
|
||||
key={index}
|
||||
key={accessSchedule.Id}
|
||||
index={index}
|
||||
Id={accessSchedule.Id}
|
||||
DayOfWeek={accessSchedule.DayOfWeek}
|
|
@ -1,10 +1,11 @@
|
|||
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
|
||||
import SectionTabs from '../../components/dashboard/users/SectionTabs';
|
||||
import UserPasswordForm from '../../components/dashboard/users/UserPasswordForm';
|
||||
import { getParameterByName } from '../../utils/url';
|
||||
import SectionTitleContainer from '../../elements/SectionTitleContainer';
|
||||
import Page from '../../components/Page';
|
||||
import loading from '../../components/loading/loading';
|
||||
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import Page from '../../../../components/Page';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
|
||||
const UserPassword: FunctionComponent = () => {
|
||||
const userId = getParameterByName('userId');
|
||||
|
@ -18,6 +19,8 @@ const UserPassword: FunctionComponent = () => {
|
|||
}
|
||||
setUserName(user.Name);
|
||||
loading.hide();
|
||||
}).catch(err => {
|
||||
console.error('[userpassword] failed to fetch user', err);
|
||||
});
|
||||
}, [userId]);
|
||||
useEffect(() => {
|
|
@ -2,17 +2,17 @@ import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
|||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||
import React, { FunctionComponent, useEffect, useState, useRef, useCallback } from 'react';
|
||||
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import LibraryMenu from '../../scripts/libraryMenu';
|
||||
import { appHost } from '../../components/apphost';
|
||||
import confirm from '../../components/confirm/confirm';
|
||||
import ButtonElement from '../../elements/ButtonElement';
|
||||
import UserPasswordForm from '../../components/dashboard/users/UserPasswordForm';
|
||||
import loading from '../../components/loading/loading';
|
||||
import toast from '../../components/toast/toast';
|
||||
import { getParameterByName } from '../../utils/url';
|
||||
import Page from '../../components/Page';
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import LibraryMenu from '../../../../scripts/libraryMenu';
|
||||
import { appHost } from '../../../../components/apphost';
|
||||
import confirm from '../../../../components/confirm/confirm';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
const UserProfile: FunctionComponent = () => {
|
||||
const userId = getParameterByName('userId');
|
||||
|
@ -30,12 +30,8 @@ const UserProfile: FunctionComponent = () => {
|
|||
|
||||
loading.show();
|
||||
window.ApiClient.getUser(userId).then(function (user) {
|
||||
if (!user.Name) {
|
||||
throw new Error('Unexpected null user.Name');
|
||||
}
|
||||
|
||||
if (!user.Id) {
|
||||
throw new Error('Unexpected null user.Id');
|
||||
if (!user.Name || !user.Id) {
|
||||
throw new Error('Unexpected null user name or id');
|
||||
}
|
||||
|
||||
setUserName(user.Name);
|
||||
|
@ -63,8 +59,12 @@ const UserProfile: FunctionComponent = () => {
|
|||
(page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.add('hide');
|
||||
(page.querySelector('#btnAddImage') as HTMLButtonElement).classList.remove('hide');
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('[userprofile] failed to get current user', err);
|
||||
});
|
||||
loading.hide();
|
||||
}).catch(err => {
|
||||
console.error('[userprofile] failed to load data', err);
|
||||
});
|
||||
}, [userId]);
|
||||
|
||||
|
@ -102,7 +102,7 @@ const UserProfile: FunctionComponent = () => {
|
|||
const target = evt.target as HTMLInputElement;
|
||||
const file = (target.files as FileList)[0];
|
||||
|
||||
if (!file || !file.type.match('image.*')) {
|
||||
if (!file || !/image.*/.exec(file.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -114,6 +114,8 @@ const UserProfile: FunctionComponent = () => {
|
|||
window.ApiClient.uploadUserImage(userId, ImageType.Primary, file).then(function () {
|
||||
loading.hide();
|
||||
reloadUser();
|
||||
}).catch(err => {
|
||||
console.error('[userprofile] failed to upload image', err);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -129,7 +131,11 @@ const UserProfile: FunctionComponent = () => {
|
|||
window.ApiClient.deleteUserImage(userId, ImageType.Primary).then(function () {
|
||||
loading.hide();
|
||||
reloadUser();
|
||||
}).catch(err => {
|
||||
console.error('[userprofile] failed to delete image', err);
|
||||
});
|
||||
}).catch(() => {
|
||||
// confirm dialog closed
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -153,25 +159,25 @@ const UserProfile: FunctionComponent = () => {
|
|||
<div ref={element} className='padded-left padded-right padded-bottom-page'>
|
||||
<div
|
||||
className='readOnlyContent'
|
||||
style={{margin: '0 auto', marginBottom: '1.8em', padding: '0 1em', display: 'flex', flexDirection: 'row', alignItems: 'center'}}
|
||||
style={{ margin: '0 auto', marginBottom: '1.8em', padding: '0 1em', display: 'flex', flexDirection: 'row', alignItems: 'center' }}
|
||||
>
|
||||
<div
|
||||
className='imagePlaceHolder'
|
||||
style={{position: 'relative', display: 'inline-block', maxWidth: 200 }}
|
||||
style={{ position: 'relative', display: 'inline-block', maxWidth: 200 }}
|
||||
>
|
||||
<input
|
||||
id='uploadImage'
|
||||
type='file'
|
||||
accept='image/*'
|
||||
style={{position: 'absolute', right: 0, width: '100%', height: '100%', opacity: 0, cursor: 'pointer'}}
|
||||
style={{ position: 'absolute', right: 0, width: '100%', height: '100%', opacity: 0, cursor: 'pointer' }}
|
||||
/>
|
||||
<div
|
||||
id='image'
|
||||
style={{width: 200, height: 200, backgroundRepeat: 'no-repeat', backgroundPosition: 'center', borderRadius: '100%', backgroundSize: 'cover'}}
|
||||
style={{ width: 200, height: 200, backgroundRepeat: 'no-repeat', backgroundPosition: 'center', borderRadius: '100%', backgroundSize: 'cover' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{verticalAlign: 'top', margin: '1em 2em', display: 'flex', flexDirection: 'column', alignItems: 'center'}}>
|
||||
<h2 className='username' style={{margin: 0, fontSize: 'xx-large'}}>
|
||||
<div style={{ verticalAlign: 'top', margin: '1em 2em', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<h2 className='username' style={{ margin: 0, fontSize: 'xx-large' }}>
|
||||
{userName}
|
||||
</h2>
|
||||
<br />
|
|
@ -1,24 +1,25 @@
|
|||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, {FunctionComponent, useEffect, useState, useRef} from 'react';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import loading from '../../components/loading/loading';
|
||||
import dom from '../../scripts/dom';
|
||||
import confirm from '../../components/confirm/confirm';
|
||||
import UserCardBox from '../../components/dashboard/users/UserCardBox';
|
||||
import SectionTitleContainer from '../../elements/SectionTitleContainer';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../../components/cardbuilder/card.scss';
|
||||
import '../../components/indicators/indicators.scss';
|
||||
import '../../assets/css/flexstyles.scss';
|
||||
import Page from '../../components/Page';
|
||||
import React, { FunctionComponent, useEffect, useState, useRef } from 'react';
|
||||
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import dom from '../../../../scripts/dom';
|
||||
import confirm from '../../../../components/confirm/confirm';
|
||||
import UserCardBox from '../../../../components/dashboard/users/UserCardBox';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import '../../../../elements/emby-button/emby-button';
|
||||
import '../../../../elements/emby-button/paper-icon-button-light';
|
||||
import '../../../../components/cardbuilder/card.scss';
|
||||
import '../../../../components/indicators/indicators.scss';
|
||||
import '../../../../styles/flexstyles.scss';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type MenuEntry = {
|
||||
name?: string;
|
||||
id?: string;
|
||||
icon?: string;
|
||||
}
|
||||
};
|
||||
|
||||
const UserProfiles: FunctionComponent = () => {
|
||||
const [ users, setUsers ] = useState<UserDto[]>([]);
|
||||
|
@ -30,6 +31,8 @@ const UserProfiles: FunctionComponent = () => {
|
|||
window.ApiClient.getUsers().then(function (result) {
|
||||
setUsers(result);
|
||||
loading.hide();
|
||||
}).catch(err => {
|
||||
console.error('[userprofiles] failed to fetch users', err);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -75,29 +78,42 @@ const UserProfiles: FunctionComponent = () => {
|
|||
icon: 'delete'
|
||||
});
|
||||
|
||||
import('../../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
|
||||
import('../../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
actionsheet.show({
|
||||
items: menuItems,
|
||||
positionTo: card,
|
||||
callback: function (id: string) {
|
||||
switch (id) {
|
||||
case 'open':
|
||||
Dashboard.navigate('useredit.html?userId=' + userId);
|
||||
Dashboard.navigate('useredit.html?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to user edit page', err);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'access':
|
||||
Dashboard.navigate('userlibraryaccess.html?userId=' + userId);
|
||||
Dashboard.navigate('userlibraryaccess.html?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to user library page', err);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'parentalcontrol':
|
||||
Dashboard.navigate('userparentalcontrol.html?userId=' + userId);
|
||||
Dashboard.navigate('userparentalcontrol.html?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to parental control page', err);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
deleteUser(userId);
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
// action sheet closed
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[userprofiles] failed to load action sheet', err);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -113,7 +129,11 @@ const UserProfiles: FunctionComponent = () => {
|
|||
loading.show();
|
||||
window.ApiClient.deleteUser(id).then(function () {
|
||||
loadData();
|
||||
}).catch(err => {
|
||||
console.error('[userprofiles] failed to delete user', err);
|
||||
});
|
||||
}).catch(() => {
|
||||
// confirm dialog closed
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -126,7 +146,10 @@ const UserProfiles: FunctionComponent = () => {
|
|||
});
|
||||
|
||||
(page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
|
||||
Dashboard.navigate('usernew.html');
|
||||
Dashboard.navigate('usernew.html')
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to new user page', err);
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
20
src/components/AppHeader.tsx
Normal file
20
src/components/AppHeader.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React, { useEffect } from 'react';
|
||||
|
||||
const AppHeader = () => {
|
||||
useEffect(() => {
|
||||
// Initialize the UI components after first render
|
||||
import('../scripts/libraryMenu');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='mainDrawer hide'>
|
||||
<div className='mainDrawer-scrollContainer scrollContainer focuscontainer-y' />
|
||||
</div>
|
||||
<div className='skinHeader focuscontainer-x' />
|
||||
<div className='mainDrawerHandle' />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppHeader;
|
17
src/components/Backdrop.tsx
Normal file
17
src/components/Backdrop.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import React, { useEffect } from 'react';
|
||||
|
||||
const Backdrop = () => {
|
||||
useEffect(() => {
|
||||
// Initialize the UI components after first render
|
||||
import('../scripts/autoBackdrops');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='backdropContainer' />
|
||||
<div className='backgroundContainer' />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Backdrop;
|
|
@ -1,9 +1,9 @@
|
|||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import type { ConnectResponse } from 'jellyfin-apiclient';
|
||||
|
||||
import alert from './alert';
|
||||
import { appRouter } from './appRouter';
|
||||
import { appRouter } from './router/appRouter';
|
||||
import Loading from './loading/LoadingComponent';
|
||||
import ServerConnections from './ServerConnections';
|
||||
import globalize from '../scripts/globalize';
|
||||
|
@ -31,120 +31,138 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
|||
isUserRequired = true
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const [ isLoading, setIsLoading ] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const bounce = async (connectionResponse: ConnectResponse) => {
|
||||
switch (connectionResponse.State) {
|
||||
case ConnectionState.SignedIn:
|
||||
// Already logged in, bounce to the home page
|
||||
console.debug('[ConnectionRequired] already logged in, redirecting to home');
|
||||
navigate(BounceRoutes.Home);
|
||||
return;
|
||||
case ConnectionState.ServerSignIn:
|
||||
// Bounce to the login page
|
||||
const bounce = useCallback(async (connectionResponse: ConnectResponse) => {
|
||||
switch (connectionResponse.State) {
|
||||
case ConnectionState.SignedIn:
|
||||
// Already logged in, bounce to the home page
|
||||
console.debug('[ConnectionRequired] already logged in, redirecting to home');
|
||||
navigate(BounceRoutes.Home);
|
||||
return;
|
||||
case ConnectionState.ServerSignIn:
|
||||
// Bounce to the login page
|
||||
if (location.pathname === BounceRoutes.Login) {
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
console.debug('[ConnectionRequired] not logged in, redirecting to login page');
|
||||
navigate(BounceRoutes.Login, {
|
||||
state: {
|
||||
serverid: connectionResponse.ApiClient.serverId()
|
||||
}
|
||||
navigate(`${BounceRoutes.Login}?serverid=${connectionResponse.ApiClient.serverId()}`);
|
||||
}
|
||||
return;
|
||||
case ConnectionState.ServerSelection:
|
||||
// Bounce to select server page
|
||||
console.debug('[ConnectionRequired] redirecting to select server page');
|
||||
navigate(BounceRoutes.SelectServer);
|
||||
return;
|
||||
case ConnectionState.ServerUpdateNeeded:
|
||||
// Show update needed message and bounce to select server page
|
||||
try {
|
||||
await alert({
|
||||
text: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin'),
|
||||
html: globalize.translate('ServerUpdateNeeded', '<a href="https://github.com/jellyfin/jellyfin">https://github.com/jellyfin/jellyfin</a>')
|
||||
});
|
||||
} catch (ex) {
|
||||
console.warn('[ConnectionRequired] failed to show alert', ex);
|
||||
}
|
||||
console.debug('[ConnectionRequired] server update required, redirecting to select server page');
|
||||
navigate(BounceRoutes.SelectServer);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn('[ConnectionRequired] unhandled connection state', connectionResponse.State);
|
||||
}, [location.pathname, navigate]);
|
||||
|
||||
const handleIncompleteWizard = useCallback(async (firstConnection: ConnectResponse) => {
|
||||
if (firstConnection.State === ConnectionState.ServerSignIn) {
|
||||
// Verify the wizard is complete
|
||||
try {
|
||||
const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`);
|
||||
if (!infoResponse.ok) {
|
||||
throw new Error('Public system info request failed');
|
||||
}
|
||||
const systemInfo = await infoResponse.json();
|
||||
if (!systemInfo?.StartupWizardCompleted) {
|
||||
// Update the current ApiClient
|
||||
// TODO: Is there a better place to handle this?
|
||||
ServerConnections.setLocalApiClient(firstConnection.ApiClient);
|
||||
// Bounce to the wizard
|
||||
console.info('[ConnectionRequired] startup wizard is not complete, redirecting there');
|
||||
navigate(BounceRoutes.StartWizard);
|
||||
return;
|
||||
case ConnectionState.ServerSelection:
|
||||
// Bounce to select server page
|
||||
console.debug('[ConnectionRequired] redirecting to select server page');
|
||||
navigate(BounceRoutes.SelectServer);
|
||||
return;
|
||||
case ConnectionState.ServerUpdateNeeded:
|
||||
// Show update needed message and bounce to select server page
|
||||
try {
|
||||
await alert({
|
||||
text: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin'),
|
||||
html: globalize.translate('ServerUpdateNeeded', '<a href="https://github.com/jellyfin/jellyfin">https://github.com/jellyfin/jellyfin</a>')
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error('[ConnectionRequired] checking wizard status failed', ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Bounce to the correct page in the login flow
|
||||
bounce(firstConnection)
|
||||
.catch(err => {
|
||||
console.error('[ConnectionRequired] failed to bounce', err);
|
||||
});
|
||||
}, [bounce, navigate]);
|
||||
|
||||
const validateUserAccess = useCallback(async () => {
|
||||
const client = ServerConnections.currentApiClient();
|
||||
|
||||
// If this is a user route, ensure a user is logged in
|
||||
if ((isAdminRequired || isUserRequired) && !client?.isLoggedIn()) {
|
||||
try {
|
||||
console.warn('[ConnectionRequired] unauthenticated user attempted to access user route');
|
||||
bounce(await ServerConnections.connect())
|
||||
.catch(err => {
|
||||
console.error('[ConnectionRequired] failed to bounce', err);
|
||||
});
|
||||
} catch (ex) {
|
||||
console.warn('[ConnectionRequired] error bouncing from user route', ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is an admin route, ensure the user has access
|
||||
if (isAdminRequired) {
|
||||
try {
|
||||
const user = await client?.getCurrentUser();
|
||||
if (!user?.Policy?.IsAdministrator) {
|
||||
console.warn('[ConnectionRequired] normal user attempted to access admin route');
|
||||
bounce(await ServerConnections.connect())
|
||||
.catch(err => {
|
||||
console.error('[ConnectionRequired] failed to bounce', err);
|
||||
});
|
||||
} catch (ex) {
|
||||
console.warn('[ConnectionRequired] failed to show alert', ex);
|
||||
}
|
||||
console.debug('[ConnectionRequired] server update required, redirecting to select server page');
|
||||
navigate(BounceRoutes.SelectServer);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn('[ConnectionRequired] unhandled connection state', connectionResponse.State);
|
||||
};
|
||||
|
||||
const validateConnection = async () => {
|
||||
// Check connection status on initial page load
|
||||
const firstConnection = appRouter.firstConnectionResult;
|
||||
appRouter.firstConnectionResult = null;
|
||||
|
||||
if (firstConnection && firstConnection.State !== ConnectionState.SignedIn) {
|
||||
if (firstConnection.State === ConnectionState.ServerSignIn) {
|
||||
// Verify the wizard is complete
|
||||
try {
|
||||
const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`);
|
||||
if (!infoResponse.ok) {
|
||||
throw new Error('Public system info request failed');
|
||||
}
|
||||
const systemInfo = await infoResponse.json();
|
||||
if (!systemInfo?.StartupWizardCompleted) {
|
||||
// Update the current ApiClient
|
||||
// TODO: Is there a better place to handle this?
|
||||
ServerConnections.setLocalApiClient(firstConnection.ApiClient);
|
||||
// Bounce to the wizard
|
||||
console.info('[ConnectionRequired] startup wizard is not complete, redirecting there');
|
||||
navigate(BounceRoutes.StartWizard);
|
||||
return;
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error('[ConnectionRequired] checking wizard status failed', ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Bounce to the correct page in the login flow
|
||||
bounce(firstConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: appRouter will call appHost.exit() if navigating back when you are already at the default route.
|
||||
// This case will need to be handled elsewhere before appRouter can be killed.
|
||||
|
||||
const client = ServerConnections.currentApiClient();
|
||||
|
||||
// If this is a user route, ensure a user is logged in
|
||||
if ((isAdminRequired || isUserRequired) && !client?.isLoggedIn()) {
|
||||
try {
|
||||
console.warn('[ConnectionRequired] unauthenticated user attempted to access user route');
|
||||
bounce(await ServerConnections.connect());
|
||||
} catch (ex) {
|
||||
console.warn('[ConnectionRequired] error bouncing from user route', ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is an admin route, ensure the user has access
|
||||
if (isAdminRequired) {
|
||||
try {
|
||||
const user = await client.getCurrentUser();
|
||||
if (!user.Policy.IsAdministrator) {
|
||||
console.warn('[ConnectionRequired] normal user attempted to access admin route');
|
||||
bounce(await ServerConnections.connect());
|
||||
return;
|
||||
}
|
||||
} catch (ex) {
|
||||
console.warn('[ConnectionRequired] error bouncing from admin route', ex);
|
||||
return;
|
||||
}
|
||||
} catch (ex) {
|
||||
console.warn('[ConnectionRequired] error bouncing from admin route', ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
setIsLoading(false);
|
||||
}, [bounce, isAdminRequired, isUserRequired]);
|
||||
|
||||
validateConnection();
|
||||
}, [ isAdminRequired, isUserRequired, navigate ]);
|
||||
useEffect(() => {
|
||||
// TODO: appRouter will call appHost.exit() if navigating back when you are already at the default route.
|
||||
// This case will need to be handled elsewhere before appRouter can be killed.
|
||||
|
||||
// Check connection status on initial page load
|
||||
const firstConnection = appRouter.firstConnectionResult;
|
||||
appRouter.firstConnectionResult = null;
|
||||
|
||||
if (firstConnection && firstConnection.State !== ConnectionState.SignedIn) {
|
||||
handleIncompleteWizard(firstConnection)
|
||||
.catch(err => {
|
||||
console.error('[ConnectionRequired] failed to start wizard', err);
|
||||
});
|
||||
} else {
|
||||
validateUserAccess()
|
||||
.catch(err => {
|
||||
console.error('[ConnectionRequired] failed to validate user access', err);
|
||||
});
|
||||
}
|
||||
}, [handleIncompleteWizard, validateUserAccess]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
|
|
|
@ -86,6 +86,10 @@ class ServerConnections extends ConnectionManager {
|
|||
return this.localApiClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ApiClient that is currently connected.
|
||||
* @returns {ApiClient|undefined} apiClient
|
||||
*/
|
||||
currentApiClient() {
|
||||
let apiClient = this.getLocalApiClient();
|
||||
|
||||
|
|
62
src/components/ServerContentPage.tsx
Normal file
62
src/components/ServerContentPage.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React, { FunctionComponent, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import ServerConnections from './ServerConnections';
|
||||
import viewManager from './viewManager/viewManager';
|
||||
import globalize from '../scripts/globalize';
|
||||
import type { RestoreViewFailResponse } from '../types/viewManager';
|
||||
|
||||
interface ServerContentPageProps {
|
||||
view: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Page component that renders html content from a server request.
|
||||
* Uses the ViewManager to dynamically load and execute the page JS.
|
||||
*/
|
||||
const ServerContentPage: FunctionComponent<ServerContentPageProps> = ({ view }) => {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const loadPage = () => {
|
||||
const viewOptions = {
|
||||
url: location.pathname + location.search,
|
||||
state: location.state,
|
||||
autoFocus: false,
|
||||
options: {
|
||||
supportsThemeMedia: false,
|
||||
enableMediaControl: true
|
||||
}
|
||||
};
|
||||
|
||||
viewManager.tryRestoreView(viewOptions)
|
||||
.catch(async (result?: RestoreViewFailResponse) => {
|
||||
if (!result?.cancelled) {
|
||||
const apiClient = ServerConnections.currentApiClient();
|
||||
|
||||
// Fetch the view html from the server and translate it
|
||||
const viewHtml = await apiClient?.get(apiClient.getUrl(view + location.search))
|
||||
.then((html: string) => globalize.translateHtml(html));
|
||||
|
||||
viewManager.loadView({
|
||||
...viewOptions,
|
||||
view: viewHtml
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
loadPage();
|
||||
},
|
||||
// location.state is NOT included as a dependency here since dialogs will update state while the current view stays the same
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
view,
|
||||
location.pathname,
|
||||
location.search
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default ServerContentPage;
|
|
@ -1,6 +1,3 @@
|
|||
|
||||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module for controlling user parental control from.
|
||||
* @module components/accessSchedule/accessSchedule
|
||||
|
@ -14,84 +11,82 @@ import '../../elements/emby-button/paper-icon-button-light';
|
|||
import '../formdialog.scss';
|
||||
import template from './accessSchedule.template.html';
|
||||
|
||||
function getDisplayTime(hours) {
|
||||
let minutes = 0;
|
||||
const pct = hours % 1;
|
||||
function getDisplayTime(hours) {
|
||||
let minutes = 0;
|
||||
const pct = hours % 1;
|
||||
|
||||
if (pct) {
|
||||
minutes = parseInt(60 * pct);
|
||||
}
|
||||
|
||||
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
|
||||
if (pct) {
|
||||
minutes = parseInt(60 * pct, 10);
|
||||
}
|
||||
|
||||
function populateHours(context) {
|
||||
let html = '';
|
||||
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
|
||||
}
|
||||
|
||||
for (let i = 0; i < 24; i++) {
|
||||
html += `<option value="${i}">${getDisplayTime(i)}</option>`;
|
||||
}
|
||||
function populateHours(context) {
|
||||
let html = '';
|
||||
|
||||
html += `<option value="24">${getDisplayTime(0)}</option>`;
|
||||
context.querySelector('#selectStart').innerHTML = html;
|
||||
context.querySelector('#selectEnd').innerHTML = html;
|
||||
for (let i = 0; i < 24; i += 0.5) {
|
||||
html += `<option value="${i}">${getDisplayTime(i)}</option>`;
|
||||
}
|
||||
|
||||
function loadSchedule(context, {DayOfWeek, StartHour, EndHour}) {
|
||||
context.querySelector('#selectDay').value = DayOfWeek || 'Sunday';
|
||||
context.querySelector('#selectStart').value = StartHour || 0;
|
||||
context.querySelector('#selectEnd').value = EndHour || 0;
|
||||
html += `<option value="24">${getDisplayTime(0)}</option>`;
|
||||
context.querySelector('#selectStart').innerHTML = html;
|
||||
context.querySelector('#selectEnd').innerHTML = html;
|
||||
}
|
||||
|
||||
function loadSchedule(context, { DayOfWeek, StartHour, EndHour }) {
|
||||
context.querySelector('#selectDay').value = DayOfWeek || 'Sunday';
|
||||
context.querySelector('#selectStart').value = StartHour || 0;
|
||||
context.querySelector('#selectEnd').value = EndHour || 0;
|
||||
}
|
||||
|
||||
function submitSchedule(context, options) {
|
||||
const updatedSchedule = {
|
||||
DayOfWeek: context.querySelector('#selectDay').value,
|
||||
StartHour: context.querySelector('#selectStart').value,
|
||||
EndHour: context.querySelector('#selectEnd').value
|
||||
};
|
||||
|
||||
if (parseFloat(updatedSchedule.StartHour) >= parseFloat(updatedSchedule.EndHour)) {
|
||||
alert(globalize.translate('ErrorStartHourGreaterThanEnd'));
|
||||
return;
|
||||
}
|
||||
|
||||
function submitSchedule(context, options) {
|
||||
const updatedSchedule = {
|
||||
DayOfWeek: context.querySelector('#selectDay').value,
|
||||
StartHour: context.querySelector('#selectStart').value,
|
||||
EndHour: context.querySelector('#selectEnd').value
|
||||
};
|
||||
context.submitted = true;
|
||||
options.schedule = Object.assign(options.schedule, updatedSchedule);
|
||||
dialogHelper.close(context);
|
||||
}
|
||||
|
||||
if (parseFloat(updatedSchedule.StartHour) >= parseFloat(updatedSchedule.EndHour)) {
|
||||
alert(globalize.translate('ErrorStartHourGreaterThanEnd'));
|
||||
return;
|
||||
}
|
||||
|
||||
context.submitted = true;
|
||||
options.schedule = Object.assign(options.schedule, updatedSchedule);
|
||||
dialogHelper.close(context);
|
||||
}
|
||||
|
||||
export function show(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dlg = dialogHelper.createDialog({
|
||||
removeOnClose: true,
|
||||
size: 'small'
|
||||
});
|
||||
dlg.classList.add('formDialog');
|
||||
let html = '';
|
||||
html += globalize.translateHtml(template);
|
||||
dlg.innerHTML = html;
|
||||
populateHours(dlg);
|
||||
loadSchedule(dlg, options.schedule);
|
||||
dialogHelper.open(dlg);
|
||||
dlg.addEventListener('close', () => {
|
||||
if (dlg.submitted) {
|
||||
resolve(options.schedule);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
dlg.querySelector('form').addEventListener('submit', event => {
|
||||
submitSchedule(dlg, options);
|
||||
event.preventDefault();
|
||||
return false;
|
||||
});
|
||||
export function show(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dlg = dialogHelper.createDialog({
|
||||
removeOnClose: true,
|
||||
size: 'small'
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
dlg.classList.add('formDialog');
|
||||
let html = '';
|
||||
html += globalize.translateHtml(template);
|
||||
dlg.innerHTML = html;
|
||||
populateHours(dlg);
|
||||
loadSchedule(dlg, options.schedule);
|
||||
dialogHelper.open(dlg);
|
||||
dlg.addEventListener('close', () => {
|
||||
if (dlg.submitted) {
|
||||
resolve(options.schedule);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
dlg.querySelector('form').addEventListener('submit', event => {
|
||||
submitSchedule(dlg, options);
|
||||
event.preventDefault();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
show: show
|
||||
|
|
|
@ -6,7 +6,7 @@ import dom from '../../scripts/dom';
|
|||
import '../../elements/emby-button/emby-button';
|
||||
import './actionSheet.scss';
|
||||
import 'material-design-icons-iconfont';
|
||||
import '../../assets/css/scrollstyles.scss';
|
||||
import '../../styles/scrollstyles.scss';
|
||||
import '../../components/listview/listview.scss';
|
||||
|
||||
function getOffsets(elems) {
|
||||
|
|
|
@ -11,130 +11,128 @@ import alert from './alert';
|
|||
import { getLocale } from '../utils/dateFnsLocale.ts';
|
||||
import { toBoolean } from '../utils/string.ts';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function getEntryHtml(entry, apiClient) {
|
||||
let html = '';
|
||||
html += '<div class="listItem listItem-border">';
|
||||
let color = '#00a4dc';
|
||||
let icon = 'notifications';
|
||||
|
||||
function getEntryHtml(entry, apiClient) {
|
||||
let html = '';
|
||||
html += '<div class="listItem listItem-border">';
|
||||
let color = '#00a4dc';
|
||||
let icon = 'notifications';
|
||||
if (entry.Severity == 'Error' || entry.Severity == 'Fatal' || entry.Severity == 'Warn') {
|
||||
color = '#cc0000';
|
||||
icon = 'notification_important';
|
||||
}
|
||||
|
||||
if (entry.Severity == 'Error' || entry.Severity == 'Fatal' || entry.Severity == 'Warn') {
|
||||
color = '#cc0000';
|
||||
icon = 'notification_important';
|
||||
}
|
||||
if (entry.UserId && entry.UserPrimaryImageTag) {
|
||||
html += '<span class="listItemIcon material-icons dvr" aria-hidden="true" style="width:2em!important;height:2em!important;padding:0;color:transparent;background-color:' + color + ";background-image:url('" + apiClient.getUserImageUrl(entry.UserId, {
|
||||
type: 'Primary',
|
||||
tag: entry.UserPrimaryImageTag
|
||||
}) + "');background-repeat:no-repeat;background-position:center center;background-size: cover;\"></span>";
|
||||
} else {
|
||||
html += '<span class="listItemIcon material-icons ' + icon + '" aria-hidden="true" style="background-color:' + color + '"></span>';
|
||||
}
|
||||
|
||||
if (entry.UserId && entry.UserPrimaryImageTag) {
|
||||
html += '<span class="listItemIcon material-icons dvr" aria-hidden="true" style="width:2em!important;height:2em!important;padding:0;color:transparent;background-color:' + color + ";background-image:url('" + apiClient.getUserImageUrl(entry.UserId, {
|
||||
type: 'Primary',
|
||||
tag: entry.UserPrimaryImageTag
|
||||
}) + "');background-repeat:no-repeat;background-position:center center;background-size: cover;\"></span>";
|
||||
} else {
|
||||
html += '<span class="listItemIcon material-icons ' + icon + '" aria-hidden="true" style="background-color:' + color + '"></span>';
|
||||
}
|
||||
html += '<div class="listItemBody three-line">';
|
||||
html += '<div class="listItemBodyText">';
|
||||
html += escapeHtml(entry.Name);
|
||||
html += '</div>';
|
||||
html += '<div class="listItemBodyText secondary">';
|
||||
html += formatRelative(Date.parse(entry.Date), Date.now(), { locale: getLocale() });
|
||||
html += '</div>';
|
||||
html += '<div class="listItemBodyText secondary listItemBodyText-nowrap">';
|
||||
html += escapeHtml(entry.ShortOverview || '');
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="listItemBody three-line">';
|
||||
html += '<div class="listItemBodyText">';
|
||||
html += escapeHtml(entry.Name);
|
||||
html += '</div>';
|
||||
html += '<div class="listItemBodyText secondary">';
|
||||
html += formatRelative(Date.parse(entry.Date), Date.now(), { locale: getLocale() });
|
||||
html += '</div>';
|
||||
html += '<div class="listItemBodyText secondary listItemBodyText-nowrap">';
|
||||
html += escapeHtml(entry.ShortOverview || '');
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
if (entry.Overview) {
|
||||
html += `<button type="button" is="paper-icon-button-light" class="btnEntryInfo" data-id="${entry.Id}" title="${globalize.translate('Info')}">
|
||||
if (entry.Overview) {
|
||||
html += `<button type="button" is="paper-icon-button-light" class="btnEntryInfo" data-id="${entry.Id}" title="${globalize.translate('Info')}">
|
||||
<span class="material-icons info" aria-hidden="true"></span>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderList(elem, apiClient, result) {
|
||||
elem.innerHTML = result.Items.map(function (i) {
|
||||
return getEntryHtml(i, apiClient);
|
||||
}).join('');
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderList(elem, apiClient, result) {
|
||||
elem.innerHTML = result.Items.map(function (i) {
|
||||
return getEntryHtml(i, apiClient);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function reloadData(instance, elem, apiClient, startIndex, limit) {
|
||||
if (startIndex == null) {
|
||||
startIndex = parseInt(elem.getAttribute('data-activitystartindex') || '0', 10);
|
||||
}
|
||||
|
||||
function reloadData(instance, elem, apiClient, startIndex, limit) {
|
||||
if (startIndex == null) {
|
||||
startIndex = parseInt(elem.getAttribute('data-activitystartindex') || '0');
|
||||
}
|
||||
limit = limit || parseInt(elem.getAttribute('data-activitylimit') || '7', 10);
|
||||
const minDate = new Date();
|
||||
const hasUserId = toBoolean(elem.getAttribute('data-useractivity'), true);
|
||||
|
||||
limit = limit || parseInt(elem.getAttribute('data-activitylimit') || '7');
|
||||
const minDate = new Date();
|
||||
const hasUserId = toBoolean(elem.getAttribute('data-useractivity'), true);
|
||||
|
||||
// TODO: Use date-fns
|
||||
if (hasUserId) {
|
||||
minDate.setTime(minDate.getTime() - 24 * 60 * 60 * 1000); // one day back
|
||||
} else {
|
||||
minDate.setTime(minDate.getTime() - 7 * 24 * 60 * 60 * 1000); // one week back
|
||||
}
|
||||
|
||||
ApiClient.getJSON(ApiClient.getUrl('System/ActivityLog/Entries', {
|
||||
startIndex: startIndex,
|
||||
limit: limit,
|
||||
minDate: minDate.toISOString(),
|
||||
hasUserId: hasUserId
|
||||
})).then(function (result) {
|
||||
elem.setAttribute('data-activitystartindex', startIndex);
|
||||
elem.setAttribute('data-activitylimit', limit);
|
||||
if (!startIndex) {
|
||||
const activityContainer = dom.parentWithClass(elem, 'activityContainer');
|
||||
|
||||
if (activityContainer) {
|
||||
if (result.Items.length) {
|
||||
activityContainer.classList.remove('hide');
|
||||
} else {
|
||||
activityContainer.classList.add('hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instance.items = result.Items;
|
||||
renderList(elem, apiClient, result);
|
||||
});
|
||||
// TODO: Use date-fns
|
||||
if (hasUserId) {
|
||||
minDate.setTime(minDate.getTime() - 24 * 60 * 60 * 1000); // one day back
|
||||
} else {
|
||||
minDate.setTime(minDate.getTime() - 7 * 24 * 60 * 60 * 1000); // one week back
|
||||
}
|
||||
|
||||
function onActivityLogUpdate(e, apiClient) {
|
||||
const options = this.options;
|
||||
ApiClient.getJSON(ApiClient.getUrl('System/ActivityLog/Entries', {
|
||||
startIndex: startIndex,
|
||||
limit: limit,
|
||||
minDate: minDate.toISOString(),
|
||||
hasUserId: hasUserId
|
||||
})).then(function (result) {
|
||||
elem.setAttribute('data-activitystartindex', startIndex);
|
||||
elem.setAttribute('data-activitylimit', limit);
|
||||
if (!startIndex) {
|
||||
const activityContainer = dom.parentWithClass(elem, 'activityContainer');
|
||||
|
||||
if (options && options.serverId === apiClient.serverId()) {
|
||||
reloadData(this, options.element, apiClient);
|
||||
}
|
||||
}
|
||||
|
||||
function onListClick(e) {
|
||||
const btnEntryInfo = dom.parentWithClass(e.target, 'btnEntryInfo');
|
||||
|
||||
if (btnEntryInfo) {
|
||||
const id = btnEntryInfo.getAttribute('data-id');
|
||||
const items = this.items;
|
||||
|
||||
if (items) {
|
||||
const item = items.filter(function (i) {
|
||||
return i.Id.toString() === id;
|
||||
})[0];
|
||||
|
||||
if (item) {
|
||||
showItemOverview(item);
|
||||
if (activityContainer) {
|
||||
if (result.Items.length) {
|
||||
activityContainer.classList.remove('hide');
|
||||
} else {
|
||||
activityContainer.classList.add('hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showItemOverview(item) {
|
||||
alert({
|
||||
text: item.Overview
|
||||
});
|
||||
instance.items = result.Items;
|
||||
renderList(elem, apiClient, result);
|
||||
});
|
||||
}
|
||||
|
||||
function onActivityLogUpdate(e, apiClient) {
|
||||
const options = this.options;
|
||||
|
||||
if (options && options.serverId === apiClient.serverId()) {
|
||||
reloadData(this, options.element, apiClient);
|
||||
}
|
||||
}
|
||||
|
||||
function onListClick(e) {
|
||||
const btnEntryInfo = dom.parentWithClass(e.target, 'btnEntryInfo');
|
||||
|
||||
if (btnEntryInfo) {
|
||||
const id = btnEntryInfo.getAttribute('data-id');
|
||||
const items = this.items;
|
||||
|
||||
if (items) {
|
||||
const item = items.filter(function (i) {
|
||||
return i.Id.toString() === id;
|
||||
})[0];
|
||||
|
||||
if (item) {
|
||||
showItemOverview(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showItemOverview(item) {
|
||||
alert({
|
||||
text: item.Overview
|
||||
});
|
||||
}
|
||||
|
||||
class ActivityLog {
|
||||
constructor(options) {
|
||||
|
@ -169,5 +167,3 @@ class ActivityLog {
|
|||
}
|
||||
|
||||
export default ActivityLog;
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
|
|
@ -1,47 +1,43 @@
|
|||
import { appRouter } from './appRouter';
|
||||
import { appRouter } from './router/appRouter';
|
||||
import browser from '../scripts/browser';
|
||||
import dialog from './dialog/dialog';
|
||||
import globalize from '../scripts/globalize';
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
function useNativeAlert() {
|
||||
// webOS seems to block modals
|
||||
// Tizen 2.x seems to block modals
|
||||
return !browser.web0s
|
||||
function useNativeAlert() {
|
||||
// webOS seems to block modals
|
||||
// Tizen 2.x seems to block modals
|
||||
return !browser.web0s
|
||||
&& !(browser.tizenVersion && browser.tizenVersion < 3)
|
||||
&& browser.tv
|
||||
&& window.alert;
|
||||
}
|
||||
|
||||
export default async function (text, title) {
|
||||
let options;
|
||||
if (typeof text === 'string') {
|
||||
options = {
|
||||
title: title,
|
||||
text: text
|
||||
};
|
||||
} else {
|
||||
options = text;
|
||||
}
|
||||
|
||||
export default async function (text, title) {
|
||||
let options;
|
||||
if (typeof text === 'string') {
|
||||
options = {
|
||||
title: title,
|
||||
text: text
|
||||
};
|
||||
} else {
|
||||
options = text;
|
||||
}
|
||||
await appRouter.ready();
|
||||
|
||||
await appRouter.ready();
|
||||
if (useNativeAlert()) {
|
||||
alert((options.text || '').replaceAll('<br/>', '\n'));
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
const items = [];
|
||||
|
||||
if (useNativeAlert()) {
|
||||
alert((options.text || '').replaceAll('<br/>', '\n'));
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
const items = [];
|
||||
items.push({
|
||||
name: globalize.translate('ButtonGotIt'),
|
||||
id: 'ok',
|
||||
type: 'submit'
|
||||
});
|
||||
|
||||
items.push({
|
||||
name: globalize.translate('ButtonGotIt'),
|
||||
id: 'ok',
|
||||
type: 'submit'
|
||||
});
|
||||
|
||||
options.buttons = items;
|
||||
return dialog.show(options);
|
||||
}
|
||||
options.buttons = items;
|
||||
return dialog.show(options);
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module alphaPicker.
|
||||
* @module components/alphaPicker/alphaPicker
|
||||
|
@ -13,312 +11,311 @@ import './style.scss';
|
|||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import 'material-design-icons-iconfont';
|
||||
|
||||
const selectedButtonClass = 'alphaPickerButton-selected';
|
||||
const selectedButtonClass = 'alphaPickerButton-selected';
|
||||
|
||||
function focus() {
|
||||
const scope = this;
|
||||
const selected = scope.querySelector(`.${selectedButtonClass}`);
|
||||
function focus() {
|
||||
const scope = this;
|
||||
const selected = scope.querySelector(`.${selectedButtonClass}`);
|
||||
|
||||
if (selected) {
|
||||
focusManager.focus(selected);
|
||||
} else {
|
||||
focusManager.autoFocus(scope, true);
|
||||
}
|
||||
if (selected) {
|
||||
focusManager.focus(selected);
|
||||
} else {
|
||||
focusManager.autoFocus(scope, true);
|
||||
}
|
||||
}
|
||||
|
||||
function getAlphaPickerButtonClassName(vertical) {
|
||||
let alphaPickerButtonClassName = 'alphaPickerButton';
|
||||
|
||||
if (layoutManager.tv) {
|
||||
alphaPickerButtonClassName += ' alphaPickerButton-tv';
|
||||
}
|
||||
|
||||
function getAlphaPickerButtonClassName(vertical) {
|
||||
let alphaPickerButtonClassName = 'alphaPickerButton';
|
||||
|
||||
if (layoutManager.tv) {
|
||||
alphaPickerButtonClassName += ' alphaPickerButton-tv';
|
||||
}
|
||||
|
||||
if (vertical) {
|
||||
alphaPickerButtonClassName += ' alphaPickerButton-vertical';
|
||||
}
|
||||
|
||||
return alphaPickerButtonClassName;
|
||||
if (vertical) {
|
||||
alphaPickerButtonClassName += ' alphaPickerButton-vertical';
|
||||
}
|
||||
|
||||
function getLetterButton(l, vertical) {
|
||||
return `<button data-value="${l}" class="${getAlphaPickerButtonClassName(vertical)}">${l}</button>`;
|
||||
return alphaPickerButtonClassName;
|
||||
}
|
||||
|
||||
function getLetterButton(l, vertical) {
|
||||
return `<button data-value="${l}" class="${getAlphaPickerButtonClassName(vertical)}">${l}</button>`;
|
||||
}
|
||||
|
||||
function mapLetters(letters, vertical) {
|
||||
return letters.map(l => {
|
||||
return getLetterButton(l, vertical);
|
||||
});
|
||||
}
|
||||
|
||||
function render(element, options) {
|
||||
element.classList.add('alphaPicker');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
element.classList.add('alphaPicker-tv');
|
||||
}
|
||||
|
||||
function mapLetters(letters, vertical) {
|
||||
return letters.map(l => {
|
||||
return getLetterButton(l, vertical);
|
||||
});
|
||||
const vertical = element.classList.contains('alphaPicker-vertical');
|
||||
|
||||
if (!vertical) {
|
||||
element.classList.add('focuscontainer-x');
|
||||
}
|
||||
|
||||
function render(element, options) {
|
||||
element.classList.add('alphaPicker');
|
||||
let html = '';
|
||||
let letters;
|
||||
|
||||
if (layoutManager.tv) {
|
||||
element.classList.add('alphaPicker-tv');
|
||||
}
|
||||
const alphaPickerButtonClassName = getAlphaPickerButtonClassName(vertical);
|
||||
|
||||
const vertical = element.classList.contains('alphaPicker-vertical');
|
||||
let rowClassName = 'alphaPickerRow';
|
||||
|
||||
if (!vertical) {
|
||||
element.classList.add('focuscontainer-x');
|
||||
}
|
||||
if (vertical) {
|
||||
rowClassName += ' alphaPickerRow-vertical';
|
||||
}
|
||||
|
||||
let html = '';
|
||||
let letters;
|
||||
|
||||
const alphaPickerButtonClassName = getAlphaPickerButtonClassName(vertical);
|
||||
|
||||
let rowClassName = 'alphaPickerRow';
|
||||
|
||||
if (vertical) {
|
||||
rowClassName += ' alphaPickerRow-vertical';
|
||||
}
|
||||
|
||||
html += `<div class="${rowClassName}">`;
|
||||
if (options.mode === 'keyboard') {
|
||||
html += `<button data-value=" " is="paper-icon-button-light" class="${alphaPickerButtonClassName}" aria-label="${globalize.translate('ButtonSpace')}"><span class="material-icons alphaPickerButtonIcon space_bar" aria-hidden="true"></span></button>`;
|
||||
} else {
|
||||
letters = ['#'];
|
||||
html += mapLetters(letters, vertical).join('');
|
||||
}
|
||||
|
||||
letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
|
||||
html += `<div class="${rowClassName}">`;
|
||||
if (options.mode === 'keyboard') {
|
||||
html += `<button data-value=" " is="paper-icon-button-light" class="${alphaPickerButtonClassName}" aria-label="${globalize.translate('ButtonSpace')}"><span class="material-icons alphaPickerButtonIcon space_bar" aria-hidden="true"></span></button>`;
|
||||
} else {
|
||||
letters = ['#'];
|
||||
html += mapLetters(letters, vertical).join('');
|
||||
|
||||
if (options.mode === 'keyboard') {
|
||||
html += `<button data-value="backspace" is="paper-icon-button-light" class="${alphaPickerButtonClassName}" aria-label="${globalize.translate('ButtonBackspace')}"><span class="material-icons alphaPickerButtonIcon backspace" aria-hidden="true"></span></button>`;
|
||||
html += '</div>';
|
||||
|
||||
letters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
html += `<div class="${rowClassName}">`;
|
||||
html += '<br/>';
|
||||
html += mapLetters(letters, vertical).join('');
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
element.innerHTML = html;
|
||||
|
||||
element.classList.add('focusable');
|
||||
element.focus = focus;
|
||||
}
|
||||
|
||||
export class AlphaPicker {
|
||||
constructor(options) {
|
||||
const self = this;
|
||||
letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
|
||||
html += mapLetters(letters, vertical).join('');
|
||||
|
||||
this.options = options;
|
||||
if (options.mode === 'keyboard') {
|
||||
html += `<button data-value="backspace" is="paper-icon-button-light" class="${alphaPickerButtonClassName}" aria-label="${globalize.translate('ButtonBackspace')}"><span class="material-icons alphaPickerButtonIcon backspace" aria-hidden="true"></span></button>`;
|
||||
html += '</div>';
|
||||
|
||||
const element = options.element;
|
||||
const itemsContainer = options.itemsContainer;
|
||||
const itemClass = options.itemClass;
|
||||
letters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
html += `<div class="${rowClassName}">`;
|
||||
html += '<br/>';
|
||||
html += mapLetters(letters, vertical).join('');
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
let itemFocusValue;
|
||||
let itemFocusTimeout;
|
||||
element.innerHTML = html;
|
||||
|
||||
function onItemFocusTimeout() {
|
||||
itemFocusTimeout = null;
|
||||
self.value(itemFocusValue);
|
||||
}
|
||||
element.classList.add('focusable');
|
||||
element.focus = focus;
|
||||
}
|
||||
|
||||
let alphaFocusedElement;
|
||||
let alphaFocusTimeout;
|
||||
export class AlphaPicker {
|
||||
constructor(options) {
|
||||
const self = this;
|
||||
|
||||
function onAlphaFocusTimeout() {
|
||||
alphaFocusTimeout = null;
|
||||
this.options = options;
|
||||
|
||||
if (document.activeElement === alphaFocusedElement) {
|
||||
const value = alphaFocusedElement.getAttribute('data-value');
|
||||
self.value(value, true);
|
||||
}
|
||||
}
|
||||
const element = options.element;
|
||||
const itemsContainer = options.itemsContainer;
|
||||
const itemClass = options.itemClass;
|
||||
|
||||
function onAlphaPickerInKeyboardModeClick(e) {
|
||||
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
|
||||
let itemFocusValue;
|
||||
let itemFocusTimeout;
|
||||
|
||||
if (alphaPickerButton) {
|
||||
const value = alphaPickerButton.getAttribute('data-value');
|
||||
|
||||
element.dispatchEvent(new CustomEvent('alphavalueclicked', {
|
||||
cancelable: false,
|
||||
detail: {
|
||||
value
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function onAlphaPickerClick(e) {
|
||||
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
|
||||
|
||||
if (alphaPickerButton) {
|
||||
const value = alphaPickerButton.getAttribute('data-value');
|
||||
if ((this._currentValue || '').toUpperCase() === value.toUpperCase()) {
|
||||
this.value(null, true);
|
||||
} else {
|
||||
this.value(value, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onAlphaPickerFocusIn(e) {
|
||||
if (alphaFocusTimeout) {
|
||||
clearTimeout(alphaFocusTimeout);
|
||||
alphaFocusTimeout = null;
|
||||
}
|
||||
|
||||
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
|
||||
|
||||
if (alphaPickerButton) {
|
||||
alphaFocusedElement = alphaPickerButton;
|
||||
alphaFocusTimeout = setTimeout(onAlphaFocusTimeout, 600);
|
||||
}
|
||||
}
|
||||
|
||||
function onItemsFocusIn(e) {
|
||||
const item = dom.parentWithClass(e.target, itemClass);
|
||||
|
||||
if (item) {
|
||||
const prefix = item.getAttribute('data-prefix');
|
||||
if (prefix && prefix.length) {
|
||||
itemFocusValue = prefix[0];
|
||||
if (itemFocusTimeout) {
|
||||
clearTimeout(itemFocusTimeout);
|
||||
}
|
||||
itemFocusTimeout = setTimeout(onItemFocusTimeout, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.enabled = function (enabled) {
|
||||
if (enabled) {
|
||||
if (itemsContainer) {
|
||||
itemsContainer.addEventListener('focus', onItemsFocusIn, true);
|
||||
}
|
||||
|
||||
if (options.mode === 'keyboard') {
|
||||
element.addEventListener('click', onAlphaPickerInKeyboardModeClick);
|
||||
}
|
||||
|
||||
if (options.valueChangeEvent !== 'click') {
|
||||
element.addEventListener('focus', onAlphaPickerFocusIn, true);
|
||||
} else {
|
||||
element.addEventListener('click', onAlphaPickerClick.bind(this));
|
||||
}
|
||||
} else {
|
||||
if (itemsContainer) {
|
||||
itemsContainer.removeEventListener('focus', onItemsFocusIn, true);
|
||||
}
|
||||
|
||||
element.removeEventListener('click', onAlphaPickerInKeyboardModeClick);
|
||||
element.removeEventListener('focus', onAlphaPickerFocusIn, true);
|
||||
element.removeEventListener('click', onAlphaPickerClick.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
render(element, options);
|
||||
|
||||
this.enabled(true);
|
||||
this.visible(true);
|
||||
function onItemFocusTimeout() {
|
||||
itemFocusTimeout = null;
|
||||
self.value(itemFocusValue);
|
||||
}
|
||||
|
||||
value(value, applyValue) {
|
||||
const element = this.options.element;
|
||||
let btn;
|
||||
let selected;
|
||||
let alphaFocusedElement;
|
||||
let alphaFocusTimeout;
|
||||
|
||||
if (value !== undefined) {
|
||||
if (value != null) {
|
||||
value = value.toUpperCase();
|
||||
this._currentValue = value;
|
||||
function onAlphaFocusTimeout() {
|
||||
alphaFocusTimeout = null;
|
||||
|
||||
if (this.options.mode !== 'keyboard') {
|
||||
selected = element.querySelector(`.${selectedButtonClass}`);
|
||||
|
||||
try {
|
||||
btn = element.querySelector(`.alphaPickerButton[data-value='${value}']`);
|
||||
} catch (err) {
|
||||
console.error('error in querySelector:', err);
|
||||
}
|
||||
|
||||
if (btn && btn !== selected) {
|
||||
btn.classList.add(selectedButtonClass);
|
||||
}
|
||||
if (selected && selected !== btn) {
|
||||
selected.classList.remove(selectedButtonClass);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._currentValue = value;
|
||||
|
||||
selected = element.querySelector(`.${selectedButtonClass}`);
|
||||
if (selected) {
|
||||
selected.classList.remove(selectedButtonClass);
|
||||
}
|
||||
}
|
||||
if (document.activeElement === alphaFocusedElement) {
|
||||
const value = alphaFocusedElement.getAttribute('data-value');
|
||||
self.value(value, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (applyValue) {
|
||||
element.dispatchEvent(new CustomEvent('alphavaluechanged', {
|
||||
function onAlphaPickerInKeyboardModeClick(e) {
|
||||
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
|
||||
|
||||
if (alphaPickerButton) {
|
||||
const value = alphaPickerButton.getAttribute('data-value');
|
||||
|
||||
element.dispatchEvent(new CustomEvent('alphavalueclicked', {
|
||||
cancelable: false,
|
||||
detail: {
|
||||
value
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return this._currentValue;
|
||||
}
|
||||
|
||||
on(name, fn) {
|
||||
const element = this.options.element;
|
||||
element.addEventListener(name, fn);
|
||||
function onAlphaPickerClick(e) {
|
||||
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
|
||||
|
||||
if (alphaPickerButton) {
|
||||
const value = alphaPickerButton.getAttribute('data-value');
|
||||
if ((this._currentValue || '').toUpperCase() === value.toUpperCase()) {
|
||||
this.value(null, true);
|
||||
} else {
|
||||
this.value(value, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
off(name, fn) {
|
||||
const element = this.options.element;
|
||||
element.removeEventListener(name, fn);
|
||||
function onAlphaPickerFocusIn(e) {
|
||||
if (alphaFocusTimeout) {
|
||||
clearTimeout(alphaFocusTimeout);
|
||||
alphaFocusTimeout = null;
|
||||
}
|
||||
|
||||
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
|
||||
|
||||
if (alphaPickerButton) {
|
||||
alphaFocusedElement = alphaPickerButton;
|
||||
alphaFocusTimeout = setTimeout(onAlphaFocusTimeout, 600);
|
||||
}
|
||||
}
|
||||
|
||||
updateControls(query) {
|
||||
if (query.NameLessThan) {
|
||||
this.value('#');
|
||||
function onItemsFocusIn(e) {
|
||||
const item = dom.parentWithClass(e.target, itemClass);
|
||||
|
||||
if (item) {
|
||||
const prefix = item.getAttribute('data-prefix');
|
||||
if (prefix?.length) {
|
||||
itemFocusValue = prefix[0];
|
||||
if (itemFocusTimeout) {
|
||||
clearTimeout(itemFocusTimeout);
|
||||
}
|
||||
itemFocusTimeout = setTimeout(onItemFocusTimeout, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.enabled = function (enabled) {
|
||||
if (enabled) {
|
||||
if (itemsContainer) {
|
||||
itemsContainer.addEventListener('focus', onItemsFocusIn, true);
|
||||
}
|
||||
|
||||
if (options.mode === 'keyboard') {
|
||||
element.addEventListener('click', onAlphaPickerInKeyboardModeClick);
|
||||
}
|
||||
|
||||
if (options.valueChangeEvent !== 'click') {
|
||||
element.addEventListener('focus', onAlphaPickerFocusIn, true);
|
||||
} else {
|
||||
element.addEventListener('click', onAlphaPickerClick.bind(this));
|
||||
}
|
||||
} else {
|
||||
this.value(query.NameStartsWith);
|
||||
if (itemsContainer) {
|
||||
itemsContainer.removeEventListener('focus', onItemsFocusIn, true);
|
||||
}
|
||||
|
||||
element.removeEventListener('click', onAlphaPickerInKeyboardModeClick);
|
||||
element.removeEventListener('focus', onAlphaPickerFocusIn, true);
|
||||
element.removeEventListener('click', onAlphaPickerClick.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
this.visible(query.SortBy.indexOf('SortName') !== -1);
|
||||
}
|
||||
render(element, options);
|
||||
|
||||
visible(visible) {
|
||||
const element = this.options.element;
|
||||
element.style.visibility = visible ? 'visible' : 'hidden';
|
||||
}
|
||||
|
||||
values() {
|
||||
const element = this.options.element;
|
||||
const elems = element.querySelectorAll('.alphaPickerButton');
|
||||
const values = [];
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
values.push(elems[i].getAttribute('data-value'));
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
focus() {
|
||||
const element = this.options.element;
|
||||
focusManager.autoFocus(element, true);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
const element = this.options.element;
|
||||
this.enabled(false);
|
||||
element.classList.remove('focuscontainer-x');
|
||||
this.options = null;
|
||||
}
|
||||
this.enabled(true);
|
||||
this.visible(true);
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
value(value, applyValue) {
|
||||
const element = this.options.element;
|
||||
let btn;
|
||||
let selected;
|
||||
|
||||
if (value !== undefined) {
|
||||
if (value != null) {
|
||||
value = value.toUpperCase();
|
||||
this._currentValue = value;
|
||||
|
||||
if (this.options.mode !== 'keyboard') {
|
||||
selected = element.querySelector(`.${selectedButtonClass}`);
|
||||
|
||||
try {
|
||||
btn = element.querySelector(`.alphaPickerButton[data-value='${value}']`);
|
||||
} catch (err) {
|
||||
console.error('error in querySelector:', err);
|
||||
}
|
||||
|
||||
if (btn && btn !== selected) {
|
||||
btn.classList.add(selectedButtonClass);
|
||||
}
|
||||
if (selected && selected !== btn) {
|
||||
selected.classList.remove(selectedButtonClass);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._currentValue = value;
|
||||
|
||||
selected = element.querySelector(`.${selectedButtonClass}`);
|
||||
if (selected) {
|
||||
selected.classList.remove(selectedButtonClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (applyValue) {
|
||||
element.dispatchEvent(new CustomEvent('alphavaluechanged', {
|
||||
cancelable: false,
|
||||
detail: {
|
||||
value
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return this._currentValue;
|
||||
}
|
||||
|
||||
on(name, fn) {
|
||||
const element = this.options.element;
|
||||
element.addEventListener(name, fn);
|
||||
}
|
||||
|
||||
off(name, fn) {
|
||||
const element = this.options.element;
|
||||
element.removeEventListener(name, fn);
|
||||
}
|
||||
|
||||
updateControls(query) {
|
||||
if (query.NameLessThan) {
|
||||
this.value('#');
|
||||
} else {
|
||||
this.value(query.NameStartsWith);
|
||||
}
|
||||
|
||||
this.visible(query.SortBy.indexOf('SortName') !== -1);
|
||||
}
|
||||
|
||||
visible(visible) {
|
||||
const element = this.options.element;
|
||||
element.style.visibility = visible ? 'visible' : 'hidden';
|
||||
}
|
||||
|
||||
values() {
|
||||
const element = this.options.element;
|
||||
const elems = element.querySelectorAll('.alphaPickerButton');
|
||||
const values = [];
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
values.push(elems[i].getAttribute('data-value'));
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
focus() {
|
||||
const element = this.options.element;
|
||||
focusManager.autoFocus(element, true);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
const element = this.options.element;
|
||||
this.enabled(false);
|
||||
element.classList.remove('focuscontainer-x');
|
||||
this.options = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default AlphaPicker;
|
||||
|
|
|
@ -309,8 +309,8 @@ function askForExit() {
|
|||
exitPromise = actionsheet.show({
|
||||
title: globalize.translate('MessageConfirmAppExit'),
|
||||
items: [
|
||||
{id: 'yes', name: globalize.translate('Yes')},
|
||||
{id: 'no', name: globalize.translate('No')}
|
||||
{ id: 'yes', name: globalize.translate('Yes') },
|
||||
{ id: 'no', name: globalize.translate('No') }
|
||||
]
|
||||
}).then(function (value) {
|
||||
if (value === 'yes') {
|
||||
|
@ -366,20 +366,20 @@ export const appHost = {
|
|||
};
|
||||
},
|
||||
deviceName: function () {
|
||||
return window.NativeShell?.AppHost?.deviceName
|
||||
? window.NativeShell.AppHost.deviceName() : getDeviceName();
|
||||
return window.NativeShell?.AppHost?.deviceName ?
|
||||
window.NativeShell.AppHost.deviceName() : getDeviceName();
|
||||
},
|
||||
deviceId: function () {
|
||||
return window.NativeShell?.AppHost?.deviceId
|
||||
? window.NativeShell.AppHost.deviceId() : getDeviceId();
|
||||
return window.NativeShell?.AppHost?.deviceId ?
|
||||
window.NativeShell.AppHost.deviceId() : getDeviceId();
|
||||
},
|
||||
appName: function () {
|
||||
return window.NativeShell?.AppHost?.appName
|
||||
? window.NativeShell.AppHost.appName() : appName;
|
||||
return window.NativeShell?.AppHost?.appName ?
|
||||
window.NativeShell.AppHost.appName() : appName;
|
||||
},
|
||||
appVersion: function () {
|
||||
return window.NativeShell?.AppHost?.appVersion
|
||||
? window.NativeShell.AppHost.appVersion() : Package.version;
|
||||
return window.NativeShell?.AppHost?.appVersion ?
|
||||
window.NativeShell.AppHost.appVersion() : Package.version;
|
||||
},
|
||||
getPushTokenInfo: function () {
|
||||
return {};
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module for performing auto-focus.
|
||||
* @module components/autoFocuser
|
||||
|
@ -8,93 +6,91 @@
|
|||
import focusManager from './focusManager';
|
||||
import layoutManager from './layoutManager';
|
||||
|
||||
/**
|
||||
/**
|
||||
* Previously selected element.
|
||||
*/
|
||||
let activeElement;
|
||||
let activeElement;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns _true_ if AutoFocuser is enabled.
|
||||
*/
|
||||
export function isEnabled() {
|
||||
return layoutManager.tv;
|
||||
}
|
||||
export function isEnabled() {
|
||||
return layoutManager.tv;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Start AutoFocuser.
|
||||
*/
|
||||
export function enable() {
|
||||
if (!isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('focusin', function (e) {
|
||||
activeElement = e.target;
|
||||
});
|
||||
|
||||
console.debug('AutoFocuser enabled');
|
||||
export function enable() {
|
||||
if (!isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
window.addEventListener('focusin', function (e) {
|
||||
activeElement = e.target;
|
||||
});
|
||||
|
||||
console.debug('AutoFocuser enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set focus on a suitable element, taking into account the previously selected.
|
||||
* @param {HTMLElement} [container] - Element to limit scope.
|
||||
* @returns {HTMLElement} Focused element.
|
||||
*/
|
||||
export function autoFocus(container) {
|
||||
if (!isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
container = container || document.body;
|
||||
|
||||
let candidates = [];
|
||||
|
||||
if (activeElement) {
|
||||
// These elements are recreated
|
||||
if (activeElement.classList.contains('btnPreviousPage')) {
|
||||
candidates.push(container.querySelector('.btnPreviousPage'));
|
||||
candidates.push(container.querySelector('.btnNextPage'));
|
||||
} else if (activeElement.classList.contains('btnNextPage')) {
|
||||
candidates.push(container.querySelector('.btnNextPage'));
|
||||
candidates.push(container.querySelector('.btnPreviousPage'));
|
||||
} else if (activeElement.classList.contains('btnSelectView')) {
|
||||
candidates.push(container.querySelector('.btnSelectView'));
|
||||
}
|
||||
|
||||
candidates.push(activeElement);
|
||||
}
|
||||
|
||||
candidates = candidates.concat(Array.from(container.querySelectorAll('.btnPlay')));
|
||||
|
||||
let focusedElement;
|
||||
|
||||
candidates.every(function (element) {
|
||||
if (focusManager.isCurrentlyFocusable(element)) {
|
||||
focusManager.focus(element);
|
||||
focusedElement = element;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!focusedElement) {
|
||||
// FIXME: Multiple itemsContainers
|
||||
const itemsContainer = container.querySelector('.itemsContainer');
|
||||
|
||||
if (itemsContainer) {
|
||||
focusedElement = focusManager.autoFocus(itemsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
if (!focusedElement) {
|
||||
focusedElement = focusManager.autoFocus(container);
|
||||
}
|
||||
|
||||
return focusedElement;
|
||||
export function autoFocus(container) {
|
||||
if (!isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
container = container || document.body;
|
||||
|
||||
let candidates = [];
|
||||
|
||||
if (activeElement) {
|
||||
// These elements are recreated
|
||||
if (activeElement.classList.contains('btnPreviousPage')) {
|
||||
candidates.push(container.querySelector('.btnPreviousPage'));
|
||||
candidates.push(container.querySelector('.btnNextPage'));
|
||||
} else if (activeElement.classList.contains('btnNextPage')) {
|
||||
candidates.push(container.querySelector('.btnNextPage'));
|
||||
candidates.push(container.querySelector('.btnPreviousPage'));
|
||||
} else if (activeElement.classList.contains('btnSelectView')) {
|
||||
candidates.push(container.querySelector('.btnSelectView'));
|
||||
}
|
||||
|
||||
candidates.push(activeElement);
|
||||
}
|
||||
|
||||
candidates = candidates.concat(Array.from(container.querySelectorAll('.btnPlay')));
|
||||
|
||||
let focusedElement;
|
||||
|
||||
candidates.every(function (element) {
|
||||
if (focusManager.isCurrentlyFocusable(element)) {
|
||||
focusManager.focus(element);
|
||||
focusedElement = element;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!focusedElement) {
|
||||
// FIXME: Multiple itemsContainers
|
||||
const itemsContainer = container.querySelector('.itemsContainer');
|
||||
|
||||
if (itemsContainer) {
|
||||
focusedElement = focusManager.autoFocus(itemsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
if (!focusedElement) {
|
||||
focusedElement = focusManager.autoFocus(container);
|
||||
}
|
||||
|
||||
return focusedElement;
|
||||
}
|
||||
|
||||
export default {
|
||||
isEnabled: isEnabled,
|
||||
|
|
|
@ -7,282 +7,278 @@ import ServerConnections from '../ServerConnections';
|
|||
|
||||
import './backdrop.scss';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function enableAnimation() {
|
||||
return !browser.slow;
|
||||
}
|
||||
|
||||
function enableAnimation() {
|
||||
return !browser.slow;
|
||||
}
|
||||
|
||||
function enableRotation() {
|
||||
return !browser.tv
|
||||
function enableRotation() {
|
||||
return !browser.tv
|
||||
// Causes high cpu usage
|
||||
&& !browser.firefox;
|
||||
}
|
||||
}
|
||||
|
||||
class Backdrop {
|
||||
load(url, parent, existingBackdropImage) {
|
||||
const img = new Image();
|
||||
const self = this;
|
||||
class Backdrop {
|
||||
load(url, parent, existingBackdropImage) {
|
||||
const img = new Image();
|
||||
const self = this;
|
||||
|
||||
img.onload = () => {
|
||||
if (self.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const backdropImage = document.createElement('div');
|
||||
backdropImage.classList.add('backdropImage');
|
||||
backdropImage.classList.add('displayingBackdropImage');
|
||||
backdropImage.style.backgroundImage = `url('${url}')`;
|
||||
backdropImage.setAttribute('data-url', url);
|
||||
|
||||
backdropImage.classList.add('backdropImageFadeIn');
|
||||
parent.appendChild(backdropImage);
|
||||
|
||||
if (!enableAnimation()) {
|
||||
if (existingBackdropImage && existingBackdropImage.parentNode) {
|
||||
existingBackdropImage.parentNode.removeChild(existingBackdropImage);
|
||||
}
|
||||
internalBackdrop(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const onAnimationComplete = () => {
|
||||
dom.removeEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, {
|
||||
once: true
|
||||
});
|
||||
if (backdropImage === self.currentAnimatingElement) {
|
||||
self.currentAnimatingElement = null;
|
||||
}
|
||||
if (existingBackdropImage && existingBackdropImage.parentNode) {
|
||||
existingBackdropImage.parentNode.removeChild(existingBackdropImage);
|
||||
}
|
||||
};
|
||||
|
||||
dom.addEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, {
|
||||
once: true
|
||||
});
|
||||
|
||||
internalBackdrop(true);
|
||||
};
|
||||
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
cancelAnimation() {
|
||||
const elem = this.currentAnimatingElement;
|
||||
if (elem) {
|
||||
elem.classList.remove('backdropImageFadeIn');
|
||||
this.currentAnimatingElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.isDestroyed = true;
|
||||
this.cancelAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
let backdropContainer;
|
||||
function getBackdropContainer() {
|
||||
if (!backdropContainer) {
|
||||
backdropContainer = document.querySelector('.backdropContainer');
|
||||
}
|
||||
|
||||
if (!backdropContainer) {
|
||||
backdropContainer = document.createElement('div');
|
||||
backdropContainer.classList.add('backdropContainer');
|
||||
document.body.insertBefore(backdropContainer, document.body.firstChild);
|
||||
}
|
||||
|
||||
return backdropContainer;
|
||||
}
|
||||
|
||||
export function clearBackdrop(clearAll) {
|
||||
clearRotation();
|
||||
|
||||
if (currentLoadingBackdrop) {
|
||||
currentLoadingBackdrop.destroy();
|
||||
currentLoadingBackdrop = null;
|
||||
}
|
||||
|
||||
const elem = getBackdropContainer();
|
||||
elem.innerHTML = '';
|
||||
|
||||
if (clearAll) {
|
||||
hasExternalBackdrop = false;
|
||||
}
|
||||
|
||||
internalBackdrop(false);
|
||||
}
|
||||
|
||||
let backgroundContainer;
|
||||
function getBackgroundContainer() {
|
||||
if (!backgroundContainer) {
|
||||
backgroundContainer = document.querySelector('.backgroundContainer');
|
||||
}
|
||||
return backgroundContainer;
|
||||
}
|
||||
|
||||
function setBackgroundContainerBackgroundEnabled() {
|
||||
if (hasInternalBackdrop || hasExternalBackdrop) {
|
||||
getBackgroundContainer().classList.add('withBackdrop');
|
||||
} else {
|
||||
getBackgroundContainer().classList.remove('withBackdrop');
|
||||
}
|
||||
}
|
||||
|
||||
let hasInternalBackdrop;
|
||||
function internalBackdrop(isEnabled) {
|
||||
hasInternalBackdrop = isEnabled;
|
||||
setBackgroundContainerBackgroundEnabled();
|
||||
}
|
||||
|
||||
let hasExternalBackdrop;
|
||||
export function externalBackdrop(isEnabled) {
|
||||
hasExternalBackdrop = isEnabled;
|
||||
setBackgroundContainerBackgroundEnabled();
|
||||
}
|
||||
|
||||
let currentLoadingBackdrop;
|
||||
function setBackdropImage(url) {
|
||||
if (currentLoadingBackdrop) {
|
||||
currentLoadingBackdrop.destroy();
|
||||
currentLoadingBackdrop = null;
|
||||
}
|
||||
|
||||
const elem = getBackdropContainer();
|
||||
const existingBackdropImage = elem.querySelector('.displayingBackdropImage');
|
||||
|
||||
if (existingBackdropImage && existingBackdropImage.getAttribute('data-url') === url) {
|
||||
if (existingBackdropImage.getAttribute('data-url') === url) {
|
||||
img.onload = () => {
|
||||
if (self.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
existingBackdropImage.classList.remove('displayingBackdropImage');
|
||||
}
|
||||
|
||||
const instance = new Backdrop();
|
||||
instance.load(url, elem, existingBackdropImage);
|
||||
currentLoadingBackdrop = instance;
|
||||
}
|
||||
const backdropImage = document.createElement('div');
|
||||
backdropImage.classList.add('backdropImage');
|
||||
backdropImage.classList.add('displayingBackdropImage');
|
||||
backdropImage.style.backgroundImage = `url('${url}')`;
|
||||
backdropImage.setAttribute('data-url', url);
|
||||
|
||||
function getItemImageUrls(item, imageOptions) {
|
||||
imageOptions = imageOptions || {};
|
||||
backdropImage.classList.add('backdropImageFadeIn');
|
||||
parent.appendChild(backdropImage);
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
if (item.BackdropImageTags && item.BackdropImageTags.length > 0) {
|
||||
return item.BackdropImageTags.map((imgTag, index) => {
|
||||
return apiClient.getScaledImageUrl(item.BackdropItemId || item.Id, Object.assign(imageOptions, {
|
||||
type: 'Backdrop',
|
||||
tag: imgTag,
|
||||
maxWidth: dom.getScreenWidth(),
|
||||
index: index
|
||||
}));
|
||||
if (!enableAnimation()) {
|
||||
if (existingBackdropImage && existingBackdropImage.parentNode) {
|
||||
existingBackdropImage.parentNode.removeChild(existingBackdropImage);
|
||||
}
|
||||
internalBackdrop(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const onAnimationComplete = () => {
|
||||
dom.removeEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, {
|
||||
once: true
|
||||
});
|
||||
if (backdropImage === self.currentAnimatingElement) {
|
||||
self.currentAnimatingElement = null;
|
||||
}
|
||||
if (existingBackdropImage && existingBackdropImage.parentNode) {
|
||||
existingBackdropImage.parentNode.removeChild(existingBackdropImage);
|
||||
}
|
||||
};
|
||||
|
||||
dom.addEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, {
|
||||
once: true
|
||||
});
|
||||
}
|
||||
|
||||
if (item.ParentBackdropItemId && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) {
|
||||
return item.ParentBackdropImageTags.map((imgTag, index) => {
|
||||
return apiClient.getScaledImageUrl(item.ParentBackdropItemId, Object.assign(imageOptions, {
|
||||
type: 'Backdrop',
|
||||
tag: imgTag,
|
||||
maxWidth: dom.getScreenWidth(),
|
||||
index: index
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getImageUrls(items, imageOptions) {
|
||||
const list = [];
|
||||
const onImg = img => {
|
||||
list.push(img);
|
||||
internalBackdrop(true);
|
||||
};
|
||||
|
||||
for (let i = 0, length = items.length; i < length; i++) {
|
||||
const itemImages = getItemImageUrls(items[i], imageOptions);
|
||||
itemImages.forEach(onImg);
|
||||
}
|
||||
|
||||
return list;
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
function enabled() {
|
||||
return userSettings.enableBackdrops();
|
||||
}
|
||||
|
||||
let rotationInterval;
|
||||
let currentRotatingImages = [];
|
||||
let currentRotationIndex = -1;
|
||||
export function setBackdrops(items, imageOptions, enableImageRotation) {
|
||||
if (enabled()) {
|
||||
const images = getImageUrls(items, imageOptions);
|
||||
|
||||
if (images.length) {
|
||||
startRotation(images, enableImageRotation);
|
||||
} else {
|
||||
clearBackdrop();
|
||||
}
|
||||
cancelAnimation() {
|
||||
const elem = this.currentAnimatingElement;
|
||||
if (elem) {
|
||||
elem.classList.remove('backdropImageFadeIn');
|
||||
this.currentAnimatingElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startRotation(images, enableImageRotation) {
|
||||
if (isEqual(images, currentRotatingImages)) {
|
||||
destroy() {
|
||||
this.isDestroyed = true;
|
||||
this.cancelAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
let backdropContainer;
|
||||
function getBackdropContainer() {
|
||||
if (!backdropContainer) {
|
||||
backdropContainer = document.querySelector('.backdropContainer');
|
||||
}
|
||||
|
||||
if (!backdropContainer) {
|
||||
backdropContainer = document.createElement('div');
|
||||
backdropContainer.classList.add('backdropContainer');
|
||||
document.body.insertBefore(backdropContainer, document.body.firstChild);
|
||||
}
|
||||
|
||||
return backdropContainer;
|
||||
}
|
||||
|
||||
export function clearBackdrop(clearAll) {
|
||||
clearRotation();
|
||||
|
||||
if (currentLoadingBackdrop) {
|
||||
currentLoadingBackdrop.destroy();
|
||||
currentLoadingBackdrop = null;
|
||||
}
|
||||
|
||||
const elem = getBackdropContainer();
|
||||
elem.innerHTML = '';
|
||||
|
||||
if (clearAll) {
|
||||
hasExternalBackdrop = false;
|
||||
}
|
||||
|
||||
internalBackdrop(false);
|
||||
}
|
||||
|
||||
let backgroundContainer;
|
||||
function getBackgroundContainer() {
|
||||
if (!backgroundContainer) {
|
||||
backgroundContainer = document.querySelector('.backgroundContainer');
|
||||
}
|
||||
return backgroundContainer;
|
||||
}
|
||||
|
||||
function setBackgroundContainerBackgroundEnabled() {
|
||||
if (hasInternalBackdrop || hasExternalBackdrop) {
|
||||
getBackgroundContainer().classList.add('withBackdrop');
|
||||
} else {
|
||||
getBackgroundContainer().classList.remove('withBackdrop');
|
||||
}
|
||||
}
|
||||
|
||||
let hasInternalBackdrop;
|
||||
function internalBackdrop(isEnabled) {
|
||||
hasInternalBackdrop = isEnabled;
|
||||
setBackgroundContainerBackgroundEnabled();
|
||||
}
|
||||
|
||||
let hasExternalBackdrop;
|
||||
export function externalBackdrop(isEnabled) {
|
||||
hasExternalBackdrop = isEnabled;
|
||||
setBackgroundContainerBackgroundEnabled();
|
||||
}
|
||||
|
||||
let currentLoadingBackdrop;
|
||||
function setBackdropImage(url) {
|
||||
if (currentLoadingBackdrop) {
|
||||
currentLoadingBackdrop.destroy();
|
||||
currentLoadingBackdrop = null;
|
||||
}
|
||||
|
||||
const elem = getBackdropContainer();
|
||||
const existingBackdropImage = elem.querySelector('.displayingBackdropImage');
|
||||
|
||||
if (existingBackdropImage && existingBackdropImage.getAttribute('data-url') === url) {
|
||||
if (existingBackdropImage.getAttribute('data-url') === url) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearRotation();
|
||||
|
||||
currentRotatingImages = images;
|
||||
currentRotationIndex = -1;
|
||||
|
||||
if (images.length > 1 && enableImageRotation !== false && enableRotation()) {
|
||||
rotationInterval = setInterval(onRotationInterval, 24000);
|
||||
}
|
||||
|
||||
onRotationInterval();
|
||||
existingBackdropImage.classList.remove('displayingBackdropImage');
|
||||
}
|
||||
|
||||
function onRotationInterval() {
|
||||
if (playbackManager.isPlayingLocally(['Video'])) {
|
||||
return;
|
||||
}
|
||||
const instance = new Backdrop();
|
||||
instance.load(url, elem, existingBackdropImage);
|
||||
currentLoadingBackdrop = instance;
|
||||
}
|
||||
|
||||
let newIndex = currentRotationIndex + 1;
|
||||
if (newIndex >= currentRotatingImages.length) {
|
||||
newIndex = 0;
|
||||
}
|
||||
function getItemImageUrls(item, imageOptions) {
|
||||
imageOptions = imageOptions || {};
|
||||
|
||||
currentRotationIndex = newIndex;
|
||||
setBackdropImage(currentRotatingImages[newIndex]);
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
if (item.BackdropImageTags && item.BackdropImageTags.length > 0) {
|
||||
return item.BackdropImageTags.map((imgTag, index) => {
|
||||
return apiClient.getScaledImageUrl(item.BackdropItemId || item.Id, Object.assign(imageOptions, {
|
||||
type: 'Backdrop',
|
||||
tag: imgTag,
|
||||
maxWidth: dom.getScreenWidth(),
|
||||
index: index
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function clearRotation() {
|
||||
const interval = rotationInterval;
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
||||
rotationInterval = null;
|
||||
currentRotatingImages = [];
|
||||
currentRotationIndex = -1;
|
||||
if (item.ParentBackdropItemId && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) {
|
||||
return item.ParentBackdropImageTags.map((imgTag, index) => {
|
||||
return apiClient.getScaledImageUrl(item.ParentBackdropItemId, Object.assign(imageOptions, {
|
||||
type: 'Backdrop',
|
||||
tag: imgTag,
|
||||
maxWidth: dom.getScreenWidth(),
|
||||
index: index
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
export function setBackdrop(url, imageOptions) {
|
||||
if (url && typeof url !== 'string') {
|
||||
url = getImageUrls([url], imageOptions)[0];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
if (url) {
|
||||
clearRotation();
|
||||
setBackdropImage(url);
|
||||
function getImageUrls(items, imageOptions) {
|
||||
const list = [];
|
||||
const onImg = img => {
|
||||
list.push(img);
|
||||
};
|
||||
|
||||
for (let i = 0, length = items.length; i < length; i++) {
|
||||
const itemImages = getItemImageUrls(items[i], imageOptions);
|
||||
itemImages.forEach(onImg);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function enabled() {
|
||||
return userSettings.enableBackdrops();
|
||||
}
|
||||
|
||||
let rotationInterval;
|
||||
let currentRotatingImages = [];
|
||||
let currentRotationIndex = -1;
|
||||
export function setBackdrops(items, imageOptions, enableImageRotation) {
|
||||
if (enabled()) {
|
||||
const images = getImageUrls(items, imageOptions);
|
||||
|
||||
if (images.length) {
|
||||
startRotation(images, enableImageRotation);
|
||||
} else {
|
||||
clearBackdrop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
function startRotation(images, enableImageRotation) {
|
||||
if (isEqual(images, currentRotatingImages)) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearRotation();
|
||||
|
||||
currentRotatingImages = images;
|
||||
currentRotationIndex = -1;
|
||||
|
||||
if (images.length > 1 && enableImageRotation !== false && enableRotation()) {
|
||||
rotationInterval = setInterval(onRotationInterval, 24000);
|
||||
}
|
||||
|
||||
onRotationInterval();
|
||||
}
|
||||
|
||||
function onRotationInterval() {
|
||||
if (playbackManager.isPlayingLocally(['Video'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newIndex = currentRotationIndex + 1;
|
||||
if (newIndex >= currentRotatingImages.length) {
|
||||
newIndex = 0;
|
||||
}
|
||||
|
||||
currentRotationIndex = newIndex;
|
||||
setBackdropImage(currentRotatingImages[newIndex]);
|
||||
}
|
||||
|
||||
function clearRotation() {
|
||||
const interval = rotationInterval;
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
||||
rotationInterval = null;
|
||||
currentRotatingImages = [];
|
||||
currentRotationIndex = -1;
|
||||
}
|
||||
|
||||
export function setBackdrop(url, imageOptions) {
|
||||
if (url && typeof url !== 'string') {
|
||||
url = getImageUrls([url], imageOptions)[0];
|
||||
}
|
||||
|
||||
if (url) {
|
||||
clearRotation();
|
||||
setBackdropImage(url);
|
||||
} else {
|
||||
clearBackdrop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @enum TransparencyLevel
|
||||
|
|
|
@ -114,7 +114,7 @@ button::-moz-focus-inner {
|
|||
}
|
||||
|
||||
.card.show-animation:focus > .cardBox {
|
||||
transform: scale(1.18, 1.18);
|
||||
transform: scale(1.07, 1.07);
|
||||
}
|
||||
|
||||
.cardBox-bottompadded {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module for building cards from item data.
|
||||
|
@ -12,123 +11,121 @@ import layoutManager from '../layoutManager';
|
|||
import browser from '../../scripts/browser';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
|
||||
function buildChapterCardsHtml(item, chapters, options) {
|
||||
// TODO move card creation code to Card component
|
||||
function buildChapterCardsHtml(item, chapters, options) {
|
||||
// TODO move card creation code to Card component
|
||||
|
||||
let className = 'card itemAction chapterCard';
|
||||
let className = 'card itemAction chapterCard';
|
||||
|
||||
if (layoutManager.tv) {
|
||||
className += ' show-focus';
|
||||
if (layoutManager.tv) {
|
||||
className += ' show-focus';
|
||||
|
||||
if (enableFocusTransform) {
|
||||
className += ' show-animation';
|
||||
}
|
||||
if (enableFocusTransform) {
|
||||
className += ' show-animation';
|
||||
}
|
||||
|
||||
const mediaStreams = ((item.MediaSources || [])[0] || {}).MediaStreams || [];
|
||||
const videoStream = mediaStreams.filter(({Type}) => {
|
||||
return Type === 'Video';
|
||||
})[0] || {};
|
||||
|
||||
let shape = (options.backdropShape || 'backdrop');
|
||||
|
||||
if (videoStream.Width && videoStream.Height && (videoStream.Width / videoStream.Height) <= 1.2) {
|
||||
shape = (options.squareShape || 'square');
|
||||
}
|
||||
|
||||
className += ` ${shape}Card`;
|
||||
|
||||
if (options.block || options.rows) {
|
||||
className += ' block';
|
||||
}
|
||||
|
||||
let html = '';
|
||||
let itemsInRow = 0;
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
|
||||
for (let i = 0, length = chapters.length; i < length; i++) {
|
||||
if (options.rows && itemsInRow === 0) {
|
||||
html += '<div class="cardColumn">';
|
||||
}
|
||||
|
||||
const chapter = chapters[i];
|
||||
|
||||
html += buildChapterCard(item, apiClient, chapter, i, options, className, shape);
|
||||
itemsInRow++;
|
||||
|
||||
if (options.rows && itemsInRow >= options.rows) {
|
||||
itemsInRow = 0;
|
||||
html += '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function getImgUrl({Id}, {ImageTag}, index, maxWidth, apiClient) {
|
||||
if (ImageTag) {
|
||||
return apiClient.getScaledImageUrl(Id, {
|
||||
const mediaStreams = ((item.MediaSources || [])[0] || {}).MediaStreams || [];
|
||||
const videoStream = mediaStreams.filter(({ Type }) => {
|
||||
return Type === 'Video';
|
||||
})[0] || {};
|
||||
|
||||
maxWidth: maxWidth,
|
||||
tag: ImageTag,
|
||||
type: 'Chapter',
|
||||
index
|
||||
});
|
||||
}
|
||||
let shape = (options.backdropShape || 'backdrop');
|
||||
|
||||
return null;
|
||||
if (videoStream.Width && videoStream.Height && (videoStream.Width / videoStream.Height) <= 1.2) {
|
||||
shape = (options.squareShape || 'square');
|
||||
}
|
||||
|
||||
function buildChapterCard(item, apiClient, chapter, index, {width, coverImage}, className, shape) {
|
||||
const imgUrl = getImgUrl(item, chapter, index, width || 400, apiClient);
|
||||
className += ` ${shape}Card`;
|
||||
|
||||
let cardImageContainerClass = 'cardContent cardContent-shadow cardImageContainer chapterCardImageContainer';
|
||||
if (coverImage) {
|
||||
cardImageContainerClass += ' coveredImage';
|
||||
}
|
||||
const dataAttributes = ` data-action="play" data-isfolder="${item.IsFolder}" data-id="${item.Id}" data-serverid="${item.ServerId}" data-type="${item.Type}" data-mediatype="${item.MediaType}" data-positionticks="${chapter.StartPositionTicks}"`;
|
||||
let cardImageContainer = imgUrl ? (`<div class="${cardImageContainerClass} lazy" data-src="${imgUrl}">`) : (`<div class="${cardImageContainerClass}">`);
|
||||
|
||||
if (!imgUrl) {
|
||||
cardImageContainer += '<span class="material-icons cardImageIcon local_movies" aria-hidden="true"></span>';
|
||||
}
|
||||
|
||||
let nameHtml = '';
|
||||
nameHtml += `<div class="cardText">${escapeHtml(chapter.Name)}</div>`;
|
||||
nameHtml += `<div class="cardText">${datetime.getDisplayRunningTime(chapter.StartPositionTicks)}</div>`;
|
||||
|
||||
const cardBoxCssClass = 'cardBox';
|
||||
const cardScalableClass = 'cardScalable';
|
||||
|
||||
return `<button type="button" class="${className}"${dataAttributes}><div class="${cardBoxCssClass}"><div class="${cardScalableClass}"><div class="cardPadder-${shape}"></div>${cardImageContainer}</div><div class="innerCardFooter">${nameHtml}</div></div></div></button>`;
|
||||
if (options.block || options.rows) {
|
||||
className += ' block';
|
||||
}
|
||||
|
||||
export function buildChapterCards(item, chapters, options) {
|
||||
if (options.parentContainer) {
|
||||
// Abort if the container has been disposed
|
||||
if (!document.body.contains(options.parentContainer)) {
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
let itemsInRow = 0;
|
||||
|
||||
if (chapters.length) {
|
||||
options.parentContainer.classList.remove('hide');
|
||||
} else {
|
||||
options.parentContainer.classList.add('hide');
|
||||
return;
|
||||
}
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
|
||||
for (let i = 0, length = chapters.length; i < length; i++) {
|
||||
if (options.rows && itemsInRow === 0) {
|
||||
html += '<div class="cardColumn">';
|
||||
}
|
||||
|
||||
const html = buildChapterCardsHtml(item, chapters, options);
|
||||
const chapter = chapters[i];
|
||||
|
||||
options.itemsContainer.innerHTML = html;
|
||||
html += buildChapterCard(item, apiClient, chapter, i, options, className, shape);
|
||||
itemsInRow++;
|
||||
|
||||
imageLoader.lazyChildren(options.itemsContainer);
|
||||
if (options.rows && itemsInRow >= options.rows) {
|
||||
itemsInRow = 0;
|
||||
html += '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
return html;
|
||||
}
|
||||
|
||||
function getImgUrl({ Id }, { ImageTag }, index, maxWidth, apiClient) {
|
||||
if (ImageTag) {
|
||||
return apiClient.getScaledImageUrl(Id, {
|
||||
|
||||
maxWidth: maxWidth,
|
||||
tag: ImageTag,
|
||||
type: 'Chapter',
|
||||
index
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildChapterCard(item, apiClient, chapter, index, { width, coverImage }, className, shape) {
|
||||
const imgUrl = getImgUrl(item, chapter, index, width || 400, apiClient);
|
||||
|
||||
let cardImageContainerClass = 'cardContent cardContent-shadow cardImageContainer chapterCardImageContainer';
|
||||
if (coverImage) {
|
||||
cardImageContainerClass += ' coveredImage';
|
||||
}
|
||||
const dataAttributes = ` data-action="play" data-isfolder="${item.IsFolder}" data-id="${item.Id}" data-serverid="${item.ServerId}" data-type="${item.Type}" data-mediatype="${item.MediaType}" data-positionticks="${chapter.StartPositionTicks}"`;
|
||||
let cardImageContainer = imgUrl ? (`<div class="${cardImageContainerClass} lazy" data-src="${imgUrl}">`) : (`<div class="${cardImageContainerClass}">`);
|
||||
|
||||
if (!imgUrl) {
|
||||
cardImageContainer += '<span class="material-icons cardImageIcon local_movies" aria-hidden="true"></span>';
|
||||
}
|
||||
|
||||
let nameHtml = '';
|
||||
nameHtml += `<div class="cardText">${escapeHtml(chapter.Name)}</div>`;
|
||||
nameHtml += `<div class="cardText">${datetime.getDisplayRunningTime(chapter.StartPositionTicks)}</div>`;
|
||||
|
||||
const cardBoxCssClass = 'cardBox';
|
||||
const cardScalableClass = 'cardScalable';
|
||||
|
||||
return `<button type="button" class="${className}"${dataAttributes}><div class="${cardBoxCssClass}"><div class="${cardScalableClass}"><div class="cardPadder-${shape}"></div>${cardImageContainer}</div><div class="innerCardFooter">${nameHtml}</div></div></div></button>`;
|
||||
}
|
||||
|
||||
export function buildChapterCards(item, chapters, options) {
|
||||
if (options.parentContainer) {
|
||||
// Abort if the container has been disposed
|
||||
if (!document.body.contains(options.parentContainer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (chapters.length) {
|
||||
options.parentContainer.classList.remove('hide');
|
||||
} else {
|
||||
options.parentContainer.classList.add('hide');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const html = buildChapterCardsHtml(item, chapters, options);
|
||||
|
||||
options.itemsContainer.innerHTML = html;
|
||||
|
||||
imageLoader.lazyChildren(options.itemsContainer);
|
||||
}
|
||||
|
||||
export default {
|
||||
buildChapterCards: buildChapterCards
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module for building cards from item data.
|
||||
|
@ -7,20 +6,18 @@
|
|||
|
||||
import cardBuilder from './cardBuilder';
|
||||
|
||||
export function buildPeopleCards(items, options) {
|
||||
options = Object.assign(options || {}, {
|
||||
cardLayout: false,
|
||||
centerText: true,
|
||||
showTitle: true,
|
||||
cardFooterAside: 'none',
|
||||
showPersonRoleOrType: true,
|
||||
cardCssClass: 'personCard',
|
||||
defaultCardImageIcon: 'person'
|
||||
});
|
||||
cardBuilder.buildCards(items, options);
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
export function buildPeopleCards(items, options) {
|
||||
options = Object.assign(options || {}, {
|
||||
cardLayout: false,
|
||||
centerText: true,
|
||||
showTitle: true,
|
||||
cardFooterAside: 'none',
|
||||
showPersonRoleOrType: true,
|
||||
cardCssClass: 'personCard',
|
||||
defaultCardImageIcon: 'person'
|
||||
});
|
||||
cardBuilder.buildCards(items, options);
|
||||
}
|
||||
|
||||
export default {
|
||||
buildPeopleCards: buildPeopleCards
|
||||
|
|
|
@ -3,7 +3,7 @@ import dom from '../../scripts/dom';
|
|||
import dialogHelper from '../dialogHelper/dialogHelper';
|
||||
import loading from '../loading/loading';
|
||||
import layoutManager from '../layoutManager';
|
||||
import { appRouter } from '../appRouter';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
|
@ -12,258 +12,255 @@ import '../../elements/emby-input/emby-input';
|
|||
import '../../elements/emby-select/emby-select';
|
||||
import 'material-design-icons-iconfont';
|
||||
import '../formdialog.scss';
|
||||
import '../../assets/css/flexstyles.scss';
|
||||
import '../../styles/flexstyles.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
|
||||
/* eslint-disable indent */
|
||||
let currentServerId;
|
||||
|
||||
let currentServerId;
|
||||
function onSubmit(e) {
|
||||
loading.show();
|
||||
|
||||
function onSubmit(e) {
|
||||
loading.show();
|
||||
const panel = dom.parentWithClass(this, 'dialog');
|
||||
|
||||
const panel = dom.parentWithClass(this, 'dialog');
|
||||
const collectionId = panel.querySelector('#selectCollectionToAddTo').value;
|
||||
|
||||
const collectionId = panel.querySelector('#selectCollectionToAddTo').value;
|
||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||
|
||||
if (collectionId) {
|
||||
addToCollection(apiClient, panel, collectionId);
|
||||
} else {
|
||||
createCollection(apiClient, panel);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
if (collectionId) {
|
||||
addToCollection(apiClient, panel, collectionId);
|
||||
} else {
|
||||
createCollection(apiClient, panel);
|
||||
}
|
||||
|
||||
function createCollection(apiClient, dlg) {
|
||||
const url = apiClient.getUrl('Collections', {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
Name: dlg.querySelector('#txtNewCollectionName').value,
|
||||
IsLocked: !dlg.querySelector('#chkEnableInternetMetadata').checked,
|
||||
Ids: dlg.querySelector('.fldSelectedItemIds').value || ''
|
||||
});
|
||||
function createCollection(apiClient, dlg) {
|
||||
const url = apiClient.getUrl('Collections', {
|
||||
|
||||
apiClient.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
dataType: 'json'
|
||||
Name: dlg.querySelector('#txtNewCollectionName').value,
|
||||
IsLocked: !dlg.querySelector('#chkEnableInternetMetadata').checked,
|
||||
Ids: dlg.querySelector('.fldSelectedItemIds').value || ''
|
||||
});
|
||||
|
||||
}).then(result => {
|
||||
loading.hide();
|
||||
apiClient.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
dataType: 'json'
|
||||
|
||||
const id = result.Id;
|
||||
}).then(result => {
|
||||
loading.hide();
|
||||
|
||||
dlg.submitted = true;
|
||||
dialogHelper.close(dlg);
|
||||
redirectToCollection(apiClient, id);
|
||||
});
|
||||
}
|
||||
const id = result.Id;
|
||||
|
||||
function redirectToCollection(apiClient, id) {
|
||||
appRouter.showItem(id, apiClient.serverId());
|
||||
}
|
||||
dlg.submitted = true;
|
||||
dialogHelper.close(dlg);
|
||||
redirectToCollection(apiClient, id);
|
||||
});
|
||||
}
|
||||
|
||||
function addToCollection(apiClient, dlg, id) {
|
||||
const url = apiClient.getUrl(`Collections/${id}/Items`, {
|
||||
function redirectToCollection(apiClient, id) {
|
||||
appRouter.showItem(id, apiClient.serverId());
|
||||
}
|
||||
|
||||
Ids: dlg.querySelector('.fldSelectedItemIds').value || ''
|
||||
});
|
||||
function addToCollection(apiClient, dlg, id) {
|
||||
const url = apiClient.getUrl(`Collections/${id}/Items`, {
|
||||
|
||||
apiClient.ajax({
|
||||
type: 'POST',
|
||||
url: url
|
||||
Ids: dlg.querySelector('.fldSelectedItemIds').value || ''
|
||||
});
|
||||
|
||||
}).then(() => {
|
||||
loading.hide();
|
||||
apiClient.ajax({
|
||||
type: 'POST',
|
||||
url: url
|
||||
|
||||
dlg.submitted = true;
|
||||
dialogHelper.close(dlg);
|
||||
}).then(() => {
|
||||
loading.hide();
|
||||
|
||||
toast(globalize.translate('MessageItemsAdded'));
|
||||
});
|
||||
}
|
||||
dlg.submitted = true;
|
||||
dialogHelper.close(dlg);
|
||||
|
||||
function triggerChange(select) {
|
||||
select.dispatchEvent(new CustomEvent('change', {}));
|
||||
}
|
||||
toast(globalize.translate('MessageItemsAdded'));
|
||||
});
|
||||
}
|
||||
|
||||
function populateCollections(panel) {
|
||||
loading.show();
|
||||
function triggerChange(select) {
|
||||
select.dispatchEvent(new CustomEvent('change', {}));
|
||||
}
|
||||
|
||||
const select = panel.querySelector('#selectCollectionToAddTo');
|
||||
function populateCollections(panel) {
|
||||
loading.show();
|
||||
|
||||
panel.querySelector('.newCollectionInfo').classList.add('hide');
|
||||
const select = panel.querySelector('#selectCollectionToAddTo');
|
||||
|
||||
const options = {
|
||||
panel.querySelector('.newCollectionInfo').classList.add('hide');
|
||||
|
||||
Recursive: true,
|
||||
IncludeItemTypes: 'BoxSet',
|
||||
SortBy: 'SortName',
|
||||
EnableTotalRecordCount: false
|
||||
};
|
||||
const options = {
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||
apiClient.getItems(apiClient.getCurrentUserId(), options).then(result => {
|
||||
let html = '';
|
||||
Recursive: true,
|
||||
IncludeItemTypes: 'BoxSet',
|
||||
SortBy: 'SortName',
|
||||
EnableTotalRecordCount: false
|
||||
};
|
||||
|
||||
html += `<option value="">${globalize.translate('OptionNew')}</option>`;
|
||||
|
||||
html += result.Items.map(i => {
|
||||
return `<option value="${i.Id}">${escapeHtml(i.Name)}</option>`;
|
||||
});
|
||||
|
||||
select.innerHTML = html;
|
||||
select.value = '';
|
||||
triggerChange(select);
|
||||
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function getEditorHtml() {
|
||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||
apiClient.getItems(apiClient.getCurrentUserId(), options).then(result => {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="formDialogContent smoothScrollY" style="padding-top:2em;">';
|
||||
html += '<div class="dialogContentInner dialog-content-centered">';
|
||||
html += '<form class="newCollectionForm" style="margin:auto;">';
|
||||
html += `<option value="">${globalize.translate('OptionNew')}</option>`;
|
||||
|
||||
html += '<div>';
|
||||
html += globalize.translate('NewCollectionHelp');
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="fldSelectCollection">';
|
||||
html += '<br/>';
|
||||
html += '<br/>';
|
||||
html += '<div class="selectContainer">';
|
||||
html += `<select is="emby-select" label="${globalize.translate('LabelCollection')}" id="selectCollectionToAddTo" autofocus></select>`;
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="newCollectionInfo">';
|
||||
|
||||
html += '<div class="inputContainer">';
|
||||
html += `<input is="emby-input" type="text" id="txtNewCollectionName" required="required" label="${globalize.translate('LabelName')}" />`;
|
||||
html += `<div class="fieldDescription">${globalize.translate('NewCollectionNameExample')}</div>`;
|
||||
html += '</div>';
|
||||
|
||||
html += '<label class="checkboxContainer">';
|
||||
html += '<input is="emby-checkbox" type="checkbox" id="chkEnableInternetMetadata" />';
|
||||
html += `<span>${globalize.translate('SearchForCollectionInternetMetadata')}</span>`;
|
||||
html += '</label>';
|
||||
|
||||
// newCollectionInfo
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="formDialogFooter">';
|
||||
html += `<button is="emby-button" type="submit" class="raised btnSubmit block formDialogFooterItem button-submit">${globalize.translate('ButtonOk')}</button>`;
|
||||
html += '</div>';
|
||||
|
||||
html += '<input type="hidden" class="fldSelectedItemIds" />';
|
||||
|
||||
html += '</form>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function initEditor(content, items) {
|
||||
content.querySelector('#selectCollectionToAddTo').addEventListener('change', function () {
|
||||
if (this.value) {
|
||||
content.querySelector('.newCollectionInfo').classList.add('hide');
|
||||
content.querySelector('#txtNewCollectionName').removeAttribute('required');
|
||||
} else {
|
||||
content.querySelector('.newCollectionInfo').classList.remove('hide');
|
||||
content.querySelector('#txtNewCollectionName').setAttribute('required', 'required');
|
||||
}
|
||||
html += result.Items.map(i => {
|
||||
return `<option value="${i.Id}">${escapeHtml(i.Name)}</option>`;
|
||||
});
|
||||
|
||||
content.querySelector('form').addEventListener('submit', onSubmit);
|
||||
select.innerHTML = html;
|
||||
select.value = '';
|
||||
triggerChange(select);
|
||||
|
||||
content.querySelector('.fldSelectedItemIds', content).value = items.join(',');
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
if (items.length) {
|
||||
content.querySelector('.fldSelectCollection').classList.remove('hide');
|
||||
populateCollections(content);
|
||||
function getEditorHtml() {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="formDialogContent smoothScrollY" style="padding-top:2em;">';
|
||||
html += '<div class="dialogContentInner dialog-content-centered">';
|
||||
html += '<form class="newCollectionForm" style="margin:auto;">';
|
||||
|
||||
html += '<div>';
|
||||
html += globalize.translate('NewCollectionHelp');
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="fldSelectCollection">';
|
||||
html += '<br/>';
|
||||
html += '<br/>';
|
||||
html += '<div class="selectContainer">';
|
||||
html += `<select is="emby-select" label="${globalize.translate('LabelCollection')}" id="selectCollectionToAddTo" autofocus></select>`;
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="newCollectionInfo">';
|
||||
|
||||
html += '<div class="inputContainer">';
|
||||
html += `<input is="emby-input" type="text" id="txtNewCollectionName" required="required" label="${globalize.translate('LabelName')}" />`;
|
||||
html += `<div class="fieldDescription">${globalize.translate('NewCollectionNameExample')}</div>`;
|
||||
html += '</div>';
|
||||
|
||||
html += '<label class="checkboxContainer">';
|
||||
html += '<input is="emby-checkbox" type="checkbox" id="chkEnableInternetMetadata" />';
|
||||
html += `<span>${globalize.translate('SearchForCollectionInternetMetadata')}</span>`;
|
||||
html += '</label>';
|
||||
|
||||
// newCollectionInfo
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="formDialogFooter">';
|
||||
html += `<button is="emby-button" type="submit" class="raised btnSubmit block formDialogFooterItem button-submit">${globalize.translate('ButtonOk')}</button>`;
|
||||
html += '</div>';
|
||||
|
||||
html += '<input type="hidden" class="fldSelectedItemIds" />';
|
||||
|
||||
html += '</form>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function initEditor(content, items) {
|
||||
content.querySelector('#selectCollectionToAddTo').addEventListener('change', function () {
|
||||
if (this.value) {
|
||||
content.querySelector('.newCollectionInfo').classList.add('hide');
|
||||
content.querySelector('#txtNewCollectionName').removeAttribute('required');
|
||||
} else {
|
||||
content.querySelector('.fldSelectCollection').classList.add('hide');
|
||||
|
||||
const selectCollectionToAddTo = content.querySelector('#selectCollectionToAddTo');
|
||||
selectCollectionToAddTo.innerHTML = '';
|
||||
selectCollectionToAddTo.value = '';
|
||||
triggerChange(selectCollectionToAddTo);
|
||||
content.querySelector('.newCollectionInfo').classList.remove('hide');
|
||||
content.querySelector('#txtNewCollectionName').setAttribute('required', 'required');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function centerFocus(elem, horiz, on) {
|
||||
import('../../scripts/scrollHelper').then((scrollHelper) => {
|
||||
const fn = on ? 'on' : 'off';
|
||||
scrollHelper.centerFocus[fn](elem, horiz);
|
||||
content.querySelector('form').addEventListener('submit', onSubmit);
|
||||
|
||||
content.querySelector('.fldSelectedItemIds', content).value = items.join(',');
|
||||
|
||||
if (items.length) {
|
||||
content.querySelector('.fldSelectCollection').classList.remove('hide');
|
||||
populateCollections(content);
|
||||
} else {
|
||||
content.querySelector('.fldSelectCollection').classList.add('hide');
|
||||
|
||||
const selectCollectionToAddTo = content.querySelector('#selectCollectionToAddTo');
|
||||
selectCollectionToAddTo.innerHTML = '';
|
||||
selectCollectionToAddTo.value = '';
|
||||
triggerChange(selectCollectionToAddTo);
|
||||
}
|
||||
}
|
||||
|
||||
function centerFocus(elem, horiz, on) {
|
||||
import('../../scripts/scrollHelper').then((scrollHelper) => {
|
||||
const fn = on ? 'on' : 'off';
|
||||
scrollHelper.centerFocus[fn](elem, horiz);
|
||||
});
|
||||
}
|
||||
|
||||
class CollectionEditor {
|
||||
show(options) {
|
||||
const items = options.items || {};
|
||||
currentServerId = options.serverId;
|
||||
|
||||
const dialogOptions = {
|
||||
removeOnClose: true,
|
||||
scrollY: false
|
||||
};
|
||||
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
} else {
|
||||
dialogOptions.size = 'small';
|
||||
}
|
||||
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.classList.add('formDialog');
|
||||
|
||||
let html = '';
|
||||
const title = items.length ? globalize.translate('HeaderAddToCollection') : globalize.translate('NewCollection');
|
||||
|
||||
html += '<div class="formDialogHeader">';
|
||||
html += `<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${globalize.translate('ButtonBack')}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
|
||||
html += '<h3 class="formDialogHeaderTitle">';
|
||||
html += title;
|
||||
html += '</h3>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
html += getEditorHtml();
|
||||
|
||||
dlg.innerHTML = html;
|
||||
|
||||
initEditor(dlg, items);
|
||||
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg.querySelector('.formDialogContent'), false, true);
|
||||
}
|
||||
|
||||
return dialogHelper.open(dlg).then(() => {
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg.querySelector('.formDialogContent'), false, false);
|
||||
}
|
||||
|
||||
if (dlg.submitted) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.reject();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CollectionEditor {
|
||||
show(options) {
|
||||
const items = options.items || {};
|
||||
currentServerId = options.serverId;
|
||||
|
||||
const dialogOptions = {
|
||||
removeOnClose: true,
|
||||
scrollY: false
|
||||
};
|
||||
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
} else {
|
||||
dialogOptions.size = 'small';
|
||||
}
|
||||
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.classList.add('formDialog');
|
||||
|
||||
let html = '';
|
||||
const title = items.length ? globalize.translate('HeaderAddToCollection') : globalize.translate('NewCollection');
|
||||
|
||||
html += '<div class="formDialogHeader">';
|
||||
html += `<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${globalize.translate('ButtonBack')}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
|
||||
html += '<h3 class="formDialogHeaderTitle">';
|
||||
html += title;
|
||||
html += '</h3>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
html += getEditorHtml();
|
||||
|
||||
dlg.innerHTML = html;
|
||||
|
||||
initEditor(dlg, items);
|
||||
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg.querySelector('.formDialogContent'), false, true);
|
||||
}
|
||||
|
||||
return dialogHelper.open(dlg).then(() => {
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg.querySelector('.formDialogContent'), false, false);
|
||||
}
|
||||
|
||||
if (dlg.submitted) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.reject();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
export default CollectionEditor;
|
||||
|
|
|
@ -17,9 +17,15 @@ const AlphaPickerContainer: FC<AlphaPickerContainerProps> = ({ viewQuerySettings
|
|||
const newValue = (e as CustomEvent).detail.value;
|
||||
let updatedValue: React.SetStateAction<ViewQuerySettings>;
|
||||
if (newValue === '#') {
|
||||
updatedValue = {NameLessThan: 'A'};
|
||||
updatedValue = {
|
||||
NameLessThan: 'A',
|
||||
NameStartsWith: undefined
|
||||
};
|
||||
} else {
|
||||
updatedValue = {NameStartsWith: newValue};
|
||||
updatedValue = {
|
||||
NameLessThan: undefined,
|
||||
NameStartsWith: newValue
|
||||
};
|
||||
}
|
||||
setViewQuerySettings((prevState) => ({
|
||||
...prevState,
|
||||
|
|
|
@ -22,7 +22,7 @@ const Filter: FC<FilterProps> = ({
|
|||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const showFilterMenu = useCallback(() => {
|
||||
import('../filtermenu/filtermenu').then(({default: FilterMenu}) => {
|
||||
import('../filtermenu/filtermenu').then(({ default: FilterMenu }) => {
|
||||
const filterMenu = new FilterMenu();
|
||||
filterMenu.show({
|
||||
settings: viewQuerySettings,
|
||||
|
@ -32,7 +32,11 @@ const Filter: FC<FilterProps> = ({
|
|||
serverId: window.ApiClient.serverId(),
|
||||
filterMenuOptions: getFilterMenuOptions(),
|
||||
setfilters: setViewQuerySettings
|
||||
}).catch(() => {
|
||||
// filter menu closed
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[Filter] failed to load filter menu', err);
|
||||
});
|
||||
}, [viewQuerySettings, getVisibleFilters, topParentId, getItemTypes, getFilterMenuOptions, setViewQuerySettings]);
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'
|
|||
import escapeHTML from 'escape-html';
|
||||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import { appRouter } from '../appRouter';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||
import layoutManager from '../layoutManager';
|
||||
import lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver';
|
||||
|
@ -73,6 +73,8 @@ const GenresItemsContainer: FC<GenresItemsContainerProps> = ({
|
|||
centerText: true,
|
||||
showYear: true
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[GenresItemsContainer] failed to fetch items', err);
|
||||
});
|
||||
}, [getPortraitShape, topParentId]);
|
||||
|
||||
|
@ -90,8 +92,8 @@ const GenresItemsContainer: FC<GenresItemsContainerProps> = ({
|
|||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||
<p>{globalize.translate('MessageNoGenresAvailable')}</p>
|
||||
</div>
|
||||
) : items.map((item, index) => (
|
||||
<div key={index} className='verticalSection'>
|
||||
) : items.map(item => (
|
||||
<div key={item.Id} className='verticalSection'>
|
||||
<div
|
||||
className='sectionTitleContainer sectionTitleContainer-cards padded-left'
|
||||
dangerouslySetInnerHTML={createLinkElement({
|
||||
|
|
|
@ -6,13 +6,17 @@ const NewCollection: FC = () => {
|
|||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const showCollectionEditor = useCallback(() => {
|
||||
import('../collectionEditor/collectionEditor').then(({default: CollectionEditor}) => {
|
||||
import('../collectionEditor/collectionEditor').then(({ default: CollectionEditor }) => {
|
||||
const serverId = window.ApiClient.serverId();
|
||||
const collectionEditor = new CollectionEditor();
|
||||
collectionEditor.show({
|
||||
items: [],
|
||||
serverId: serverId
|
||||
}).catch(() => {
|
||||
// closed collection editor
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[NewCollection] failed to load collection editor', err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -15,8 +15,9 @@ const Pagination: FC<PaginationProps> = ({ viewQuerySettings, setViewQuerySettin
|
|||
const limit = userSettings.libraryPageSize(undefined);
|
||||
const totalRecordCount = itemsResult.TotalRecordCount || 0;
|
||||
const startIndex = viewQuerySettings.StartIndex || 0;
|
||||
const recordsEnd = Math.min(startIndex + limit, totalRecordCount);
|
||||
const showControls = limit < totalRecordCount;
|
||||
const recordsStart = totalRecordCount ? startIndex + 1 : 0;
|
||||
const recordsEnd = limit ? Math.min(startIndex + limit, totalRecordCount) : totalRecordCount;
|
||||
const showControls = limit > 0 && limit < totalRecordCount;
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onNextPageClick = useCallback(() => {
|
||||
|
@ -69,25 +70,25 @@ const Pagination: FC<PaginationProps> = ({ viewQuerySettings, setViewQuerySettin
|
|||
return (
|
||||
<div ref={element}>
|
||||
<div className='paging'>
|
||||
{showControls && (
|
||||
<div className='listPaging' style={{ display: 'flex', alignItems: 'center' }}>
|
||||
|
||||
<span>
|
||||
{globalize.translate('ListPaging', (totalRecordCount ? startIndex + 1 : 0), recordsEnd, totalRecordCount)}
|
||||
</span>
|
||||
|
||||
<IconButtonElement
|
||||
is='paper-icon-button-light'
|
||||
className='btnPreviousPage autoSize'
|
||||
icon='material-icons arrow_back'
|
||||
/>
|
||||
<IconButtonElement
|
||||
is='paper-icon-button-light'
|
||||
className='btnNextPage autoSize'
|
||||
icon='material-icons arrow_forward'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className='listPaging' style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span>
|
||||
{globalize.translate('ListPaging', recordsStart, recordsEnd, totalRecordCount)}
|
||||
</span>
|
||||
{showControls && (
|
||||
<>
|
||||
<IconButtonElement
|
||||
is='paper-icon-button-light'
|
||||
className='btnPreviousPage autoSize'
|
||||
icon='material-icons arrow_back'
|
||||
/>
|
||||
<IconButtonElement
|
||||
is='paper-icon-button-light'
|
||||
className='btnNextPage autoSize'
|
||||
icon='material-icons arrow_forward'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -16,13 +16,17 @@ const SelectView: FC<SelectViewProps> = ({
|
|||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const showViewSettingsMenu = useCallback(() => {
|
||||
import('../viewSettings/viewSettings').then(({default: ViewSettings}) => {
|
||||
import('../viewSettings/viewSettings').then(({ default: ViewSettings }) => {
|
||||
const viewsettings = new ViewSettings();
|
||||
viewsettings.show({
|
||||
settings: viewQuerySettings,
|
||||
visibleSettings: getVisibleViewSettings(),
|
||||
setviewsettings: setViewQuerySettings
|
||||
}).catch(() => {
|
||||
// view settings closed
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[SelectView] failed to load view settings', err);
|
||||
});
|
||||
}, [getVisibleViewSettings, viewQuerySettings, setViewQuerySettings]);
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ const Shuffle: FC<ShuffleProps> = ({ itemsResult = {}, topParentId }) => {
|
|||
topParentId as string
|
||||
).then((item) => {
|
||||
playbackManager.shuffle(item);
|
||||
}).catch(err => {
|
||||
console.error('[Shuffle] failed to fetch items', err);
|
||||
});
|
||||
}, [topParentId]);
|
||||
|
||||
|
|
|
@ -19,13 +19,17 @@ const Sort: FC<SortProps> = ({
|
|||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const showSortMenu = useCallback(() => {
|
||||
import('../sortmenu/sortmenu').then(({default: SortMenu}) => {
|
||||
import('../sortmenu/sortmenu').then(({ default: SortMenu }) => {
|
||||
const sortMenu = new SortMenu();
|
||||
sortMenu.show({
|
||||
settings: viewQuerySettings,
|
||||
sortOptions: getSortMenuOptions(),
|
||||
setSortValues: setViewQuerySettings
|
||||
}).catch(() => {
|
||||
// sort menu closed
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[Sort] failed to load sort menu', err);
|
||||
});
|
||||
}, [getSortMenuOptions, viewQuerySettings, setViewQuerySettings]);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { type BaseItemDtoQueryResult, ItemFields, ItemFilter } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import loading from '../loading/loading';
|
||||
|
@ -33,6 +33,41 @@ const getDefaultSortBy = () => {
|
|||
return 'SortName';
|
||||
};
|
||||
|
||||
const getFields = (viewQuerySettings: ViewQuerySettings) => {
|
||||
const fields: ItemFields[] = [
|
||||
ItemFields.BasicSyncInfo,
|
||||
ItemFields.MediaSourceCount
|
||||
];
|
||||
|
||||
if (viewQuerySettings.imageType === 'primary') {
|
||||
fields.push(ItemFields.PrimaryImageAspectRatio);
|
||||
}
|
||||
|
||||
return fields.join(',');
|
||||
};
|
||||
|
||||
const getFilters = (viewQuerySettings: ViewQuerySettings) => {
|
||||
const filters: ItemFilter[] = [];
|
||||
|
||||
if (viewQuerySettings.IsPlayed) {
|
||||
filters.push(ItemFilter.IsPlayed);
|
||||
}
|
||||
|
||||
if (viewQuerySettings.IsUnplayed) {
|
||||
filters.push(ItemFilter.IsUnplayed);
|
||||
}
|
||||
|
||||
if (viewQuerySettings.IsFavorite) {
|
||||
filters.push(ItemFilter.IsFavorite);
|
||||
}
|
||||
|
||||
if (viewQuerySettings.IsResumable) {
|
||||
filters.push(ItemFilter.IsResumable);
|
||||
}
|
||||
|
||||
return filters;
|
||||
};
|
||||
|
||||
const getVisibleViewSettings = () => {
|
||||
return [
|
||||
'showTitle',
|
||||
|
@ -228,33 +263,7 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
|
|||
}, [getCardOptions, getContext, itemsResult.Items, getNoItemsMessage, viewQuerySettings.imageType]);
|
||||
|
||||
const getQuery = useCallback(() => {
|
||||
let fields = 'BasicSyncInfo,MediaSourceCount';
|
||||
|
||||
if (viewQuerySettings.imageType === 'primary') {
|
||||
fields += ',PrimaryImageAspectRatio';
|
||||
}
|
||||
|
||||
if (viewQuerySettings.showYear) {
|
||||
fields += ',ProductionYear';
|
||||
}
|
||||
|
||||
const queryFilters: string[] = [];
|
||||
|
||||
if (viewQuerySettings.IsPlayed) {
|
||||
queryFilters.push('IsPlayed');
|
||||
}
|
||||
|
||||
if (viewQuerySettings.IsUnplayed) {
|
||||
queryFilters.push('IsUnplayed');
|
||||
}
|
||||
|
||||
if (viewQuerySettings.IsFavorite) {
|
||||
queryFilters.push('IsFavorite');
|
||||
}
|
||||
|
||||
if (viewQuerySettings.IsResumable) {
|
||||
queryFilters.push('IsResumable');
|
||||
}
|
||||
const queryFilters = getFilters(viewQuerySettings);
|
||||
|
||||
let queryIsHD;
|
||||
|
||||
|
@ -271,10 +280,10 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
|
|||
SortOrder: viewQuerySettings.SortOrder,
|
||||
IncludeItemTypes: getItemTypes().join(','),
|
||||
Recursive: true,
|
||||
Fields: fields,
|
||||
Fields: getFields(viewQuerySettings),
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo',
|
||||
Limit: userSettings.libraryPageSize(undefined),
|
||||
Limit: userSettings.libraryPageSize(undefined) || undefined,
|
||||
IsFavorite: getBasekey() === 'favorites' ? true : null,
|
||||
VideoTypes: viewQuerySettings.VideoTypes,
|
||||
GenreIds: viewQuerySettings.GenreIds,
|
||||
|
@ -293,28 +302,7 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
|
|||
ParentId: topParentId
|
||||
};
|
||||
}, [
|
||||
viewQuerySettings.imageType,
|
||||
viewQuerySettings.showYear,
|
||||
viewQuerySettings.IsPlayed,
|
||||
viewQuerySettings.IsUnplayed,
|
||||
viewQuerySettings.IsFavorite,
|
||||
viewQuerySettings.IsResumable,
|
||||
viewQuerySettings.IsHD,
|
||||
viewQuerySettings.IsSD,
|
||||
viewQuerySettings.SortBy,
|
||||
viewQuerySettings.SortOrder,
|
||||
viewQuerySettings.VideoTypes,
|
||||
viewQuerySettings.GenreIds,
|
||||
viewQuerySettings.Is4K,
|
||||
viewQuerySettings.Is3D,
|
||||
viewQuerySettings.HasSubtitles,
|
||||
viewQuerySettings.HasTrailer,
|
||||
viewQuerySettings.HasSpecialFeature,
|
||||
viewQuerySettings.HasThemeSong,
|
||||
viewQuerySettings.HasThemeVideo,
|
||||
viewQuerySettings.StartIndex,
|
||||
viewQuerySettings.NameLessThan,
|
||||
viewQuerySettings.NameStartsWith,
|
||||
viewQuerySettings,
|
||||
getItemTypes,
|
||||
getBasekey,
|
||||
topParentId
|
||||
|
@ -347,9 +335,13 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
|
|||
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
}).catch(err => {
|
||||
console.error('[ViewItemsContainer] failed to load autofocuser', err);
|
||||
});
|
||||
loading.hide();
|
||||
setisLoading(true);
|
||||
}).catch(err => {
|
||||
console.error('[ViewItemsContainer] failed to fetch data', err);
|
||||
});
|
||||
}, [fetchData]);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { appRouter } from '../appRouter';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import browser from '../../scripts/browser';
|
||||
import dialog from '../dialog/dialog';
|
||||
import globalize from '../../scripts/globalize';
|
||||
|
|
|
@ -12,9 +12,9 @@ type IProps = {
|
|||
listTitle?: string;
|
||||
description?: string;
|
||||
children?: React.ReactNode
|
||||
}
|
||||
};
|
||||
|
||||
const AccessContainer: FunctionComponent<IProps> = ({containerClassName, headerTitle, checkBoxClassName, checkBoxTitle, listContainerClassName, accessClassName, listTitle, description, children }: IProps) => {
|
||||
const AccessContainer: FunctionComponent<IProps> = ({ containerClassName, headerTitle, checkBoxClassName, checkBoxTitle, listContainerClassName, accessClassName, listTitle, description, children }: IProps) => {
|
||||
return (
|
||||
<div className={containerClassName}>
|
||||
<h2>{globalize.translate(headerTitle)}</h2>
|
||||
|
|
|
@ -9,7 +9,7 @@ type AccessScheduleListProps = {
|
|||
DayOfWeek?: string;
|
||||
StartHour?: number ;
|
||||
EndHour?: number;
|
||||
}
|
||||
};
|
||||
|
||||
function getDisplayTime(hours = 0) {
|
||||
let minutes = 0;
|
||||
|
@ -22,7 +22,7 @@ function getDisplayTime(hours = 0) {
|
|||
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
|
||||
}
|
||||
|
||||
const AccessScheduleList: FunctionComponent<AccessScheduleListProps> = ({index, DayOfWeek, StartHour, EndHour}: AccessScheduleListProps) => {
|
||||
const AccessScheduleList: FunctionComponent<AccessScheduleListProps> = ({ index, DayOfWeek, StartHour, EndHour }: AccessScheduleListProps) => {
|
||||
return (
|
||||
<div
|
||||
className='liSchedule listItem'
|
||||
|
|
|
@ -3,9 +3,9 @@ import IconButtonElement from '../../../elements/IconButtonElement';
|
|||
|
||||
type IProps = {
|
||||
tag?: string;
|
||||
}
|
||||
};
|
||||
|
||||
const BlockedTagList: FunctionComponent<IProps> = ({tag}: IProps) => {
|
||||
const BlockedTagList: FunctionComponent<IProps> = ({ tag }: IProps) => {
|
||||
return (
|
||||
<div className='paperList'>
|
||||
<div className='listItem'>
|
||||
|
|
|
@ -4,7 +4,7 @@ import globalize from '../../../scripts/globalize';
|
|||
type IProps = {
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
};
|
||||
|
||||
const createLinkElement = ({ className, title }: IProps) => ({
|
||||
__html: `<a
|
||||
|
|
|
@ -3,7 +3,7 @@ import globalize from '../../../scripts/globalize';
|
|||
|
||||
type IProps = {
|
||||
activeTab: string;
|
||||
}
|
||||
};
|
||||
|
||||
const createLinkElement = (activeTab: string) => ({
|
||||
__html: `<a href="#"
|
||||
|
@ -36,7 +36,7 @@ const createLinkElement = (activeTab: string) => ({
|
|||
</a>`
|
||||
});
|
||||
|
||||
const SectionTabs: FunctionComponent<IProps> = ({activeTab}: IProps) => {
|
||||
const SectionTabs: FunctionComponent<IProps> = ({ activeTab }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
data-role='controlgroup'
|
||||
|
|
|
@ -19,7 +19,7 @@ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl
|
|||
|
||||
type IProps = {
|
||||
user?: UserDto;
|
||||
}
|
||||
};
|
||||
|
||||
const getLastSeenText = (lastActivityDate?: string | null) => {
|
||||
if (lastActivityDate) {
|
||||
|
@ -74,7 +74,7 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
|
|||
</div>
|
||||
<div className='cardFooter visualCardBox-cardFooter'>
|
||||
<div
|
||||
style={{textAlign: 'right', float: 'right', paddingTop: '5px'}}
|
||||
style={{ textAlign: 'right', float: 'right', paddingTop: '5px' }}
|
||||
>
|
||||
<IconButtonElement
|
||||
is='paper-icon-button-light'
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react';
|
||||
import Dashboard from '../../../utils/dashboard';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
@ -12,12 +11,12 @@ import InputElement from '../../../elements/InputElement';
|
|||
|
||||
type IProps = {
|
||||
userId: string;
|
||||
}
|
||||
};
|
||||
|
||||
const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
||||
const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const loadUser = useCallback(() => {
|
||||
const loadUser = useCallback(async () => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
|
@ -25,61 +24,50 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
|||
return;
|
||||
}
|
||||
|
||||
window.ApiClient.getUser(userId).then(function (user) {
|
||||
Dashboard.getCurrentUser().then(function (loggedInUser: UserDto) {
|
||||
if (!user.Policy) {
|
||||
throw new Error('Unexpected null user.Policy');
|
||||
}
|
||||
const user = await window.ApiClient.getUser(userId);
|
||||
const loggedInUser = await Dashboard.getCurrentUser();
|
||||
|
||||
if (!user.Configuration) {
|
||||
throw new Error('Unexpected null user.Configuration');
|
||||
}
|
||||
if (!user.Policy || !user.Configuration) {
|
||||
throw new Error('Unexpected null user policy or configuration');
|
||||
}
|
||||
|
||||
LibraryMenu.setTitle(user.Name);
|
||||
LibraryMenu.setTitle(user.Name);
|
||||
|
||||
let showLocalAccessSection = false;
|
||||
let showLocalAccessSection = false;
|
||||
|
||||
if (user.HasConfiguredPassword) {
|
||||
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide');
|
||||
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide');
|
||||
showLocalAccessSection = true;
|
||||
} else {
|
||||
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.add('hide');
|
||||
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide');
|
||||
}
|
||||
if (user.HasConfiguredPassword) {
|
||||
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide');
|
||||
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide');
|
||||
showLocalAccessSection = true;
|
||||
} else {
|
||||
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.add('hide');
|
||||
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide');
|
||||
}
|
||||
|
||||
if (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess) {
|
||||
(page.querySelector('.passwordSection') as HTMLDivElement).classList.remove('hide');
|
||||
} else {
|
||||
(page.querySelector('.passwordSection') as HTMLDivElement).classList.add('hide');
|
||||
}
|
||||
const canChangePassword = loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess;
|
||||
(page.querySelector('.passwordSection') as HTMLDivElement).classList.toggle('hide', !canChangePassword);
|
||||
(page.querySelector('.localAccessSection') as HTMLDivElement).classList.toggle('hide', !(showLocalAccessSection && canChangePassword));
|
||||
|
||||
if (showLocalAccessSection && (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
|
||||
(page.querySelector('.localAccessSection') as HTMLDivElement).classList.remove('hide');
|
||||
} else {
|
||||
(page.querySelector('.localAccessSection') as HTMLDivElement).classList.add('hide');
|
||||
}
|
||||
const txtEasyPassword = page.querySelector('#txtEasyPassword') as HTMLInputElement;
|
||||
txtEasyPassword.value = '';
|
||||
|
||||
const txtEasyPassword = page.querySelector('#txtEasyPassword') as HTMLInputElement;
|
||||
txtEasyPassword.value = '';
|
||||
if (user.HasConfiguredEasyPassword) {
|
||||
txtEasyPassword.placeholder = '******';
|
||||
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.remove('hide');
|
||||
} else {
|
||||
txtEasyPassword.removeAttribute('placeholder');
|
||||
txtEasyPassword.placeholder = '';
|
||||
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.add('hide');
|
||||
}
|
||||
|
||||
if (user.HasConfiguredEasyPassword) {
|
||||
txtEasyPassword.placeholder = '******';
|
||||
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.remove('hide');
|
||||
} else {
|
||||
txtEasyPassword.removeAttribute('placeholder');
|
||||
txtEasyPassword.placeholder = '';
|
||||
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.add('hide');
|
||||
}
|
||||
const chkEnableLocalEasyPassword = page.querySelector('.chkEnableLocalEasyPassword') as HTMLInputElement;
|
||||
|
||||
const chkEnableLocalEasyPassword = page.querySelector('.chkEnableLocalEasyPassword') as HTMLInputElement;
|
||||
chkEnableLocalEasyPassword.checked = user.Configuration.EnableLocalPassword || false;
|
||||
|
||||
chkEnableLocalEasyPassword.checked = user.Configuration.EnableLocalPassword || false;
|
||||
|
||||
import('../../autoFocuser').then(({default: autoFocuser}) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
});
|
||||
});
|
||||
import('../../autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
}).catch(err => {
|
||||
console.error('[UserPasswordForm] failed to load autofocuser', err);
|
||||
});
|
||||
|
||||
(page.querySelector('#txtCurrentPassword') as HTMLInputElement).value = '';
|
||||
|
@ -95,7 +83,9 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
|||
return;
|
||||
}
|
||||
|
||||
loadUser();
|
||||
loadUser().catch(err => {
|
||||
console.error('[UserPasswordForm] failed to load user', err);
|
||||
});
|
||||
|
||||
const onSubmit = (e: Event) => {
|
||||
if ((page.querySelector('#txtNewPassword') as HTMLInputElement).value != (page.querySelector('#txtNewPasswordConfirm') as HTMLInputElement).value) {
|
||||
|
@ -123,7 +113,9 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
|||
loading.hide();
|
||||
toast(globalize.translate('PasswordSaved'));
|
||||
|
||||
loadUser();
|
||||
loadUser().catch(err => {
|
||||
console.error('[UserPasswordForm] failed to load user', err);
|
||||
});
|
||||
}, function () {
|
||||
loading.hide();
|
||||
Dashboard.alert({
|
||||
|
@ -146,6 +138,8 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
|||
if (easyPassword) {
|
||||
window.ApiClient.updateEasyPassword(userId, easyPassword).then(function () {
|
||||
onEasyPasswordSaved();
|
||||
}).catch(err => {
|
||||
console.error('[UserPasswordForm] failed to update easy password', err);
|
||||
});
|
||||
} else {
|
||||
onEasyPasswordSaved();
|
||||
|
@ -167,8 +161,14 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
|||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
|
||||
loadUser();
|
||||
loadUser().catch(err => {
|
||||
console.error('[UserPasswordForm] failed to load user', err);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[UserPasswordForm] failed to update user configuration', err);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[UserPasswordForm] failed to fetch user', err);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -183,8 +183,14 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
|||
message: globalize.translate('PinCodeResetComplete'),
|
||||
title: globalize.translate('HeaderPinCodeReset')
|
||||
});
|
||||
loadUser();
|
||||
loadUser().catch(err => {
|
||||
console.error('[UserPasswordForm] failed to load user', err);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[UserPasswordForm] failed to reset easy password', err);
|
||||
});
|
||||
}).catch(() => {
|
||||
// confirm dialog was closed
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -198,8 +204,14 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
|||
message: globalize.translate('PasswordResetComplete'),
|
||||
title: globalize.translate('ResetPassword')
|
||||
});
|
||||
loadUser();
|
||||
loadUser().catch(err => {
|
||||
console.error('[UserPasswordForm] failed to load user', err);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[UserPasswordForm] failed to reset user password', err);
|
||||
});
|
||||
}).catch(() => {
|
||||
// confirm dialog was closed
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -214,7 +226,7 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
|||
<div ref={element}>
|
||||
<form
|
||||
className='updatePasswordForm passwordSection hide'
|
||||
style={{margin: '0 auto 2em'}}
|
||||
style={{ margin: '0 auto 2em' }}
|
||||
>
|
||||
<div className='detailSection'>
|
||||
<div id='fldCurrentPassword' className='inputContainer hide'>
|
||||
|
@ -260,7 +272,7 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
|||
<br />
|
||||
<form
|
||||
className='localAccessForm localAccessSection'
|
||||
style={{margin: '0 auto'}}
|
||||
style={{ margin: '0 auto' }}
|
||||
>
|
||||
<div className='detailSection'>
|
||||
<div className='detailSectionHeader'>
|
||||
|
|
|
@ -10,132 +10,129 @@ import '../../elements/emby-button/emby-button';
|
|||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../../elements/emby-input/emby-input';
|
||||
import '../formdialog.scss';
|
||||
import '../../assets/css/flexstyles.scss';
|
||||
import '../../styles/flexstyles.scss';
|
||||
import template from './dialog.template.html';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function showDialog(options = { dialogOptions: {}, buttons: [] }) {
|
||||
const dialogOptions = {
|
||||
removeOnClose: true,
|
||||
scrollY: false,
|
||||
...options.dialogOptions
|
||||
};
|
||||
|
||||
function showDialog(options = { dialogOptions: {}, buttons: [] }) {
|
||||
const dialogOptions = {
|
||||
removeOnClose: true,
|
||||
scrollY: false,
|
||||
...options.dialogOptions
|
||||
};
|
||||
const enableTvLayout = layoutManager.tv;
|
||||
|
||||
const enableTvLayout = layoutManager.tv;
|
||||
if (enableTvLayout) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
}
|
||||
|
||||
if (enableTvLayout) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.classList.add('formDialog');
|
||||
|
||||
dlg.innerHTML = globalize.translateHtml(template, 'core');
|
||||
|
||||
dlg.classList.add('align-items-center');
|
||||
dlg.classList.add('justify-content-center');
|
||||
const formDialogContent = dlg.querySelector('.formDialogContent');
|
||||
formDialogContent.classList.add('no-grow');
|
||||
|
||||
if (enableTvLayout) {
|
||||
formDialogContent.style['max-width'] = '50%';
|
||||
formDialogContent.style['max-height'] = '60%';
|
||||
scrollHelper.centerFocus.on(formDialogContent, false);
|
||||
} else {
|
||||
formDialogContent.style.maxWidth = `${Math.min((options.buttons.length * 150) + 200, dom.getWindowSize().innerWidth - 50)}px`;
|
||||
dlg.classList.add('dialog-fullscreen-lowres');
|
||||
}
|
||||
|
||||
if (options.title) {
|
||||
dlg.querySelector('.formDialogHeaderTitle').innerText = options.title || '';
|
||||
} else {
|
||||
dlg.querySelector('.formDialogHeaderTitle').classList.add('hide');
|
||||
}
|
||||
|
||||
const displayText = options.html || options.text || '';
|
||||
dlg.querySelector('.text').innerHTML = DOMPurify.sanitize(displayText);
|
||||
|
||||
if (!displayText) {
|
||||
dlg.querySelector('.dialogContentInner').classList.add('hide');
|
||||
}
|
||||
|
||||
let i;
|
||||
let length;
|
||||
let html = '';
|
||||
let hasDescriptions = false;
|
||||
|
||||
for (i = 0, length = options.buttons.length; i < length; i++) {
|
||||
const item = options.buttons[i];
|
||||
const autoFocus = i === 0 ? ' autofocus' : '';
|
||||
|
||||
let buttonClass = 'btnOption raised formDialogFooterItem formDialogFooterItem-autosize';
|
||||
|
||||
if (item.type) {
|
||||
buttonClass += ` button-${item.type}`;
|
||||
}
|
||||
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.classList.add('formDialog');
|
||||
|
||||
dlg.innerHTML = globalize.translateHtml(template, 'core');
|
||||
|
||||
dlg.classList.add('align-items-center');
|
||||
dlg.classList.add('justify-content-center');
|
||||
const formDialogContent = dlg.querySelector('.formDialogContent');
|
||||
formDialogContent.classList.add('no-grow');
|
||||
|
||||
if (enableTvLayout) {
|
||||
formDialogContent.style['max-width'] = '50%';
|
||||
formDialogContent.style['max-height'] = '60%';
|
||||
scrollHelper.centerFocus.on(formDialogContent, false);
|
||||
} else {
|
||||
formDialogContent.style.maxWidth = `${Math.min((options.buttons.length * 150) + 200, dom.getWindowSize().innerWidth - 50)}px`;
|
||||
dlg.classList.add('dialog-fullscreen-lowres');
|
||||
if (item.description) {
|
||||
hasDescriptions = true;
|
||||
}
|
||||
|
||||
if (options.title) {
|
||||
dlg.querySelector('.formDialogHeaderTitle').innerText = options.title || '';
|
||||
} else {
|
||||
dlg.querySelector('.formDialogHeaderTitle').classList.add('hide');
|
||||
}
|
||||
|
||||
const displayText = options.html || options.text || '';
|
||||
dlg.querySelector('.text').innerHTML = DOMPurify.sanitize(displayText);
|
||||
|
||||
if (!displayText) {
|
||||
dlg.querySelector('.dialogContentInner').classList.add('hide');
|
||||
}
|
||||
|
||||
let i;
|
||||
let length;
|
||||
let html = '';
|
||||
let hasDescriptions = false;
|
||||
|
||||
for (i = 0, length = options.buttons.length; i < length; i++) {
|
||||
const item = options.buttons[i];
|
||||
const autoFocus = i === 0 ? ' autofocus' : '';
|
||||
|
||||
let buttonClass = 'btnOption raised formDialogFooterItem formDialogFooterItem-autosize';
|
||||
|
||||
if (item.type) {
|
||||
buttonClass += ` button-${item.type}`;
|
||||
}
|
||||
|
||||
if (item.description) {
|
||||
hasDescriptions = true;
|
||||
}
|
||||
|
||||
if (hasDescriptions) {
|
||||
buttonClass += ' formDialogFooterItem-vertical formDialogFooterItem-nomarginbottom';
|
||||
}
|
||||
|
||||
html += `<button is="emby-button" type="button" class="${buttonClass}" data-id="${item.id}"${autoFocus}>${escapeHtml(item.name)}</button>`;
|
||||
|
||||
if (item.description) {
|
||||
html += `<div class="formDialogFooterItem formDialogFooterItem-autosize fieldDescription" style="margin-top:.25em!important;margin-bottom:1.25em!important;">${item.description}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
dlg.querySelector('.formDialogFooter').innerHTML = html;
|
||||
|
||||
if (hasDescriptions) {
|
||||
dlg.querySelector('.formDialogFooter').classList.add('formDialogFooter-vertical');
|
||||
buttonClass += ' formDialogFooterItem-vertical formDialogFooterItem-nomarginbottom';
|
||||
}
|
||||
|
||||
let dialogResult;
|
||||
function onButtonClick() {
|
||||
dialogResult = this.getAttribute('data-id');
|
||||
dialogHelper.close(dlg);
|
||||
html += `<button is="emby-button" type="button" class="${buttonClass}" data-id="${item.id}"${autoFocus}>${escapeHtml(item.name)}</button>`;
|
||||
|
||||
if (item.description) {
|
||||
html += `<div class="formDialogFooterItem formDialogFooterItem-autosize fieldDescription" style="margin-top:.25em!important;margin-bottom:1.25em!important;">${item.description}</div>`;
|
||||
}
|
||||
|
||||
const buttons = dlg.querySelectorAll('.btnOption');
|
||||
for (i = 0, length = buttons.length; i < length; i++) {
|
||||
buttons[i].addEventListener('click', onButtonClick);
|
||||
}
|
||||
|
||||
return dialogHelper.open(dlg).then(() => {
|
||||
if (enableTvLayout) {
|
||||
scrollHelper.centerFocus.off(dlg.querySelector('.formDialogContent'), false);
|
||||
}
|
||||
|
||||
if (dialogResult) {
|
||||
return dialogResult;
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function show(text, title) {
|
||||
let options;
|
||||
if (typeof text === 'string') {
|
||||
options = {
|
||||
title: title,
|
||||
text: text
|
||||
};
|
||||
dlg.querySelector('.formDialogFooter').innerHTML = html;
|
||||
|
||||
if (hasDescriptions) {
|
||||
dlg.querySelector('.formDialogFooter').classList.add('formDialogFooter-vertical');
|
||||
}
|
||||
|
||||
let dialogResult;
|
||||
function onButtonClick() {
|
||||
dialogResult = this.getAttribute('data-id');
|
||||
dialogHelper.close(dlg);
|
||||
}
|
||||
|
||||
const buttons = dlg.querySelectorAll('.btnOption');
|
||||
for (i = 0, length = buttons.length; i < length; i++) {
|
||||
buttons[i].addEventListener('click', onButtonClick);
|
||||
}
|
||||
|
||||
return dialogHelper.open(dlg).then(() => {
|
||||
if (enableTvLayout) {
|
||||
scrollHelper.centerFocus.off(dlg.querySelector('.formDialogContent'), false);
|
||||
}
|
||||
|
||||
if (dialogResult) {
|
||||
return dialogResult;
|
||||
} else {
|
||||
options = text;
|
||||
return Promise.reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return showDialog(options);
|
||||
export function show(text, title) {
|
||||
let options;
|
||||
if (typeof text === 'string') {
|
||||
options = {
|
||||
title: title,
|
||||
text: text
|
||||
};
|
||||
} else {
|
||||
options = text;
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
return showDialog(options);
|
||||
}
|
||||
|
||||
export default {
|
||||
show: show
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { history } from '../appRouter';
|
||||
import { history } from '../router/appRouter';
|
||||
import focusManager from '../focusManager';
|
||||
import browser from '../../scripts/browser';
|
||||
import layoutManager from '../layoutManager';
|
||||
|
@ -7,508 +7,503 @@ import { toBoolean } from '../../utils/string.ts';
|
|||
import dom from '../../scripts/dom';
|
||||
|
||||
import './dialoghelper.scss';
|
||||
import '../../assets/css/scrollstyles.scss';
|
||||
import '../../styles/scrollstyles.scss';
|
||||
|
||||
/* eslint-disable indent */
|
||||
let globalOnOpenCallback;
|
||||
|
||||
let globalOnOpenCallback;
|
||||
|
||||
function enableAnimation() {
|
||||
// too slow
|
||||
if (browser.tv) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return browser.supportsCssAnimation();
|
||||
function enableAnimation() {
|
||||
// too slow
|
||||
if (browser.tv) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function removeCenterFocus(dlg) {
|
||||
if (layoutManager.tv) {
|
||||
if (dlg.classList.contains('scrollX')) {
|
||||
centerFocus(dlg, true, false);
|
||||
} else if (dlg.classList.contains('smoothScrollY')) {
|
||||
centerFocus(dlg, false, false);
|
||||
}
|
||||
return browser.supportsCssAnimation();
|
||||
}
|
||||
|
||||
function removeCenterFocus(dlg) {
|
||||
if (layoutManager.tv) {
|
||||
if (dlg.classList.contains('scrollX')) {
|
||||
centerFocus(dlg, true, false);
|
||||
} else if (dlg.classList.contains('smoothScrollY')) {
|
||||
centerFocus(dlg, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tryRemoveElement(elem) {
|
||||
const parentNode = elem.parentNode;
|
||||
if (parentNode) {
|
||||
// Seeing crashes in edge webview
|
||||
try {
|
||||
parentNode.removeChild(elem);
|
||||
} catch (err) {
|
||||
console.error('[dialogHelper] error removing dialog element: ' + err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function DialogHashHandler(dlg, hash, resolve) {
|
||||
const self = this;
|
||||
self.originalUrl = window.location.href;
|
||||
const activeElement = document.activeElement;
|
||||
let removeScrollLockOnClose = false;
|
||||
let unlisten;
|
||||
|
||||
function onHashChange({ location }) {
|
||||
const dialogs = location.state?.dialogs || [];
|
||||
const shouldClose = !dialogs.includes(hash);
|
||||
|
||||
if ((shouldClose || !isOpened(dlg)) && unlisten) {
|
||||
unlisten();
|
||||
unlisten = null;
|
||||
}
|
||||
|
||||
if (shouldClose) {
|
||||
close(dlg);
|
||||
}
|
||||
}
|
||||
|
||||
function tryRemoveElement(elem) {
|
||||
const parentNode = elem.parentNode;
|
||||
if (parentNode) {
|
||||
// Seeing crashes in edge webview
|
||||
try {
|
||||
parentNode.removeChild(elem);
|
||||
} catch (err) {
|
||||
console.error('[dialogHelper] error removing dialog element: ' + err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function DialogHashHandler(dlg, hash, resolve) {
|
||||
const self = this;
|
||||
self.originalUrl = window.location.href;
|
||||
const activeElement = document.activeElement;
|
||||
let removeScrollLockOnClose = false;
|
||||
let unlisten;
|
||||
|
||||
function onHashChange({ location }) {
|
||||
const dialogs = location.state?.dialogs || [];
|
||||
const shouldClose = !dialogs.includes(hash);
|
||||
|
||||
if ((shouldClose || !isOpened(dlg)) && unlisten) {
|
||||
unlisten();
|
||||
unlisten = null;
|
||||
}
|
||||
|
||||
if (shouldClose) {
|
||||
close(dlg);
|
||||
}
|
||||
function finishClose() {
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
unlisten = null;
|
||||
}
|
||||
|
||||
function finishClose() {
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
unlisten = null;
|
||||
}
|
||||
|
||||
dlg.dispatchEvent(new CustomEvent('close', {
|
||||
bubbles: false,
|
||||
cancelable: false
|
||||
}));
|
||||
|
||||
resolve({
|
||||
element: dlg
|
||||
});
|
||||
}
|
||||
|
||||
function onBackCommand(e) {
|
||||
if (e.detail.command === 'back') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
close(dlg);
|
||||
}
|
||||
}
|
||||
|
||||
function onDialogClosed() {
|
||||
if (!isHistoryEnabled(dlg)) {
|
||||
inputManager.off(dlg, onBackCommand);
|
||||
}
|
||||
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
unlisten = null;
|
||||
}
|
||||
|
||||
removeBackdrop(dlg);
|
||||
dlg.classList.remove('opened');
|
||||
|
||||
if (removeScrollLockOnClose) {
|
||||
document.body.classList.remove('noScroll');
|
||||
}
|
||||
|
||||
if (isHistoryEnabled(dlg)) {
|
||||
const state = history.location.state || {};
|
||||
if (state.dialogs?.length > 0) {
|
||||
if (state.dialogs[state.dialogs.length - 1] === hash) {
|
||||
unlisten = history.listen(finishClose);
|
||||
history.back();
|
||||
} else if (state.dialogs.includes(hash)) {
|
||||
console.warn('[dialogHelper] dialog "%s" was closed, but is not the last dialog opened', hash);
|
||||
|
||||
unlisten = history.listen(finishClose);
|
||||
|
||||
// Remove the closed dialog hash from the history state
|
||||
history.replace(
|
||||
`${history.location.pathname}${history.location.search}`,
|
||||
{
|
||||
...state,
|
||||
dialogs: state.dialogs.filter(dialog => dialog !== hash)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (layoutManager.tv) {
|
||||
focusManager.focus(activeElement);
|
||||
}
|
||||
|
||||
if (toBoolean(dlg.getAttribute('data-removeonclose'), true)) {
|
||||
removeCenterFocus(dlg);
|
||||
|
||||
const dialogContainer = dlg.dialogContainer;
|
||||
if (dialogContainer) {
|
||||
tryRemoveElement(dialogContainer);
|
||||
dlg.dialogContainer = null;
|
||||
} else {
|
||||
tryRemoveElement(dlg);
|
||||
}
|
||||
}
|
||||
|
||||
if (!unlisten) {
|
||||
finishClose();
|
||||
}
|
||||
}
|
||||
|
||||
dlg.addEventListener('_close', onDialogClosed);
|
||||
|
||||
const center = !dlg.classList.contains('dialog-fixedSize');
|
||||
if (center) {
|
||||
dlg.classList.add('centeredDialog');
|
||||
}
|
||||
|
||||
dlg.classList.remove('hide');
|
||||
|
||||
addBackdropOverlay(dlg);
|
||||
|
||||
dlg.classList.add('opened');
|
||||
dlg.dispatchEvent(new CustomEvent('open', {
|
||||
dlg.dispatchEvent(new CustomEvent('close', {
|
||||
bubbles: false,
|
||||
cancelable: false
|
||||
}));
|
||||
|
||||
if (dlg.getAttribute('data-lockscroll') === 'true' && !document.body.classList.contains('noScroll')) {
|
||||
document.body.classList.add('noScroll');
|
||||
removeScrollLockOnClose = true;
|
||||
resolve({
|
||||
element: dlg
|
||||
});
|
||||
}
|
||||
|
||||
function onBackCommand(e) {
|
||||
if (e.detail.command === 'back') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
close(dlg);
|
||||
}
|
||||
}
|
||||
|
||||
function onDialogClosed() {
|
||||
if (!isHistoryEnabled(dlg)) {
|
||||
inputManager.off(dlg, onBackCommand);
|
||||
}
|
||||
|
||||
animateDialogOpen(dlg);
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
unlisten = null;
|
||||
}
|
||||
|
||||
removeBackdrop(dlg);
|
||||
dlg.classList.remove('opened');
|
||||
|
||||
if (removeScrollLockOnClose) {
|
||||
document.body.classList.remove('noScroll');
|
||||
}
|
||||
|
||||
if (isHistoryEnabled(dlg)) {
|
||||
const state = history.location.state || {};
|
||||
const dialogs = state.dialogs || [];
|
||||
// Add new dialog to the list of open dialogs
|
||||
dialogs.push(hash);
|
||||
if (state.dialogs?.length > 0) {
|
||||
if (state.dialogs[state.dialogs.length - 1] === hash) {
|
||||
unlisten = history.listen(finishClose);
|
||||
history.back();
|
||||
} else if (state.dialogs.includes(hash)) {
|
||||
console.warn('[dialogHelper] dialog "%s" was closed, but is not the last dialog opened', hash);
|
||||
|
||||
history.push(
|
||||
`${history.location.pathname}${history.location.search}`,
|
||||
{
|
||||
...state,
|
||||
dialogs
|
||||
unlisten = history.listen(finishClose);
|
||||
|
||||
// Remove the closed dialog hash from the history state
|
||||
history.replace(
|
||||
`${history.location.pathname}${history.location.search}`,
|
||||
{
|
||||
...state,
|
||||
dialogs: state.dialogs.filter(dialog => dialog !== hash)
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
unlisten = history.listen(onHashChange);
|
||||
} else {
|
||||
inputManager.on(dlg, onBackCommand);
|
||||
}
|
||||
}
|
||||
|
||||
function addBackdropOverlay(dlg) {
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.classList.add('dialogBackdrop');
|
||||
|
||||
const backdropParent = dlg.dialogContainer || dlg;
|
||||
backdropParent.parentNode.insertBefore(backdrop, backdropParent);
|
||||
dlg.backdrop = backdrop;
|
||||
|
||||
// trigger reflow or the backdrop will not animate
|
||||
void backdrop.offsetWidth;
|
||||
backdrop.classList.add('dialogBackdropOpened');
|
||||
|
||||
dom.addEventListener((dlg.dialogContainer || backdrop), 'click', e => {
|
||||
if (e.target === dlg.dialogContainer) {
|
||||
close(dlg);
|
||||
}
|
||||
}, {
|
||||
passive: true
|
||||
});
|
||||
}
|
||||
|
||||
dom.addEventListener((dlg.dialogContainer || backdrop), 'contextmenu', e => {
|
||||
if (e.target === dlg.dialogContainer) {
|
||||
// Close the application dialog menu
|
||||
close(dlg);
|
||||
// Prevent the default browser context menu from appearing
|
||||
e.preventDefault();
|
||||
if (layoutManager.tv) {
|
||||
focusManager.focus(activeElement);
|
||||
}
|
||||
|
||||
if (toBoolean(dlg.getAttribute('data-removeonclose'), true)) {
|
||||
removeCenterFocus(dlg);
|
||||
|
||||
const dialogContainer = dlg.dialogContainer;
|
||||
if (dialogContainer) {
|
||||
tryRemoveElement(dialogContainer);
|
||||
dlg.dialogContainer = null;
|
||||
} else {
|
||||
tryRemoveElement(dlg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isHistoryEnabled(dlg) {
|
||||
return dlg.getAttribute('data-history') === 'true';
|
||||
}
|
||||
|
||||
export function open(dlg) {
|
||||
if (globalOnOpenCallback) {
|
||||
globalOnOpenCallback(dlg);
|
||||
}
|
||||
|
||||
const parent = dlg.parentNode;
|
||||
if (parent) {
|
||||
parent.removeChild(dlg);
|
||||
if (!unlisten) {
|
||||
finishClose();
|
||||
}
|
||||
|
||||
const dialogContainer = document.createElement('div');
|
||||
dialogContainer.classList.add('dialogContainer');
|
||||
dialogContainer.appendChild(dlg);
|
||||
dlg.dialogContainer = dialogContainer;
|
||||
document.body.appendChild(dialogContainer);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
new DialogHashHandler(dlg, `dlg${new Date().getTime()}`, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function isOpened(dlg) {
|
||||
//return dlg.opened;
|
||||
return !dlg.classList.contains('hide');
|
||||
dlg.addEventListener('_close', onDialogClosed);
|
||||
|
||||
const center = !dlg.classList.contains('dialog-fixedSize');
|
||||
if (center) {
|
||||
dlg.classList.add('centeredDialog');
|
||||
}
|
||||
|
||||
export function close(dlg) {
|
||||
if (!dlg.classList.contains('hide')) {
|
||||
dlg.dispatchEvent(new CustomEvent('closing', {
|
||||
dlg.classList.remove('hide');
|
||||
|
||||
addBackdropOverlay(dlg);
|
||||
|
||||
dlg.classList.add('opened');
|
||||
dlg.dispatchEvent(new CustomEvent('open', {
|
||||
bubbles: false,
|
||||
cancelable: false
|
||||
}));
|
||||
|
||||
if (dlg.getAttribute('data-lockscroll') === 'true' && !document.body.classList.contains('noScroll')) {
|
||||
document.body.classList.add('noScroll');
|
||||
removeScrollLockOnClose = true;
|
||||
}
|
||||
|
||||
animateDialogOpen(dlg);
|
||||
|
||||
if (isHistoryEnabled(dlg)) {
|
||||
const state = history.location.state || {};
|
||||
const dialogs = state.dialogs || [];
|
||||
// Add new dialog to the list of open dialogs
|
||||
dialogs.push(hash);
|
||||
|
||||
history.push(
|
||||
`${history.location.pathname}${history.location.search}`,
|
||||
{
|
||||
...state,
|
||||
dialogs
|
||||
}
|
||||
);
|
||||
|
||||
unlisten = history.listen(onHashChange);
|
||||
} else {
|
||||
inputManager.on(dlg, onBackCommand);
|
||||
}
|
||||
}
|
||||
|
||||
function addBackdropOverlay(dlg) {
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.classList.add('dialogBackdrop');
|
||||
|
||||
const backdropParent = dlg.dialogContainer || dlg;
|
||||
backdropParent.parentNode.insertBefore(backdrop, backdropParent);
|
||||
dlg.backdrop = backdrop;
|
||||
|
||||
// trigger reflow or the backdrop will not animate
|
||||
void backdrop.offsetWidth;
|
||||
backdrop.classList.add('dialogBackdropOpened');
|
||||
|
||||
dom.addEventListener((dlg.dialogContainer || backdrop), 'click', e => {
|
||||
if (e.target === dlg.dialogContainer) {
|
||||
close(dlg);
|
||||
}
|
||||
}, {
|
||||
passive: true
|
||||
});
|
||||
|
||||
dom.addEventListener((dlg.dialogContainer || backdrop), 'contextmenu', e => {
|
||||
if (e.target === dlg.dialogContainer) {
|
||||
// Close the application dialog menu
|
||||
close(dlg);
|
||||
// Prevent the default browser context menu from appearing
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isHistoryEnabled(dlg) {
|
||||
return dlg.getAttribute('data-history') === 'true';
|
||||
}
|
||||
|
||||
export function open(dlg) {
|
||||
if (globalOnOpenCallback) {
|
||||
globalOnOpenCallback(dlg);
|
||||
}
|
||||
|
||||
const parent = dlg.parentNode;
|
||||
if (parent) {
|
||||
parent.removeChild(dlg);
|
||||
}
|
||||
|
||||
const dialogContainer = document.createElement('div');
|
||||
dialogContainer.classList.add('dialogContainer');
|
||||
dialogContainer.appendChild(dlg);
|
||||
dlg.dialogContainer = dialogContainer;
|
||||
document.body.appendChild(dialogContainer);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
new DialogHashHandler(dlg, `dlg${new Date().getTime()}`, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function isOpened(dlg) {
|
||||
return !dlg.classList.contains('hide');
|
||||
}
|
||||
|
||||
export function close(dlg) {
|
||||
if (!dlg.classList.contains('hide')) {
|
||||
dlg.dispatchEvent(new CustomEvent('closing', {
|
||||
bubbles: false,
|
||||
cancelable: false
|
||||
}));
|
||||
|
||||
const onAnimationFinish = () => {
|
||||
focusManager.popScope(dlg);
|
||||
|
||||
dlg.classList.add('hide');
|
||||
dlg.dispatchEvent(new CustomEvent('_close', {
|
||||
bubbles: false,
|
||||
cancelable: false
|
||||
}));
|
||||
};
|
||||
|
||||
const onAnimationFinish = () => {
|
||||
focusManager.popScope(dlg);
|
||||
|
||||
dlg.classList.add('hide');
|
||||
dlg.dispatchEvent(new CustomEvent('_close', {
|
||||
bubbles: false,
|
||||
cancelable: false
|
||||
}));
|
||||
};
|
||||
|
||||
animateDialogClose(dlg, onAnimationFinish);
|
||||
}
|
||||
animateDialogClose(dlg, onAnimationFinish);
|
||||
}
|
||||
}
|
||||
|
||||
const getAnimationEndHandler = (dlg, callback) => function handler() {
|
||||
dom.removeEventListener(dlg, dom.whichAnimationEvent(), handler, { once: true });
|
||||
callback();
|
||||
const getAnimationEndHandler = (dlg, callback) => function handler() {
|
||||
dom.removeEventListener(dlg, dom.whichAnimationEvent(), handler, { once: true });
|
||||
callback();
|
||||
};
|
||||
|
||||
function animateDialogOpen(dlg) {
|
||||
const onAnimationFinish = () => {
|
||||
focusManager.pushScope(dlg);
|
||||
|
||||
if (dlg.getAttribute('data-autofocus') === 'true') {
|
||||
focusManager.autoFocus(dlg);
|
||||
}
|
||||
|
||||
if (document.activeElement && !dlg.contains(document.activeElement)) {
|
||||
// Blur foreign element to prevent triggering of an action from the previous scope
|
||||
document.activeElement.blur();
|
||||
}
|
||||
};
|
||||
|
||||
function animateDialogOpen(dlg) {
|
||||
const onAnimationFinish = () => {
|
||||
focusManager.pushScope(dlg);
|
||||
if (enableAnimation()) {
|
||||
dom.addEventListener(
|
||||
dlg,
|
||||
dom.whichAnimationEvent(),
|
||||
getAnimationEndHandler(dlg, onAnimationFinish),
|
||||
{ once: true });
|
||||
|
||||
if (dlg.getAttribute('data-autofocus') === 'true') {
|
||||
focusManager.autoFocus(dlg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.activeElement && !dlg.contains(document.activeElement)) {
|
||||
// Blur foreign element to prevent triggering of an action from the previous scope
|
||||
document.activeElement.blur();
|
||||
}
|
||||
};
|
||||
onAnimationFinish();
|
||||
}
|
||||
|
||||
if (enableAnimation()) {
|
||||
dom.addEventListener(
|
||||
dlg,
|
||||
dom.whichAnimationEvent(),
|
||||
getAnimationEndHandler(dlg, onAnimationFinish),
|
||||
{ once: true });
|
||||
function animateDialogClose(dlg, onAnimationFinish) {
|
||||
if (enableAnimation()) {
|
||||
let animated = true;
|
||||
|
||||
switch (dlg.animationConfig.exit.name) {
|
||||
case 'fadeout':
|
||||
dlg.style.animation = `fadeout ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
|
||||
break;
|
||||
case 'scaledown':
|
||||
dlg.style.animation = `scaledown ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
|
||||
break;
|
||||
case 'slidedown':
|
||||
dlg.style.animation = `slidedown ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
|
||||
break;
|
||||
default:
|
||||
animated = false;
|
||||
break;
|
||||
}
|
||||
|
||||
dom.addEventListener(
|
||||
dlg,
|
||||
dom.whichAnimationEvent(),
|
||||
getAnimationEndHandler(dlg, onAnimationFinish),
|
||||
{ once: true });
|
||||
|
||||
if (animated) {
|
||||
return;
|
||||
}
|
||||
|
||||
onAnimationFinish();
|
||||
}
|
||||
|
||||
function animateDialogClose(dlg, onAnimationFinish) {
|
||||
if (enableAnimation()) {
|
||||
let animated = true;
|
||||
onAnimationFinish();
|
||||
}
|
||||
|
||||
switch (dlg.animationConfig.exit.name) {
|
||||
case 'fadeout':
|
||||
dlg.style.animation = `fadeout ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
|
||||
break;
|
||||
case 'scaledown':
|
||||
dlg.style.animation = `scaledown ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
|
||||
break;
|
||||
case 'slidedown':
|
||||
dlg.style.animation = `slidedown ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
|
||||
break;
|
||||
default:
|
||||
animated = false;
|
||||
break;
|
||||
const supportsOverscrollBehavior = 'overscroll-behavior-y' in document.body.style;
|
||||
|
||||
function shouldLockDocumentScroll(options) {
|
||||
if (options.lockScroll != null) {
|
||||
return options.lockScroll;
|
||||
}
|
||||
|
||||
if (options.size === 'fullscreen') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (supportsOverscrollBehavior && (options.size || !browser.touch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return browser.touch;
|
||||
}
|
||||
|
||||
function removeBackdrop(dlg) {
|
||||
const backdrop = dlg.backdrop;
|
||||
|
||||
if (!backdrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
dlg.backdrop = null;
|
||||
|
||||
const onAnimationFinish = () => {
|
||||
tryRemoveElement(backdrop);
|
||||
};
|
||||
|
||||
if (enableAnimation()) {
|
||||
backdrop.classList.remove('dialogBackdropOpened');
|
||||
|
||||
// this is not firing animationend
|
||||
setTimeout(onAnimationFinish, 300);
|
||||
return;
|
||||
}
|
||||
|
||||
onAnimationFinish();
|
||||
}
|
||||
|
||||
function centerFocus(elem, horiz, on) {
|
||||
import('../../scripts/scrollHelper').then((scrollHelper) => {
|
||||
const fn = on ? 'on' : 'off';
|
||||
scrollHelper.centerFocus[fn](elem, horiz);
|
||||
});
|
||||
}
|
||||
|
||||
export function createDialog(options = {}) {
|
||||
// If there's no native dialog support, use a plain div
|
||||
// Also not working well in samsung tizen browser, content inside not clickable
|
||||
// Just go ahead and always use a plain div because we're seeing issues overlaying absoltutely positioned content over a modal dialog
|
||||
const dlg = document.createElement('div');
|
||||
|
||||
// Add an id so we can access the dialog element
|
||||
if (options.id) {
|
||||
dlg.id = options.id;
|
||||
}
|
||||
|
||||
dlg.classList.add('focuscontainer');
|
||||
dlg.classList.add('hide');
|
||||
|
||||
if (shouldLockDocumentScroll(options)) {
|
||||
dlg.setAttribute('data-lockscroll', 'true');
|
||||
}
|
||||
|
||||
if (options.enableHistory !== false) {
|
||||
dlg.setAttribute('data-history', 'true');
|
||||
}
|
||||
|
||||
// without this safari will scroll the background instead of the dialog contents
|
||||
// but not needed here since this is already on top of an existing dialog
|
||||
// but skip it in IE because it's causing the entire browser to hang
|
||||
// Also have to disable for firefox because it's causing select elements to not be clickable
|
||||
if (options.modal !== false) {
|
||||
dlg.setAttribute('modal', 'modal');
|
||||
}
|
||||
|
||||
if (options.autoFocus !== false) {
|
||||
dlg.setAttribute('data-autofocus', 'true');
|
||||
}
|
||||
|
||||
const defaultEntryAnimation = 'scaleup';
|
||||
const defaultExitAnimation = 'scaledown';
|
||||
const entryAnimation = options.entryAnimation || defaultEntryAnimation;
|
||||
const exitAnimation = options.exitAnimation || defaultExitAnimation;
|
||||
|
||||
// If it's not fullscreen then lower the default animation speed to make it open really fast
|
||||
const entryAnimationDuration = options.entryAnimationDuration || (options.size !== 'fullscreen' ? 180 : 280);
|
||||
const exitAnimationDuration = options.exitAnimationDuration || (options.size !== 'fullscreen' ? 120 : 220);
|
||||
|
||||
dlg.animationConfig = {
|
||||
// scale up
|
||||
'entry': {
|
||||
name: entryAnimation,
|
||||
timing: {
|
||||
duration: entryAnimationDuration,
|
||||
easing: 'ease-out'
|
||||
}
|
||||
|
||||
dom.addEventListener(
|
||||
dlg,
|
||||
dom.whichAnimationEvent(),
|
||||
getAnimationEndHandler(dlg, onAnimationFinish),
|
||||
{ once: true });
|
||||
|
||||
if (animated) {
|
||||
return;
|
||||
},
|
||||
// fade out
|
||||
'exit': {
|
||||
name: exitAnimation,
|
||||
timing: {
|
||||
duration: exitAnimationDuration,
|
||||
easing: 'ease-out',
|
||||
fill: 'both'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onAnimationFinish();
|
||||
dlg.classList.add('dialog');
|
||||
|
||||
if (options.scrollX) {
|
||||
dlg.classList.add('scrollX');
|
||||
dlg.classList.add('smoothScrollX');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg, true, true);
|
||||
}
|
||||
} else if (options.scrollY !== false) {
|
||||
dlg.classList.add('smoothScrollY');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
const supportsOverscrollBehavior = 'overscroll-behavior-y' in document.body.style;
|
||||
|
||||
function shouldLockDocumentScroll(options) {
|
||||
if (options.lockScroll != null) {
|
||||
return options.lockScroll;
|
||||
}
|
||||
|
||||
if (options.size === 'fullscreen') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (supportsOverscrollBehavior && (options.size || !browser.touch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return browser.touch;
|
||||
if (options.removeOnClose) {
|
||||
dlg.setAttribute('data-removeonclose', 'true');
|
||||
}
|
||||
|
||||
function removeBackdrop(dlg) {
|
||||
const backdrop = dlg.backdrop;
|
||||
|
||||
if (!backdrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
dlg.backdrop = null;
|
||||
|
||||
const onAnimationFinish = () => {
|
||||
tryRemoveElement(backdrop);
|
||||
};
|
||||
|
||||
if (enableAnimation()) {
|
||||
backdrop.classList.remove('dialogBackdropOpened');
|
||||
|
||||
// this is not firing animationend
|
||||
setTimeout(onAnimationFinish, 300);
|
||||
return;
|
||||
}
|
||||
|
||||
onAnimationFinish();
|
||||
if (options.size) {
|
||||
dlg.classList.add('dialog-fixedSize');
|
||||
dlg.classList.add(`dialog-${options.size}`);
|
||||
}
|
||||
|
||||
function centerFocus(elem, horiz, on) {
|
||||
import('../../scripts/scrollHelper').then((scrollHelper) => {
|
||||
const fn = on ? 'on' : 'off';
|
||||
scrollHelper.centerFocus[fn](elem, horiz);
|
||||
});
|
||||
if (enableAnimation()) {
|
||||
switch (dlg.animationConfig.entry.name) {
|
||||
case 'fadein':
|
||||
dlg.style.animation = `fadein ${entryAnimationDuration}ms ease-out normal`;
|
||||
break;
|
||||
case 'scaleup':
|
||||
dlg.style.animation = `scaleup ${entryAnimationDuration}ms ease-out normal both`;
|
||||
break;
|
||||
case 'slideup':
|
||||
dlg.style.animation = `slideup ${entryAnimationDuration}ms ease-out normal`;
|
||||
break;
|
||||
case 'slidedown':
|
||||
dlg.style.animation = `slidedown ${entryAnimationDuration}ms ease-out normal`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function createDialog(options = {}) {
|
||||
// If there's no native dialog support, use a plain div
|
||||
// Also not working well in samsung tizen browser, content inside not clickable
|
||||
// Just go ahead and always use a plain div because we're seeing issues overlaying absoltutely positioned content over a modal dialog
|
||||
const dlg = document.createElement('div');
|
||||
return dlg;
|
||||
}
|
||||
|
||||
// Add an id so we can access the dialog element
|
||||
if (options.id) {
|
||||
dlg.id = options.id;
|
||||
}
|
||||
|
||||
dlg.classList.add('focuscontainer');
|
||||
dlg.classList.add('hide');
|
||||
|
||||
if (shouldLockDocumentScroll(options)) {
|
||||
dlg.setAttribute('data-lockscroll', 'true');
|
||||
}
|
||||
|
||||
if (options.enableHistory !== false) {
|
||||
dlg.setAttribute('data-history', 'true');
|
||||
}
|
||||
|
||||
// without this safari will scroll the background instead of the dialog contents
|
||||
// but not needed here since this is already on top of an existing dialog
|
||||
// but skip it in IE because it's causing the entire browser to hang
|
||||
// Also have to disable for firefox because it's causing select elements to not be clickable
|
||||
if (options.modal !== false) {
|
||||
dlg.setAttribute('modal', 'modal');
|
||||
}
|
||||
|
||||
if (options.autoFocus !== false) {
|
||||
dlg.setAttribute('data-autofocus', 'true');
|
||||
}
|
||||
|
||||
const defaultEntryAnimation = 'scaleup';
|
||||
const defaultExitAnimation = 'scaledown';
|
||||
const entryAnimation = options.entryAnimation || defaultEntryAnimation;
|
||||
const exitAnimation = options.exitAnimation || defaultExitAnimation;
|
||||
|
||||
// If it's not fullscreen then lower the default animation speed to make it open really fast
|
||||
const entryAnimationDuration = options.entryAnimationDuration || (options.size !== 'fullscreen' ? 180 : 280);
|
||||
const exitAnimationDuration = options.exitAnimationDuration || (options.size !== 'fullscreen' ? 120 : 220);
|
||||
|
||||
dlg.animationConfig = {
|
||||
// scale up
|
||||
'entry': {
|
||||
name: entryAnimation,
|
||||
timing: {
|
||||
duration: entryAnimationDuration,
|
||||
easing: 'ease-out'
|
||||
}
|
||||
},
|
||||
// fade out
|
||||
'exit': {
|
||||
name: exitAnimation,
|
||||
timing: {
|
||||
duration: exitAnimationDuration,
|
||||
easing: 'ease-out',
|
||||
fill: 'both'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
dlg.classList.add('dialog');
|
||||
|
||||
if (options.scrollX) {
|
||||
dlg.classList.add('scrollX');
|
||||
dlg.classList.add('smoothScrollX');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg, true, true);
|
||||
}
|
||||
} else if (options.scrollY !== false) {
|
||||
dlg.classList.add('smoothScrollY');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.removeOnClose) {
|
||||
dlg.setAttribute('data-removeonclose', 'true');
|
||||
}
|
||||
|
||||
if (options.size) {
|
||||
dlg.classList.add('dialog-fixedSize');
|
||||
dlg.classList.add(`dialog-${options.size}`);
|
||||
}
|
||||
|
||||
if (enableAnimation()) {
|
||||
switch (dlg.animationConfig.entry.name) {
|
||||
case 'fadein':
|
||||
dlg.style.animation = `fadein ${entryAnimationDuration}ms ease-out normal`;
|
||||
break;
|
||||
case 'scaleup':
|
||||
dlg.style.animation = `scaleup ${entryAnimationDuration}ms ease-out normal both`;
|
||||
break;
|
||||
case 'slideup':
|
||||
dlg.style.animation = `slideup ${entryAnimationDuration}ms ease-out normal`;
|
||||
break;
|
||||
case 'slidedown':
|
||||
dlg.style.animation = `slidedown ${entryAnimationDuration}ms ease-out normal`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return dlg;
|
||||
}
|
||||
|
||||
export function setOnOpen(val) {
|
||||
globalOnOpenCallback = val;
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
export function setOnOpen(val) {
|
||||
globalOnOpenCallback = val;
|
||||
}
|
||||
|
||||
export default {
|
||||
open: open,
|
||||
|
|
|
@ -43,7 +43,7 @@ function refreshDirectoryBrowser(page, path, fileOptions, updatePathOnError) {
|
|||
Promise.all(promises).then(
|
||||
responses => {
|
||||
const folders = responses[0];
|
||||
const parentPath = responses[1] || '';
|
||||
const parentPath = (responses[1] ? JSON.parse(responses[1]) : '') || '';
|
||||
let html = '';
|
||||
|
||||
page.querySelector('.results').scrollTop = 0;
|
||||
|
|
|
@ -8,6 +8,7 @@ import datetime from '../../scripts/datetime';
|
|||
import globalize from '../../scripts/globalize';
|
||||
import loading from '../loading/loading';
|
||||
import skinManager from '../../scripts/themeManager';
|
||||
import { PluginType } from '../../types/plugin.ts';
|
||||
import Events from '../../utils/events.ts';
|
||||
import '../../elements/emby-select/emby-select';
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
|
@ -17,238 +18,235 @@ import ServerConnections from '../ServerConnections';
|
|||
import toast from '../toast/toast';
|
||||
import template from './displaySettings.template.html';
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
function fillThemes(select, selectedTheme) {
|
||||
skinManager.getThemes().then(themes => {
|
||||
select.innerHTML = themes.map(t => {
|
||||
return `<option value="${t.id}">${escapeHtml(t.name)}</option>`;
|
||||
}).join('');
|
||||
|
||||
// get default theme
|
||||
const defaultTheme = themes.find(theme => theme.default);
|
||||
|
||||
// set the current theme
|
||||
select.value = selectedTheme || defaultTheme.id;
|
||||
});
|
||||
}
|
||||
|
||||
function loadScreensavers(context, userSettings) {
|
||||
const selectScreensaver = context.querySelector('.selectScreensaver');
|
||||
const options = pluginManager.ofType('screensaver').map(plugin => {
|
||||
return {
|
||||
name: plugin.name,
|
||||
value: plugin.id
|
||||
};
|
||||
});
|
||||
|
||||
options.unshift({
|
||||
name: globalize.translate('None'),
|
||||
value: 'none'
|
||||
});
|
||||
|
||||
selectScreensaver.innerHTML = options.map(o => {
|
||||
return `<option value="${o.value}">${escapeHtml(o.name)}</option>`;
|
||||
function fillThemes(select, selectedTheme) {
|
||||
skinManager.getThemes().then(themes => {
|
||||
select.innerHTML = themes.map(t => {
|
||||
return `<option value="${t.id}">${escapeHtml(t.name)}</option>`;
|
||||
}).join('');
|
||||
|
||||
selectScreensaver.value = userSettings.screensaver();
|
||||
// get default theme
|
||||
const defaultTheme = themes.find(theme => theme.default);
|
||||
|
||||
if (!selectScreensaver.value) {
|
||||
// TODO: set the default instead of none
|
||||
selectScreensaver.value = 'none';
|
||||
}
|
||||
// set the current theme
|
||||
select.value = selectedTheme || defaultTheme.id;
|
||||
});
|
||||
}
|
||||
|
||||
function loadScreensavers(context, userSettings) {
|
||||
const selectScreensaver = context.querySelector('.selectScreensaver');
|
||||
const options = pluginManager.ofType(PluginType.Screensaver).map(plugin => {
|
||||
return {
|
||||
name: plugin.name,
|
||||
value: plugin.id
|
||||
};
|
||||
});
|
||||
|
||||
options.unshift({
|
||||
name: globalize.translate('None'),
|
||||
value: 'none'
|
||||
});
|
||||
|
||||
selectScreensaver.innerHTML = options.map(o => {
|
||||
return `<option value="${o.value}">${escapeHtml(o.name)}</option>`;
|
||||
}).join('');
|
||||
|
||||
selectScreensaver.value = userSettings.screensaver();
|
||||
|
||||
if (!selectScreensaver.value) {
|
||||
// TODO: set the default instead of none
|
||||
selectScreensaver.value = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function showOrHideMissingEpisodesField(context) {
|
||||
if (browser.tizen || browser.web0s) {
|
||||
context.querySelector('.fldDisplayMissingEpisodes').classList.add('hide');
|
||||
return;
|
||||
}
|
||||
|
||||
function showOrHideMissingEpisodesField(context) {
|
||||
if (browser.tizen || browser.web0s) {
|
||||
context.querySelector('.fldDisplayMissingEpisodes').classList.add('hide');
|
||||
return;
|
||||
}
|
||||
context.querySelector('.fldDisplayMissingEpisodes').classList.remove('hide');
|
||||
}
|
||||
|
||||
context.querySelector('.fldDisplayMissingEpisodes').classList.remove('hide');
|
||||
function loadForm(context, user, userSettings) {
|
||||
if (appHost.supports('displaylanguage')) {
|
||||
context.querySelector('.languageSection').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.languageSection').classList.add('hide');
|
||||
}
|
||||
|
||||
function loadForm(context, user, userSettings) {
|
||||
if (appHost.supports('displaylanguage')) {
|
||||
context.querySelector('.languageSection').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.languageSection').classList.add('hide');
|
||||
}
|
||||
|
||||
if (appHost.supports('displaymode')) {
|
||||
context.querySelector('.fldDisplayMode').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.fldDisplayMode').classList.add('hide');
|
||||
}
|
||||
|
||||
if (appHost.supports('externallinks')) {
|
||||
context.querySelector('.learnHowToContributeContainer').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.learnHowToContributeContainer').classList.add('hide');
|
||||
}
|
||||
|
||||
context.querySelector('.selectDashboardThemeContainer').classList.toggle('hide', !user.Policy.IsAdministrator);
|
||||
|
||||
if (appHost.supports('screensaver')) {
|
||||
context.querySelector('.selectScreensaverContainer').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.selectScreensaverContainer').classList.add('hide');
|
||||
}
|
||||
|
||||
if (datetime.supportsLocalization()) {
|
||||
context.querySelector('.fldDateTimeLocale').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.fldDateTimeLocale').classList.add('hide');
|
||||
}
|
||||
|
||||
fillThemes(context.querySelector('#selectTheme'), userSettings.theme());
|
||||
fillThemes(context.querySelector('#selectDashboardTheme'), userSettings.dashboardTheme());
|
||||
|
||||
loadScreensavers(context, userSettings);
|
||||
|
||||
context.querySelector('.chkDisplayMissingEpisodes').checked = user.Configuration.DisplayMissingEpisodes || false;
|
||||
|
||||
context.querySelector('#chkThemeSong').checked = userSettings.enableThemeSongs();
|
||||
context.querySelector('#chkThemeVideo').checked = userSettings.enableThemeVideos();
|
||||
context.querySelector('#chkFadein').checked = userSettings.enableFastFadein();
|
||||
context.querySelector('#chkBlurhash').checked = userSettings.enableBlurhash();
|
||||
context.querySelector('#chkBackdrops').checked = userSettings.enableBackdrops();
|
||||
context.querySelector('#chkDetailsBanner').checked = userSettings.detailsBanner();
|
||||
|
||||
context.querySelector('#chkDisableCustomCss').checked = userSettings.disableCustomCss();
|
||||
context.querySelector('#txtLocalCustomCss').value = userSettings.customCss();
|
||||
|
||||
context.querySelector('#selectLanguage').value = userSettings.language() || '';
|
||||
context.querySelector('.selectDateTimeLocale').value = userSettings.dateTimeLocale() || '';
|
||||
|
||||
context.querySelector('#txtLibraryPageSize').value = userSettings.libraryPageSize();
|
||||
|
||||
context.querySelector('#txtMaxDaysForNextUp').value = userSettings.maxDaysForNextUp();
|
||||
context.querySelector('#chkRewatchingNextUp').checked = userSettings.enableRewatchingInNextUp();
|
||||
context.querySelector('#chkUseEpisodeImagesInNextUp').checked = userSettings.useEpisodeImagesInNextUpAndResume();
|
||||
|
||||
context.querySelector('.selectLayout').value = layoutManager.getSavedLayout() || '';
|
||||
|
||||
showOrHideMissingEpisodesField(context);
|
||||
|
||||
loading.hide();
|
||||
if (appHost.supports('displaymode')) {
|
||||
context.querySelector('.fldDisplayMode').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.fldDisplayMode').classList.add('hide');
|
||||
}
|
||||
|
||||
function saveUser(context, user, userSettingsInstance, apiClient) {
|
||||
user.Configuration.DisplayMissingEpisodes = context.querySelector('.chkDisplayMissingEpisodes').checked;
|
||||
|
||||
if (appHost.supports('displaylanguage')) {
|
||||
userSettingsInstance.language(context.querySelector('#selectLanguage').value);
|
||||
}
|
||||
|
||||
userSettingsInstance.dateTimeLocale(context.querySelector('.selectDateTimeLocale').value);
|
||||
|
||||
userSettingsInstance.enableThemeSongs(context.querySelector('#chkThemeSong').checked);
|
||||
userSettingsInstance.enableThemeVideos(context.querySelector('#chkThemeVideo').checked);
|
||||
userSettingsInstance.theme(context.querySelector('#selectTheme').value);
|
||||
userSettingsInstance.dashboardTheme(context.querySelector('#selectDashboardTheme').value);
|
||||
userSettingsInstance.screensaver(context.querySelector('.selectScreensaver').value);
|
||||
|
||||
userSettingsInstance.libraryPageSize(context.querySelector('#txtLibraryPageSize').value);
|
||||
|
||||
userSettingsInstance.maxDaysForNextUp(context.querySelector('#txtMaxDaysForNextUp').value);
|
||||
userSettingsInstance.enableRewatchingInNextUp(context.querySelector('#chkRewatchingNextUp').checked);
|
||||
userSettingsInstance.useEpisodeImagesInNextUpAndResume(context.querySelector('#chkUseEpisodeImagesInNextUp').checked);
|
||||
|
||||
userSettingsInstance.enableFastFadein(context.querySelector('#chkFadein').checked);
|
||||
userSettingsInstance.enableBlurhash(context.querySelector('#chkBlurhash').checked);
|
||||
userSettingsInstance.enableBackdrops(context.querySelector('#chkBackdrops').checked);
|
||||
userSettingsInstance.detailsBanner(context.querySelector('#chkDetailsBanner').checked);
|
||||
|
||||
userSettingsInstance.disableCustomCss(context.querySelector('#chkDisableCustomCss').checked);
|
||||
userSettingsInstance.customCss(context.querySelector('#txtLocalCustomCss').value);
|
||||
|
||||
if (user.Id === apiClient.getCurrentUserId()) {
|
||||
skinManager.setTheme(userSettingsInstance.theme());
|
||||
}
|
||||
|
||||
layoutManager.setLayout(context.querySelector('.selectLayout').value);
|
||||
return apiClient.updateUserConfiguration(user.Id, user.Configuration);
|
||||
if (appHost.supports('externallinks')) {
|
||||
context.querySelector('.learnHowToContributeContainer').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.learnHowToContributeContainer').classList.add('hide');
|
||||
}
|
||||
|
||||
function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) {
|
||||
context.querySelector('.selectDashboardThemeContainer').classList.toggle('hide', !user.Policy.IsAdministrator);
|
||||
|
||||
if (appHost.supports('screensaver')) {
|
||||
context.querySelector('.selectScreensaverContainer').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.selectScreensaverContainer').classList.add('hide');
|
||||
}
|
||||
|
||||
if (datetime.supportsLocalization()) {
|
||||
context.querySelector('.fldDateTimeLocale').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.fldDateTimeLocale').classList.add('hide');
|
||||
}
|
||||
|
||||
fillThemes(context.querySelector('#selectTheme'), userSettings.theme());
|
||||
fillThemes(context.querySelector('#selectDashboardTheme'), userSettings.dashboardTheme());
|
||||
|
||||
loadScreensavers(context, userSettings);
|
||||
|
||||
context.querySelector('.chkDisplayMissingEpisodes').checked = user.Configuration.DisplayMissingEpisodes || false;
|
||||
|
||||
context.querySelector('#chkThemeSong').checked = userSettings.enableThemeSongs();
|
||||
context.querySelector('#chkThemeVideo').checked = userSettings.enableThemeVideos();
|
||||
context.querySelector('#chkFadein').checked = userSettings.enableFastFadein();
|
||||
context.querySelector('#chkBlurhash').checked = userSettings.enableBlurhash();
|
||||
context.querySelector('#chkBackdrops').checked = userSettings.enableBackdrops();
|
||||
context.querySelector('#chkDetailsBanner').checked = userSettings.detailsBanner();
|
||||
|
||||
context.querySelector('#chkDisableCustomCss').checked = userSettings.disableCustomCss();
|
||||
context.querySelector('#txtLocalCustomCss').value = userSettings.customCss();
|
||||
|
||||
context.querySelector('#selectLanguage').value = userSettings.language() || '';
|
||||
context.querySelector('.selectDateTimeLocale').value = userSettings.dateTimeLocale() || '';
|
||||
|
||||
context.querySelector('#txtLibraryPageSize').value = userSettings.libraryPageSize();
|
||||
|
||||
context.querySelector('#txtMaxDaysForNextUp').value = userSettings.maxDaysForNextUp();
|
||||
context.querySelector('#chkRewatchingNextUp').checked = userSettings.enableRewatchingInNextUp();
|
||||
context.querySelector('#chkUseEpisodeImagesInNextUp').checked = userSettings.useEpisodeImagesInNextUpAndResume();
|
||||
|
||||
context.querySelector('.selectLayout').value = layoutManager.getSavedLayout() || '';
|
||||
|
||||
showOrHideMissingEpisodesField(context);
|
||||
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function saveUser(context, user, userSettingsInstance, apiClient) {
|
||||
user.Configuration.DisplayMissingEpisodes = context.querySelector('.chkDisplayMissingEpisodes').checked;
|
||||
|
||||
if (appHost.supports('displaylanguage')) {
|
||||
userSettingsInstance.language(context.querySelector('#selectLanguage').value);
|
||||
}
|
||||
|
||||
userSettingsInstance.dateTimeLocale(context.querySelector('.selectDateTimeLocale').value);
|
||||
|
||||
userSettingsInstance.enableThemeSongs(context.querySelector('#chkThemeSong').checked);
|
||||
userSettingsInstance.enableThemeVideos(context.querySelector('#chkThemeVideo').checked);
|
||||
userSettingsInstance.theme(context.querySelector('#selectTheme').value);
|
||||
userSettingsInstance.dashboardTheme(context.querySelector('#selectDashboardTheme').value);
|
||||
userSettingsInstance.screensaver(context.querySelector('.selectScreensaver').value);
|
||||
|
||||
userSettingsInstance.libraryPageSize(context.querySelector('#txtLibraryPageSize').value);
|
||||
|
||||
userSettingsInstance.maxDaysForNextUp(context.querySelector('#txtMaxDaysForNextUp').value);
|
||||
userSettingsInstance.enableRewatchingInNextUp(context.querySelector('#chkRewatchingNextUp').checked);
|
||||
userSettingsInstance.useEpisodeImagesInNextUpAndResume(context.querySelector('#chkUseEpisodeImagesInNextUp').checked);
|
||||
|
||||
userSettingsInstance.enableFastFadein(context.querySelector('#chkFadein').checked);
|
||||
userSettingsInstance.enableBlurhash(context.querySelector('#chkBlurhash').checked);
|
||||
userSettingsInstance.enableBackdrops(context.querySelector('#chkBackdrops').checked);
|
||||
userSettingsInstance.detailsBanner(context.querySelector('#chkDetailsBanner').checked);
|
||||
|
||||
userSettingsInstance.disableCustomCss(context.querySelector('#chkDisableCustomCss').checked);
|
||||
userSettingsInstance.customCss(context.querySelector('#txtLocalCustomCss').value);
|
||||
|
||||
if (user.Id === apiClient.getCurrentUserId()) {
|
||||
skinManager.setTheme(userSettingsInstance.theme());
|
||||
}
|
||||
|
||||
layoutManager.setLayout(context.querySelector('.selectLayout').value);
|
||||
return apiClient.updateUserConfiguration(user.Id, user.Configuration);
|
||||
}
|
||||
|
||||
function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) {
|
||||
loading.show();
|
||||
|
||||
apiClient.getUser(userId).then(user => {
|
||||
saveUser(context, user, userSettings, apiClient).then(() => {
|
||||
loading.hide();
|
||||
if (enableSaveConfirmation) {
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
Events.trigger(instance, 'saved');
|
||||
}, () => {
|
||||
loading.hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
const self = this;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
const userId = self.options.userId;
|
||||
const userSettings = self.options.userSettings;
|
||||
|
||||
userSettings.setUserInfo(userId, apiClient).then(() => {
|
||||
const enableSaveConfirmation = self.options.enableSaveConfirmation;
|
||||
save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation);
|
||||
});
|
||||
|
||||
// Disable default form submission
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function embed(options, self) {
|
||||
options.element.innerHTML = globalize.translateHtml(template, 'core');
|
||||
options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self));
|
||||
if (options.enableSaveButton) {
|
||||
options.element.querySelector('.btnSave').classList.remove('hide');
|
||||
}
|
||||
self.loadData(options.autoFocus);
|
||||
}
|
||||
|
||||
class DisplaySettings {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
embed(options, this);
|
||||
}
|
||||
|
||||
loadData(autoFocus) {
|
||||
const self = this;
|
||||
const context = self.options.element;
|
||||
|
||||
loading.show();
|
||||
|
||||
apiClient.getUser(userId).then(user => {
|
||||
saveUser(context, user, userSettings, apiClient).then(() => {
|
||||
loading.hide();
|
||||
if (enableSaveConfirmation) {
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
Events.trigger(instance, 'saved');
|
||||
}, () => {
|
||||
loading.hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
const self = this;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
const userId = self.options.userId;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
const userSettings = self.options.userSettings;
|
||||
|
||||
userSettings.setUserInfo(userId, apiClient).then(() => {
|
||||
const enableSaveConfirmation = self.options.enableSaveConfirmation;
|
||||
save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation);
|
||||
});
|
||||
|
||||
// Disable default form submission
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function embed(options, self) {
|
||||
options.element.innerHTML = globalize.translateHtml(template, 'core');
|
||||
options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self));
|
||||
if (options.enableSaveButton) {
|
||||
options.element.querySelector('.btnSave').classList.remove('hide');
|
||||
}
|
||||
self.loadData(options.autoFocus);
|
||||
}
|
||||
|
||||
class DisplaySettings {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
embed(options, this);
|
||||
}
|
||||
|
||||
loadData(autoFocus) {
|
||||
const self = this;
|
||||
const context = self.options.element;
|
||||
|
||||
loading.show();
|
||||
|
||||
const userId = self.options.userId;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
const userSettings = self.options.userSettings;
|
||||
|
||||
return apiClient.getUser(userId).then(user => {
|
||||
return userSettings.setUserInfo(userId, apiClient).then(() => {
|
||||
self.dataLoaded = true;
|
||||
loadForm(context, user, userSettings);
|
||||
if (autoFocus) {
|
||||
focusManager.autoFocus(context);
|
||||
}
|
||||
});
|
||||
return apiClient.getUser(userId).then(user => {
|
||||
return userSettings.setUserInfo(userId, apiClient).then(() => {
|
||||
self.dataLoaded = true;
|
||||
loadForm(context, user, userSettings);
|
||||
if (autoFocus) {
|
||||
focusManager.autoFocus(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
submit() {
|
||||
onSubmit.call(this);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.options = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
submit() {
|
||||
onSubmit.call(this);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.options = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default DisplaySettings;
|
||||
|
|
|
@ -172,6 +172,7 @@
|
|||
<option value="desktop">${Desktop}</option>
|
||||
<option value="mobile">${Mobile}</option>
|
||||
<option value="tv">${TV}</option>
|
||||
<option value="experimental">${Experimental}</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${DisplayModeHelp}</div>
|
||||
<div class="fieldDescription">${LabelPleaseRestart}</div>
|
||||
|
|
|
@ -6,239 +6,235 @@ import imageLoader from './images/imageLoader';
|
|||
import globalize from '../scripts/globalize';
|
||||
import layoutManager from './layoutManager';
|
||||
import { getParameterByName } from '../utils/url.ts';
|
||||
import '../assets/css/scrollstyles.scss';
|
||||
import '../styles/scrollstyles.scss';
|
||||
import '../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function enableScrollX() {
|
||||
return !layoutManager.desktop;
|
||||
}
|
||||
|
||||
function enableScrollX() {
|
||||
return !layoutManager.desktop;
|
||||
function getThumbShape() {
|
||||
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
|
||||
}
|
||||
|
||||
function getPosterShape() {
|
||||
return enableScrollX() ? 'overflowPortrait' : 'portrait';
|
||||
}
|
||||
|
||||
function getSquareShape() {
|
||||
return enableScrollX() ? 'overflowSquare' : 'square';
|
||||
}
|
||||
|
||||
function getSections() {
|
||||
return [{
|
||||
name: 'Movies',
|
||||
types: 'Movie',
|
||||
id: 'favoriteMovies',
|
||||
shape: getPosterShape(),
|
||||
showTitle: false,
|
||||
overlayPlayButton: true
|
||||
}, {
|
||||
name: 'Shows',
|
||||
types: 'Series',
|
||||
id: 'favoriteShows',
|
||||
shape: getPosterShape(),
|
||||
showTitle: false,
|
||||
overlayPlayButton: true
|
||||
}, {
|
||||
name: 'Episodes',
|
||||
types: 'Episode',
|
||||
id: 'favoriteEpisode',
|
||||
shape: getThumbShape(),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
overlayPlayButton: true,
|
||||
overlayText: false,
|
||||
centerText: true
|
||||
}, {
|
||||
name: 'Videos',
|
||||
types: 'Video,MusicVideo',
|
||||
id: 'favoriteVideos',
|
||||
shape: getThumbShape(),
|
||||
preferThumb: true,
|
||||
showTitle: true,
|
||||
overlayPlayButton: true,
|
||||
overlayText: false,
|
||||
centerText: true
|
||||
}, {
|
||||
name: 'Artists',
|
||||
types: 'MusicArtist',
|
||||
id: 'favoriteArtists',
|
||||
shape: getSquareShape(),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
showParentTitle: false,
|
||||
centerText: true,
|
||||
overlayPlayButton: true,
|
||||
coverImage: true
|
||||
}, {
|
||||
name: 'Albums',
|
||||
types: 'MusicAlbum',
|
||||
id: 'favoriteAlbums',
|
||||
shape: getSquareShape(),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
showParentTitle: true,
|
||||
centerText: true,
|
||||
overlayPlayButton: true,
|
||||
coverImage: true
|
||||
}, {
|
||||
name: 'Songs',
|
||||
types: 'Audio',
|
||||
id: 'favoriteSongs',
|
||||
shape: getSquareShape(),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
showParentTitle: true,
|
||||
centerText: true,
|
||||
overlayMoreButton: true,
|
||||
action: 'instantmix',
|
||||
coverImage: true
|
||||
}];
|
||||
}
|
||||
|
||||
function loadSection(elem, userId, topParentId, section, isSingleSection) {
|
||||
const screenWidth = dom.getWindowSize().innerWidth;
|
||||
const options = {
|
||||
SortBy: 'SortName',
|
||||
SortOrder: 'Ascending',
|
||||
Filters: 'IsFavorite',
|
||||
Recursive: true,
|
||||
Fields: 'PrimaryImageAspectRatio,BasicSyncInfo',
|
||||
CollapseBoxSetItems: false,
|
||||
ExcludeLocationTypes: 'Virtual',
|
||||
EnableTotalRecordCount: false
|
||||
};
|
||||
|
||||
if (topParentId) {
|
||||
options.ParentId = topParentId;
|
||||
}
|
||||
|
||||
function getThumbShape() {
|
||||
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
|
||||
}
|
||||
if (!isSingleSection) {
|
||||
options.Limit = 6;
|
||||
|
||||
function getPosterShape() {
|
||||
return enableScrollX() ? 'overflowPortrait' : 'portrait';
|
||||
}
|
||||
|
||||
function getSquareShape() {
|
||||
return enableScrollX() ? 'overflowSquare' : 'square';
|
||||
}
|
||||
|
||||
function getSections() {
|
||||
return [{
|
||||
name: 'Movies',
|
||||
types: 'Movie',
|
||||
id: 'favoriteMovies',
|
||||
shape: getPosterShape(),
|
||||
showTitle: false,
|
||||
overlayPlayButton: true
|
||||
}, {
|
||||
name: 'Shows',
|
||||
types: 'Series',
|
||||
id: 'favoriteShows',
|
||||
shape: getPosterShape(),
|
||||
showTitle: false,
|
||||
overlayPlayButton: true
|
||||
}, {
|
||||
name: 'Episodes',
|
||||
types: 'Episode',
|
||||
id: 'favoriteEpisode',
|
||||
shape: getThumbShape(),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
overlayPlayButton: true,
|
||||
overlayText: false,
|
||||
centerText: true
|
||||
}, {
|
||||
name: 'Videos',
|
||||
types: 'Video,MusicVideo',
|
||||
id: 'favoriteVideos',
|
||||
shape: getThumbShape(),
|
||||
preferThumb: true,
|
||||
showTitle: true,
|
||||
overlayPlayButton: true,
|
||||
overlayText: false,
|
||||
centerText: true
|
||||
}, {
|
||||
name: 'Artists',
|
||||
types: 'MusicArtist',
|
||||
id: 'favoriteArtists',
|
||||
shape: getSquareShape(),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
showParentTitle: false,
|
||||
centerText: true,
|
||||
overlayPlayButton: true,
|
||||
coverImage: true
|
||||
}, {
|
||||
name: 'Albums',
|
||||
types: 'MusicAlbum',
|
||||
id: 'favoriteAlbums',
|
||||
shape: getSquareShape(),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
showParentTitle: true,
|
||||
centerText: true,
|
||||
overlayPlayButton: true,
|
||||
coverImage: true
|
||||
}, {
|
||||
name: 'Songs',
|
||||
types: 'Audio',
|
||||
id: 'favoriteSongs',
|
||||
shape: getSquareShape(),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
showParentTitle: true,
|
||||
centerText: true,
|
||||
overlayMoreButton: true,
|
||||
action: 'instantmix',
|
||||
coverImage: true
|
||||
}];
|
||||
}
|
||||
|
||||
function loadSection(elem, userId, topParentId, section, isSingleSection) {
|
||||
const screenWidth = dom.getWindowSize().innerWidth;
|
||||
const options = {
|
||||
SortBy: 'SortName',
|
||||
SortOrder: 'Ascending',
|
||||
Filters: 'IsFavorite',
|
||||
Recursive: true,
|
||||
Fields: 'PrimaryImageAspectRatio,BasicSyncInfo',
|
||||
CollapseBoxSetItems: false,
|
||||
ExcludeLocationTypes: 'Virtual',
|
||||
EnableTotalRecordCount: false
|
||||
};
|
||||
|
||||
if (topParentId) {
|
||||
options.ParentId = topParentId;
|
||||
if (enableScrollX()) {
|
||||
options.Limit = 20;
|
||||
} else if (screenWidth >= 1920) {
|
||||
options.Limit = 10;
|
||||
} else if (screenWidth >= 1440) {
|
||||
options.Limit = 8;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSingleSection) {
|
||||
options.Limit = 6;
|
||||
let promise;
|
||||
|
||||
if (section.types === 'MusicArtist') {
|
||||
promise = ApiClient.getArtists(userId, options);
|
||||
} else {
|
||||
options.IncludeItemTypes = section.types;
|
||||
promise = ApiClient.getItems(userId, options);
|
||||
}
|
||||
|
||||
return promise.then(function (result) {
|
||||
let html = '';
|
||||
|
||||
if (result.Items.length) {
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||
|
||||
if (!layoutManager.tv && options.Limit && result.Items.length >= options.Limit) {
|
||||
html += '<a is="emby-linkbutton" href="' + ('#/list.html?serverId=' + ApiClient.serverId() + '&type=' + section.types + '&IsFavorite=true') + '" class="more button-flat button-flat-mini sectionTitleTextButton">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||
html += globalize.translate(section.name);
|
||||
html += '</h2>';
|
||||
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
} else {
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate(section.name) + '</h2>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
if (enableScrollX()) {
|
||||
options.Limit = 20;
|
||||
} else if (screenWidth >= 1920) {
|
||||
options.Limit = 10;
|
||||
} else if (screenWidth >= 1440) {
|
||||
options.Limit = 8;
|
||||
}
|
||||
}
|
||||
|
||||
let promise;
|
||||
|
||||
if (section.types === 'MusicArtist') {
|
||||
promise = ApiClient.getArtists(userId, options);
|
||||
} else {
|
||||
options.IncludeItemTypes = section.types;
|
||||
promise = ApiClient.getItems(userId, options);
|
||||
}
|
||||
|
||||
return promise.then(function (result) {
|
||||
let html = '';
|
||||
|
||||
if (result.Items.length) {
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||
|
||||
if (!layoutManager.tv && options.Limit && result.Items.length >= options.Limit) {
|
||||
html += '<a is="emby-linkbutton" href="' + ('#/list.html?serverId=' + ApiClient.serverId() + '&type=' + section.types + '&IsFavorite=true') + '" class="more button-flat button-flat-mini sectionTitleTextButton">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||
html += globalize.translate(section.name);
|
||||
html += '</h2>';
|
||||
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
} else {
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate(section.name) + '</h2>';
|
||||
let scrollXClass = 'scrollX hiddenScrollX';
|
||||
if (layoutManager.tv) {
|
||||
scrollXClass += ' smoothScrollX';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
if (enableScrollX()) {
|
||||
let scrollXClass = 'scrollX hiddenScrollX';
|
||||
if (layoutManager.tv) {
|
||||
scrollXClass += ' smoothScrollX';
|
||||
}
|
||||
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer ' + scrollXClass + ' padded-left padded-right">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer vertical-wrap padded-left padded-right">';
|
||||
}
|
||||
|
||||
let cardLayout = appHost.preferVisualCards && section.autoCardLayout && section.showTitle;
|
||||
cardLayout = false;
|
||||
html += cardBuilder.getCardsHtml(result.Items, {
|
||||
preferThumb: section.preferThumb,
|
||||
shape: section.shape,
|
||||
centerText: section.centerText && !cardLayout,
|
||||
overlayText: section.overlayText !== false,
|
||||
showTitle: section.showTitle,
|
||||
showParentTitle: section.showParentTitle,
|
||||
scalable: true,
|
||||
coverImage: section.coverImage,
|
||||
overlayPlayButton: section.overlayPlayButton,
|
||||
overlayMoreButton: section.overlayMoreButton && !cardLayout,
|
||||
action: section.action,
|
||||
allowBottomPadding: !enableScrollX(),
|
||||
cardLayout: cardLayout
|
||||
});
|
||||
html += '</div>';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer ' + scrollXClass + ' padded-left padded-right">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer vertical-wrap padded-left padded-right">';
|
||||
}
|
||||
|
||||
elem.innerHTML = html;
|
||||
imageLoader.lazyChildren(elem);
|
||||
let cardLayout = appHost.preferVisualCards && section.autoCardLayout && section.showTitle;
|
||||
cardLayout = false;
|
||||
html += cardBuilder.getCardsHtml(result.Items, {
|
||||
preferThumb: section.preferThumb,
|
||||
shape: section.shape,
|
||||
centerText: section.centerText && !cardLayout,
|
||||
overlayText: section.overlayText !== false,
|
||||
showTitle: section.showTitle,
|
||||
showParentTitle: section.showParentTitle,
|
||||
scalable: true,
|
||||
coverImage: section.coverImage,
|
||||
overlayPlayButton: section.overlayPlayButton,
|
||||
overlayMoreButton: section.overlayMoreButton && !cardLayout,
|
||||
action: section.action,
|
||||
allowBottomPadding: !enableScrollX(),
|
||||
cardLayout: cardLayout
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
elem.innerHTML = html;
|
||||
imageLoader.lazyChildren(elem);
|
||||
});
|
||||
}
|
||||
|
||||
export function loadSections(page, userId, topParentId, types) {
|
||||
loading.show();
|
||||
let sections = getSections();
|
||||
const sectionid = getParameterByName('sectionid');
|
||||
|
||||
if (sectionid) {
|
||||
sections = sections.filter(function (s) {
|
||||
return s.id === sectionid;
|
||||
});
|
||||
}
|
||||
|
||||
export function loadSections(page, userId, topParentId, types) {
|
||||
loading.show();
|
||||
let sections = getSections();
|
||||
const sectionid = getParameterByName('sectionid');
|
||||
if (types) {
|
||||
sections = sections.filter(function (s) {
|
||||
return types.indexOf(s.id) !== -1;
|
||||
});
|
||||
}
|
||||
|
||||
if (sectionid) {
|
||||
sections = sections.filter(function (s) {
|
||||
return s.id === sectionid;
|
||||
});
|
||||
}
|
||||
let elem = page.querySelector('.favoriteSections');
|
||||
|
||||
if (types) {
|
||||
sections = sections.filter(function (s) {
|
||||
return types.indexOf(s.id) !== -1;
|
||||
});
|
||||
}
|
||||
|
||||
let elem = page.querySelector('.favoriteSections');
|
||||
|
||||
if (!elem.innerHTML) {
|
||||
let html = '';
|
||||
|
||||
for (let i = 0, length = sections.length; i < length; i++) {
|
||||
html += '<div class="verticalSection section' + sections[i].id + '"></div>';
|
||||
}
|
||||
|
||||
elem.innerHTML = html;
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
if (!elem.innerHTML) {
|
||||
let html = '';
|
||||
|
||||
for (let i = 0, length = sections.length; i < length; i++) {
|
||||
const section = sections[i];
|
||||
elem = page.querySelector('.section' + section.id);
|
||||
promises.push(loadSection(elem, userId, topParentId, section, sections.length === 1));
|
||||
html += '<div class="verticalSection section' + sections[i].id + '"></div>';
|
||||
}
|
||||
|
||||
Promise.all(promises).then(function () {
|
||||
loading.hide();
|
||||
});
|
||||
elem.innerHTML = html;
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0, length = sections.length; i < length; i++) {
|
||||
const section = sections[i];
|
||||
elem = page.querySelector('.section' + section.id);
|
||||
promises.push(loadSection(elem, userId, topParentId, section, sections.length === 1));
|
||||
}
|
||||
|
||||
Promise.all(promises).then(function () {
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
render: loadSections
|
||||
};
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
|
|
@ -1,110 +1,108 @@
|
|||
/* eslint-disable indent */
|
||||
export function getFetchPromise(request) {
|
||||
const headers = request.headers || {};
|
||||
export function getFetchPromise(request) {
|
||||
const headers = request.headers || {};
|
||||
|
||||
if (request.dataType === 'json') {
|
||||
headers.accept = 'application/json';
|
||||
}
|
||||
|
||||
const fetchRequest = {
|
||||
headers: headers,
|
||||
method: request.type,
|
||||
credentials: 'same-origin'
|
||||
};
|
||||
|
||||
let contentType = request.contentType;
|
||||
|
||||
if (request.data) {
|
||||
if (typeof request.data === 'string') {
|
||||
fetchRequest.body = request.data;
|
||||
} else {
|
||||
fetchRequest.body = paramsToString(request.data);
|
||||
|
||||
contentType = contentType || 'application/x-www-form-urlencoded; charset=UTF-8';
|
||||
}
|
||||
}
|
||||
|
||||
if (contentType) {
|
||||
headers['Content-Type'] = contentType;
|
||||
}
|
||||
|
||||
let url = request.url;
|
||||
|
||||
if (request.query) {
|
||||
const paramString = paramsToString(request.query);
|
||||
if (paramString) {
|
||||
url += `?${paramString}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!request.timeout) {
|
||||
return fetch(url, fetchRequest);
|
||||
}
|
||||
|
||||
return fetchWithTimeout(url, fetchRequest, request.timeout);
|
||||
if (request.dataType === 'json') {
|
||||
headers.accept = 'application/json';
|
||||
}
|
||||
|
||||
function fetchWithTimeout(url, options, timeoutMs) {
|
||||
console.debug(`fetchWithTimeout: timeoutMs: ${timeoutMs}, url: ${url}`);
|
||||
const fetchRequest = {
|
||||
headers: headers,
|
||||
method: request.type,
|
||||
credentials: 'same-origin'
|
||||
};
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
const timeout = setTimeout(reject, timeoutMs);
|
||||
let contentType = request.contentType;
|
||||
|
||||
options = options || {};
|
||||
options.credentials = 'same-origin';
|
||||
if (request.data) {
|
||||
if (typeof request.data === 'string') {
|
||||
fetchRequest.body = request.data;
|
||||
} else {
|
||||
fetchRequest.body = paramsToString(request.data);
|
||||
|
||||
fetch(url, options).then(function (response) {
|
||||
clearTimeout(timeout);
|
||||
contentType = contentType || 'application/x-www-form-urlencoded; charset=UTF-8';
|
||||
}
|
||||
}
|
||||
|
||||
console.debug(`fetchWithTimeout: succeeded connecting to url: ${url}`);
|
||||
if (contentType) {
|
||||
headers['Content-Type'] = contentType;
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
}, function (error) {
|
||||
clearTimeout(timeout);
|
||||
let url = request.url;
|
||||
|
||||
console.debug(`fetchWithTimeout: timed out connecting to url: ${url}`);
|
||||
if (request.query) {
|
||||
const paramString = paramsToString(request.query);
|
||||
if (paramString) {
|
||||
url += `?${paramString}`;
|
||||
}
|
||||
}
|
||||
|
||||
reject(error);
|
||||
});
|
||||
if (!request.timeout) {
|
||||
return fetch(url, fetchRequest);
|
||||
}
|
||||
|
||||
return fetchWithTimeout(url, fetchRequest, request.timeout);
|
||||
}
|
||||
|
||||
function fetchWithTimeout(url, options, timeoutMs) {
|
||||
console.debug(`fetchWithTimeout: timeoutMs: ${timeoutMs}, url: ${url}`);
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
const timeout = setTimeout(reject, timeoutMs);
|
||||
|
||||
options = options || {};
|
||||
options.credentials = 'same-origin';
|
||||
|
||||
fetch(url, options).then(function (response) {
|
||||
clearTimeout(timeout);
|
||||
|
||||
console.debug(`fetchWithTimeout: succeeded connecting to url: ${url}`);
|
||||
|
||||
resolve(response);
|
||||
}, function (error) {
|
||||
clearTimeout(timeout);
|
||||
|
||||
console.debug(`fetchWithTimeout: timed out connecting to url: ${url}`);
|
||||
|
||||
reject(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @param params {Record<string, string | number | boolean>}
|
||||
* @returns {string} Query string
|
||||
*/
|
||||
function paramsToString(params) {
|
||||
return Object.entries(params)
|
||||
.filter(([, v]) => v !== null && v !== undefined && v !== '')
|
||||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
||||
.join('&');
|
||||
function paramsToString(params) {
|
||||
return Object.entries(params)
|
||||
.filter(([, v]) => v !== null && v !== undefined && v !== '')
|
||||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
export function ajax(request) {
|
||||
if (!request) {
|
||||
throw new Error('Request cannot be null');
|
||||
}
|
||||
|
||||
export function ajax(request) {
|
||||
if (!request) {
|
||||
throw new Error('Request cannot be null');
|
||||
}
|
||||
request.headers = request.headers || {};
|
||||
|
||||
request.headers = request.headers || {};
|
||||
console.debug(`requesting url: ${request.url}`);
|
||||
|
||||
console.debug(`requesting url: ${request.url}`);
|
||||
|
||||
return getFetchPromise(request).then(function (response) {
|
||||
console.debug(`response status: ${response.status}, url: ${request.url}`);
|
||||
if (response.status < 400) {
|
||||
if (request.dataType === 'json' || request.headers.accept === 'application/json') {
|
||||
return response.json();
|
||||
} else if (request.dataType === 'text' || (response.headers.get('Content-Type') || '').toLowerCase().startsWith('text/')) {
|
||||
return response.text();
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
return getFetchPromise(request).then(function (response) {
|
||||
console.debug(`response status: ${response.status}, url: ${request.url}`);
|
||||
if (response.status < 400) {
|
||||
if (request.dataType === 'json' || request.headers.accept === 'application/json') {
|
||||
return response.json();
|
||||
} else if (request.dataType === 'text' || (response.headers.get('Content-Type') || '').toLowerCase().startsWith('text/')) {
|
||||
return response.text();
|
||||
} else {
|
||||
return Promise.reject(response);
|
||||
return response;
|
||||
}
|
||||
}, function (err) {
|
||||
console.error(`request failed to url: ${request.url}`);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
/* eslint-enable indent */
|
||||
} else {
|
||||
return Promise.reject(response);
|
||||
}
|
||||
}, function (err) {
|
||||
console.error(`request failed to url: ${request.url}`);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue