Merge branch 'master' into Search

This commit is contained in:
Nathan G 2023-10-05 08:24:48 -07:00 committed by GitHub
commit d75221afc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 3715 additions and 1377 deletions

View File

@ -0,0 +1,23 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
{
"name": "Node.js",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-20-bullseye",
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
//https://github.com/microsoft/vscode-dev-containers/issues/559
"postCreateCommand": "source $NVM_DIR/nvm.sh && nvm install 20"
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View File

@ -66,6 +66,7 @@ module.exports = {
'no-unused-expressions': ['off'], 'no-unused-expressions': ['off'],
'@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }], '@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
'no-unused-private-class-members': ['error'], 'no-unused-private-class-members': ['error'],
'no-useless-rename': ['error'],
'no-useless-constructor': ['off'], 'no-useless-constructor': ['off'],
'@typescript-eslint/no-useless-constructor': ['error'], '@typescript-eslint/no-useless-constructor': ['error'],
'no-var': ['error'], 'no-var': ['error'],

View File

@ -27,3 +27,6 @@ jobs:
- name: Run tsc - name: Run tsc
run: npm run build:check run: npm run build:check
- name: Run test suite
run: npm run test

View File

@ -66,6 +66,7 @@
- [Fishbigger](https://github.com/fishbigger) - [Fishbigger](https://github.com/fishbigger)
- [sleepycatcoding](https://github.com/sleepycatcoding) - [sleepycatcoding](https://github.com/sleepycatcoding)
- [TheMelmacian](https://github.com/TheMelmacian) - [TheMelmacian](https://github.com/TheMelmacian)
- [tehciolo](https://github.com/tehciolo)
- [Nate G](https://github.com/GGProGaming) - [Nate G](https://github.com/GGProGaming)
# Emby Contributors # Emby Contributors

View File

@ -77,8 +77,9 @@ Jellyfin Web is the frontend used for most of the clients available for end user
. .
└── src └── src
├── apps ├── apps
│   ├── experimental # New experimental app layout │   ├── dashboard # Admin dashboard app layout and routes
│   └── stable # Classic (stable) app layout │   ├── experimental # New experimental app layout and routes
│   └── stable # Classic (stable) app layout and routes
├── assets # Static assets ├── assets # Static assets
├── components # Higher order visual components and React components ├── components # Higher order visual components and React components
├── controllers # Legacy page views and controllers 🧹 ├── controllers # Legacy page views and controllers 🧹
@ -87,7 +88,6 @@ Jellyfin Web is the frontend used for most of the clients available for end user
├── legacy # Polyfills for legacy browsers ├── legacy # Polyfills for legacy browsers
├── libraries # Third party libraries 🧹 ├── libraries # Third party libraries 🧹
├── plugins # Client plugins ├── plugins # Client plugins
├── routes # React routes/pages
├── scripts # Random assortment of visual components and utilities 🐉 ├── scripts # Random assortment of visual components and utilities 🐉
├── strings # Translation files ├── strings # Translation files
├── styles # Common app Sass stylesheets ├── styles # Common app Sass stylesheets

1674
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -58,6 +58,7 @@
"stylelint-scss": "5.0.0", "stylelint-scss": "5.0.0",
"ts-loader": "9.4.4", "ts-loader": "9.4.4",
"typescript": "5.0.4", "typescript": "5.0.4",
"vitest": "0.34.6",
"webpack": "5.88.1", "webpack": "5.88.1",
"webpack-bundle-analyzer": "4.9.1", "webpack-bundle-analyzer": "4.9.1",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
@ -145,6 +146,8 @@
"build:check": "tsc --noEmit", "build:check": "tsc --noEmit",
"escheck": "es-check", "escheck": "es-check",
"lint": "eslint \"./\"", "lint": "eslint \"./\"",
"test": "vitest --watch=false",
"test:watch": "vitest",
"stylelint": "npm run stylelint:css && npm run stylelint:scss", "stylelint": "npm run stylelint:css && npm run stylelint:scss",
"stylelint:css": "stylelint \"src/**/*.css\"", "stylelint:css": "stylelint \"src/**/*.css\"",
"stylelint:scss": "stylelint --config=\".stylelintrc.scss.json\" \"src/**/*.scss\"" "stylelint:scss": "stylelint --config=\".stylelintrc.scss.json\" \"src/**/*.scss\""

View File

@ -1,10 +1,12 @@
import loadable from '@loadable/component'; import loadable from '@loadable/component';
import { ThemeProvider } from '@mui/material/styles'; import { ThemeProvider } from '@mui/material/styles';
import { History } from '@remix-run/router'; import { History } from '@remix-run/router';
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import React from 'react';
import { useLocation } from 'react-router-dom';
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
import AppHeader from 'components/AppHeader'; import AppHeader from 'components/AppHeader';
import Backdrop from 'components/Backdrop'; import Backdrop from 'components/Backdrop';
import { HistoryRouter } from 'components/router/HistoryRouter'; import { HistoryRouter } from 'components/router/HistoryRouter';
@ -12,6 +14,7 @@ import { ApiProvider } from 'hooks/useApi';
import { WebConfigProvider } from 'hooks/useWebConfig'; import { WebConfigProvider } from 'hooks/useWebConfig';
import theme from 'themes/theme'; import theme from 'themes/theme';
const DashboardApp = loadable(() => import('./apps/dashboard/App'));
const ExperimentalApp = loadable(() => import('./apps/experimental/App')); const ExperimentalApp = loadable(() => import('./apps/experimental/App'));
const StableApp = loadable(() => import('./apps/stable/App')); const StableApp = loadable(() => import('./apps/stable/App'));
@ -21,16 +24,22 @@ const RootAppLayout = () => {
const layoutMode = localStorage.getItem('layout'); const layoutMode = localStorage.getItem('layout');
const isExperimentalLayout = layoutMode === 'experimental'; const isExperimentalLayout = layoutMode === 'experimental';
const location = useLocation();
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
.some(path => location.pathname.startsWith(`/${path}`));
return ( return (
<> <>
<Backdrop /> <Backdrop />
<AppHeader isHidden={isExperimentalLayout} /> <AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
{ {
isExperimentalLayout ? isExperimentalLayout ?
<ExperimentalApp /> : <ExperimentalApp /> :
<StableApp /> <StableApp />
} }
<DashboardApp />
</> </>
); );
}; };

View File

@ -0,0 +1,66 @@
import loadable from '@loadable/component';
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import ConnectionRequired from 'components/ConnectionRequired';
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
import { AsyncPageProps, AsyncRoute, toAsyncPageRoute } from 'components/router/AsyncRoute';
import { toRedirectRoute } from 'components/router/Redirect';
import ServerContentPage from 'components/ServerContentPage';
import AppLayout from './AppLayout';
import { REDIRECTS } from './routes/_redirects';
import { ASYNC_ADMIN_ROUTES } from './routes/_asyncRoutes';
import { LEGACY_ADMIN_ROUTES } from './routes/_legacyRoutes';
const DashboardAsyncPage = loadable(
(props: { page: string }) => import(/* webpackChunkName: "[request]" */ `./routes/${props.page}`),
{ cacheKey: (props: AsyncPageProps) => props.page }
);
const toDashboardAsyncPageRoute = (route: AsyncRoute) => (
toAsyncPageRoute({
...route,
element: DashboardAsyncPage
})
);
export const DASHBOARD_APP_PATHS = {
Dashboard: 'dashboard',
MetadataManager: 'metadata',
PluginConfig: 'configurationpage'
};
const DashboardApp = () => (
<Routes>
<Route element={<ConnectionRequired isAdminRequired />}>
<Route element={<AppLayout drawerlessPaths={[ DASHBOARD_APP_PATHS.MetadataManager ]} />}>
<Route path={DASHBOARD_APP_PATHS.Dashboard}>
{ASYNC_ADMIN_ROUTES.map(toDashboardAsyncPageRoute)}
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
</Route>
{/* NOTE: The metadata editor might deserve a dedicated app in the future */}
{toViewManagerPageRoute({
path: DASHBOARD_APP_PATHS.MetadataManager,
pageProps: {
controller: 'edititemmetadata',
view: 'edititemmetadata.html'
}
})}
<Route path={DASHBOARD_APP_PATHS.PluginConfig} element={
<ServerContentPage view='/web/configurationpage' />
} />
</Route>
{/* Suppress warnings for unhandled routes */}
<Route path='*' element={null} />
</Route>
{/* Redirects for old paths */}
{REDIRECTS.map(toRedirectRoute)}
</Routes>
);
export default DashboardApp;

View File

@ -0,0 +1,108 @@
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import { useTheme } from '@mui/material/styles';
import React, { FC, useCallback, useEffect, useState } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import AppBody from 'components/AppBody';
import AppToolbar from 'components/toolbar/AppToolbar';
import ElevationScroll from 'components/ElevationScroll';
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
import { useApi } from 'hooks/useApi';
import { useLocalStorage } from 'hooks/useLocalStorage';
import AppDrawer from './components/drawer/AppDrawer';
import './AppOverrides.scss';
interface AppLayoutProps {
drawerlessPaths: string[]
}
interface DashboardAppSettings {
isDrawerPinned: boolean
}
const DEFAULT_APP_SETTINGS: DashboardAppSettings = {
isDrawerPinned: false
};
const AppLayout: FC<AppLayoutProps> = ({
drawerlessPaths
}) => {
const [ appSettings, setAppSettings ] = useLocalStorage<DashboardAppSettings>('DashboardAppSettings', DEFAULT_APP_SETTINGS);
const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned);
const location = useLocation();
const theme = useTheme();
const { user } = useApi();
const isDrawerAvailable = !drawerlessPaths.some(path => location.pathname.startsWith(`/${path}`));
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 (
<Box sx={{ display: 'flex' }}>
<ElevationScroll elevate={isDrawerOpen}>
<AppBar
position='fixed'
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.drawer + 1 }}
>
<AppToolbar
isDrawerAvailable={isDrawerAvailable}
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
})
}}
>
<AppBody>
<Outlet />
</AppBody>
</Box>
</Box>
);
};
export default AppLayout;

View File

@ -0,0 +1,22 @@
// 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 dashboard pages layout to work with drawer
.dashboardDocument {
.mainAnimatedPage {
position: relative;
}
.skinBody {
position: unset !important;
}
// Fix the padding of dashboard pages
.content-primary.content-primary {
padding-top: 3.25rem !important;
}
}

View File

@ -0,0 +1,29 @@
import React, { FC } from 'react';
import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';
import ServerDrawerSection from './sections/ServerDrawerSection';
import DevicesDrawerSection from './sections/DevicesDrawerSection';
import LiveTvDrawerSection from './sections/LiveTvDrawerSection';
import AdvancedDrawerSection from './sections/AdvancedDrawerSection';
import PluginDrawerSection from './sections/PluginDrawerSection';
const AppDrawer: FC<ResponsiveDrawerProps> = ({
open = false,
onClose,
onOpen
}) => (
<ResponsiveDrawer
open={open}
onClose={onClose}
onOpen={onOpen}
>
<ServerDrawerSection />
<DevicesDrawerSection />
<LiveTvDrawerSection />
<AdvancedDrawerSection />
<PluginDrawerSection />
</ResponsiveDrawer>
);
export default AppDrawer;

View File

@ -19,10 +19,10 @@ import ListItemLink from 'components/ListItemLink';
import globalize from 'scripts/globalize'; import globalize from 'scripts/globalize';
const PLUGIN_PATHS = [ const PLUGIN_PATHS = [
'/installedplugins.html', '/dashboard/plugins',
'/availableplugins.html', '/dashboard/plugins/catalog',
'/repositories.html', '/dashboard/plugins/repositories',
'/addplugin.html', '/dashboard/plugins/add',
'/configurationpage' '/configurationpage'
]; ];
@ -41,7 +41,7 @@ const AdvancedDrawerSection = () => {
} }
> >
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/networking.html'> <ListItemLink to='/dashboard/networking'>
<ListItemIcon> <ListItemIcon>
<Lan /> <Lan />
</ListItemIcon> </ListItemIcon>
@ -49,7 +49,7 @@ const AdvancedDrawerSection = () => {
</ListItemLink> </ListItemLink>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/apikeys.html'> <ListItemLink to='/dashboard/keys'>
<ListItemIcon> <ListItemIcon>
<VpnKey /> <VpnKey />
</ListItemIcon> </ListItemIcon>
@ -57,7 +57,7 @@ const AdvancedDrawerSection = () => {
</ListItemLink> </ListItemLink>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/log.html'> <ListItemLink to='/dashboard/logs'>
<ListItemIcon> <ListItemIcon>
<Article /> <Article />
</ListItemIcon> </ListItemIcon>
@ -65,7 +65,7 @@ const AdvancedDrawerSection = () => {
</ListItemLink> </ListItemLink>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/notificationsettings.html'> <ListItemLink to='/dashboard/notifications'>
<ListItemIcon> <ListItemIcon>
<EditNotifications /> <EditNotifications />
</ListItemIcon> </ListItemIcon>
@ -73,7 +73,7 @@ const AdvancedDrawerSection = () => {
</ListItemLink> </ListItemLink>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/installedplugins.html' selected={false}> <ListItemLink to='/dashboard/plugins' selected={false}>
<ListItemIcon> <ListItemIcon>
<Extension /> <Extension />
</ListItemIcon> </ListItemIcon>
@ -83,19 +83,19 @@ const AdvancedDrawerSection = () => {
</ListItem> </ListItem>
<Collapse in={isPluginSectionOpen} timeout='auto' unmountOnExit> <Collapse in={isPluginSectionOpen} timeout='auto' unmountOnExit>
<List component='div' disablePadding> <List component='div' disablePadding>
<ListItemLink to='/installedplugins.html' sx={{ pl: 4 }}> <ListItemLink to='/dashboard/plugins' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('TabMyPlugins')} /> <ListItemText inset primary={globalize.translate('TabMyPlugins')} />
</ListItemLink> </ListItemLink>
<ListItemLink to='/availableplugins.html' sx={{ pl: 4 }}> <ListItemLink to='/dashboard/plugins/catalog' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('TabCatalog')} /> <ListItemText inset primary={globalize.translate('TabCatalog')} />
</ListItemLink> </ListItemLink>
<ListItemLink to='/repositories.html' sx={{ pl: 4 }}> <ListItemLink to='/dashboard/plugins/repositories' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('TabRepositories')} /> <ListItemText inset primary={globalize.translate('TabRepositories')} />
</ListItemLink> </ListItemLink>
</List> </List>
</Collapse> </Collapse>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/scheduledtasks.html'> <ListItemLink to='/dashboard/tasks'>
<ListItemIcon> <ListItemIcon>
<Schedule /> <Schedule />
</ListItemIcon> </ListItemIcon>

View File

@ -12,8 +12,8 @@ import ListItemLink from 'components/ListItemLink';
import globalize from 'scripts/globalize'; import globalize from 'scripts/globalize';
const DLNA_PATHS = [ const DLNA_PATHS = [
'/dlnasettings.html', '/dashboard/dlna',
'/dlnaprofiles.html' '/dashboard/dlna/profiles'
]; ];
const DevicesDrawerSection = () => { const DevicesDrawerSection = () => {
@ -31,7 +31,7 @@ const DevicesDrawerSection = () => {
} }
> >
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/devices.html'> <ListItemLink to='/dashboard/devices'>
<ListItemIcon> <ListItemIcon>
<Devices /> <Devices />
</ListItemIcon> </ListItemIcon>
@ -47,7 +47,7 @@ const DevicesDrawerSection = () => {
</ListItemLink> </ListItemLink>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/dlnasettings.html' selected={false}> <ListItemLink to='/dashboard/dlna' selected={false}>
<ListItemIcon> <ListItemIcon>
<Input /> <Input />
</ListItemIcon> </ListItemIcon>
@ -57,10 +57,10 @@ const DevicesDrawerSection = () => {
</ListItem> </ListItem>
<Collapse in={isDlnaSectionOpen} timeout='auto' unmountOnExit> <Collapse in={isDlnaSectionOpen} timeout='auto' unmountOnExit>
<List component='div' disablePadding> <List component='div' disablePadding>
<ListItemLink to='/dlnasettings.html' sx={{ pl: 4 }}> <ListItemLink to='/dashboard/dlna' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('Settings')} /> <ListItemText inset primary={globalize.translate('Settings')} />
</ListItemLink> </ListItemLink>
<ListItemLink to='/dlnaprofiles.html' sx={{ pl: 4 }}> <ListItemLink to='/dashboard/dlna/profiles' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('TabProfiles')} /> <ListItemText inset primary={globalize.translate('TabProfiles')} />
</ListItemLink> </ListItemLink>
</List> </List>

View File

@ -20,7 +20,7 @@ const LiveTvDrawerSection = () => {
} }
> >
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/livetvstatus.html'> <ListItemLink to='/dashboard/livetv'>
<ListItemIcon> <ListItemIcon>
<LiveTv /> <LiveTv />
</ListItemIcon> </ListItemIcon>
@ -28,7 +28,7 @@ const LiveTvDrawerSection = () => {
</ListItemLink> </ListItemLink>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/livetvsettings.html'> <ListItemLink to='/dashboard/recordings'>
<ListItemIcon> <ListItemIcon>
<Dvr /> <Dvr />
</ListItemIcon> </ListItemIcon>

View File

@ -12,16 +12,16 @@ import ListItemLink from 'components/ListItemLink';
import globalize from 'scripts/globalize'; import globalize from 'scripts/globalize';
const LIBRARY_PATHS = [ const LIBRARY_PATHS = [
'/library.html', '/dashboard/libraries',
'/librarydisplay.html', '/dashboard/libraries/display',
'/metadataimages.html', '/dashboard/libraries/metadata',
'/metadatanfo.html' '/dashboard/libraries/nfo'
]; ];
const PLAYBACK_PATHS = [ const PLAYBACK_PATHS = [
'/encodingsettings.html', '/dashboard/playback/transcoding',
'/playbackconfiguration.html', '/dashboard/playback/resume',
'/streamingsettings.html' '/dashboard/playback/streaming'
]; ];
const ServerDrawerSection = () => { const ServerDrawerSection = () => {
@ -40,7 +40,7 @@ const ServerDrawerSection = () => {
} }
> >
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/dashboard.html'> <ListItemLink to='/dashboard'>
<ListItemIcon> <ListItemIcon>
<Dashboard /> <Dashboard />
</ListItemIcon> </ListItemIcon>
@ -48,7 +48,7 @@ const ServerDrawerSection = () => {
</ListItemLink> </ListItemLink>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/dashboardgeneral.html'> <ListItemLink to='/dashboard/settings'>
<ListItemIcon> <ListItemIcon>
<Settings /> <Settings />
</ListItemIcon> </ListItemIcon>
@ -56,7 +56,7 @@ const ServerDrawerSection = () => {
</ListItemLink> </ListItemLink>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/userprofiles.html'> <ListItemLink to='/dashboard/users'>
<ListItemIcon> <ListItemIcon>
<People /> <People />
</ListItemIcon> </ListItemIcon>
@ -64,7 +64,7 @@ const ServerDrawerSection = () => {
</ListItemLink> </ListItemLink>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/library.html' selected={false}> <ListItemLink to='/dashboard/libraries' selected={false}>
<ListItemIcon> <ListItemIcon>
<LibraryAdd /> <LibraryAdd />
</ListItemIcon> </ListItemIcon>
@ -74,22 +74,22 @@ const ServerDrawerSection = () => {
</ListItem> </ListItem>
<Collapse in={isLibrarySectionOpen} timeout='auto' unmountOnExit> <Collapse in={isLibrarySectionOpen} timeout='auto' unmountOnExit>
<List component='div' disablePadding> <List component='div' disablePadding>
<ListItemLink to='/library.html' sx={{ pl: 4 }}> <ListItemLink to='/dashboard/libraries' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('HeaderLibraries')} /> <ListItemText inset primary={globalize.translate('HeaderLibraries')} />
</ListItemLink> </ListItemLink>
<ListItemLink to='/librarydisplay.html' sx={{ pl: 4 }}> <ListItemLink to='/dashboard/libraries/display' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('Display')} /> <ListItemText inset primary={globalize.translate('Display')} />
</ListItemLink> </ListItemLink>
<ListItemLink to='/metadataimages.html' sx={{ pl: 4 }}> <ListItemLink to='/dashboard/libraries/metadata' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('Metadata')} /> <ListItemText inset primary={globalize.translate('Metadata')} />
</ListItemLink> </ListItemLink>
<ListItemLink to='/metadatanfo.html' sx={{ pl: 4 }}> <ListItemLink to='/dashboard/libraries/nfo' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('TabNfoSettings')} /> <ListItemText inset primary={globalize.translate('TabNfoSettings')} />
</ListItemLink> </ListItemLink>
</List> </List>
</Collapse> </Collapse>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/encodingsettings.html' selected={false}> <ListItemLink to='/dashboard/playback/transcoding' selected={false}>
<ListItemIcon> <ListItemIcon>
<PlayCircle /> <PlayCircle />
</ListItemIcon> </ListItemIcon>
@ -99,13 +99,13 @@ const ServerDrawerSection = () => {
</ListItem> </ListItem>
<Collapse in={isPlaybackSectionOpen} timeout='auto' unmountOnExit> <Collapse in={isPlaybackSectionOpen} timeout='auto' unmountOnExit>
<List component='div' disablePadding> <List component='div' disablePadding>
<ListItemLink to='/encodingsettings.html' sx={{ pl: 4 }}> <ListItemLink to='/dashboard/playback/transcoding' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('Transcoding')} /> <ListItemText inset primary={globalize.translate('Transcoding')} />
</ListItemLink> </ListItemLink>
<ListItemLink to='/playbackconfiguration.html' sx={{ pl: 4 }}> <ListItemLink to='/dashboard/playback/resume' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('ButtonResume')} /> <ListItemText inset primary={globalize.translate('ButtonResume')} />
</ListItemLink> </ListItemLink>
<ListItemLink to='/streamingsettings.html' sx={{ pl: 4 }}> <ListItemLink to='/dashboard/playback/streaming' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('TabStreaming')} /> <ListItemText inset primary={globalize.translate('TabStreaming')} />
</ListItemLink> </ListItemLink>
</List> </List>

View File

@ -0,0 +1,12 @@
import type { AsyncRoute } from 'components/router/AsyncRoute';
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'activity' },
{ path: 'notifications' },
{ path: 'users' },
{ path: 'users/access' },
{ path: 'users/add' },
{ path: 'users/parentalcontrol' },
{ path: 'users/password' },
{ path: 'users/profile' }
];

View File

@ -1,170 +1,164 @@
import { LegacyRoute } from '../../../../components/router/LegacyRoute'; import type { LegacyRoute } from 'components/router/LegacyRoute';
export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
{ {
path: 'dashboard.html', path: '/dashboard',
pageProps: { pageProps: {
controller: 'dashboard/dashboard', controller: 'dashboard/dashboard',
view: 'dashboard/dashboard.html' view: 'dashboard/dashboard.html'
} }
}, { }, {
path: 'dashboardgeneral.html', path: 'settings',
pageProps: { pageProps: {
controller: 'dashboard/general', controller: 'dashboard/general',
view: 'dashboard/general.html' view: 'dashboard/general.html'
} }
}, { }, {
path: 'networking.html', path: 'networking',
pageProps: { pageProps: {
controller: 'dashboard/networking', controller: 'dashboard/networking',
view: 'dashboard/networking.html' view: 'dashboard/networking.html'
} }
}, { }, {
path: 'devices.html', path: 'devices',
pageProps: { pageProps: {
controller: 'dashboard/devices/devices', controller: 'dashboard/devices/devices',
view: 'dashboard/devices/devices.html' view: 'dashboard/devices/devices.html'
} }
}, { }, {
path: 'device.html', path: 'devices/edit',
pageProps: { pageProps: {
controller: 'dashboard/devices/device', controller: 'dashboard/devices/device',
view: 'dashboard/devices/device.html' view: 'dashboard/devices/device.html'
} }
}, { }, {
path: 'dlnaprofile.html', path: 'dlna/profiles/edit',
pageProps: { pageProps: {
controller: 'dashboard/dlna/profile', controller: 'dashboard/dlna/profile',
view: 'dashboard/dlna/profile.html' view: 'dashboard/dlna/profile.html'
} }
}, { }, {
path: 'dlnaprofiles.html', path: 'dlna/profiles',
pageProps: { pageProps: {
controller: 'dashboard/dlna/profiles', controller: 'dashboard/dlna/profiles',
view: 'dashboard/dlna/profiles.html' view: 'dashboard/dlna/profiles.html'
} }
}, { }, {
path: 'dlnasettings.html', path: 'dlna',
pageProps: { pageProps: {
controller: 'dashboard/dlna/settings', controller: 'dashboard/dlna/settings',
view: 'dashboard/dlna/settings.html' view: 'dashboard/dlna/settings.html'
} }
}, { }, {
path: 'addplugin.html', path: 'plugins/add',
pageProps: { pageProps: {
controller: 'dashboard/plugins/add/index', controller: 'dashboard/plugins/add/index',
view: 'dashboard/plugins/add/index.html' view: 'dashboard/plugins/add/index.html'
} }
}, { }, {
path: 'library.html', path: 'libraries',
pageProps: { pageProps: {
controller: 'dashboard/library', controller: 'dashboard/library',
view: 'dashboard/library.html' view: 'dashboard/library.html'
} }
}, { }, {
path: 'librarydisplay.html', path: 'libraries/display',
pageProps: { pageProps: {
controller: 'dashboard/librarydisplay', controller: 'dashboard/librarydisplay',
view: 'dashboard/librarydisplay.html' view: 'dashboard/librarydisplay.html'
} }
}, { }, {
path: 'edititemmetadata.html', path: 'playback/transcoding',
pageProps: {
controller: 'edititemmetadata',
view: 'edititemmetadata.html'
}
}, {
path: 'encodingsettings.html',
pageProps: { pageProps: {
controller: 'dashboard/encodingsettings', controller: 'dashboard/encodingsettings',
view: 'dashboard/encodingsettings.html' view: 'dashboard/encodingsettings.html'
} }
}, { }, {
path: 'log.html', path: 'logs',
pageProps: { pageProps: {
controller: 'dashboard/logs', controller: 'dashboard/logs',
view: 'dashboard/logs.html' view: 'dashboard/logs.html'
} }
}, { }, {
path: 'metadataimages.html', path: 'libraries/metadata',
pageProps: { pageProps: {
controller: 'dashboard/metadataImages', controller: 'dashboard/metadataImages',
view: 'dashboard/metadataimages.html' view: 'dashboard/metadataimages.html'
} }
}, { }, {
path: 'metadatanfo.html', path: 'libraries/nfo',
pageProps: { pageProps: {
controller: 'dashboard/metadatanfo', controller: 'dashboard/metadatanfo',
view: 'dashboard/metadatanfo.html' view: 'dashboard/metadatanfo.html'
} }
}, { }, {
path: 'playbackconfiguration.html', path: 'playback/resume',
pageProps: { pageProps: {
controller: 'dashboard/playback', controller: 'dashboard/playback',
view: 'dashboard/playback.html' view: 'dashboard/playback.html'
} }
}, { }, {
path: 'availableplugins.html', path: 'plugins/catalog',
pageProps: { pageProps: {
controller: 'dashboard/plugins/available/index', controller: 'dashboard/plugins/available/index',
view: 'dashboard/plugins/available/index.html' view: 'dashboard/plugins/available/index.html'
} }
}, { }, {
path: 'repositories.html', path: 'plugins/repositories',
pageProps: { pageProps: {
controller: 'dashboard/plugins/repositories/index', controller: 'dashboard/plugins/repositories/index',
view: 'dashboard/plugins/repositories/index.html' view: 'dashboard/plugins/repositories/index.html'
} }
}, { }, {
path: 'livetvguideprovider.html', path: 'livetv/guide',
pageProps: { pageProps: {
controller: 'livetvguideprovider', controller: 'livetvguideprovider',
view: 'livetvguideprovider.html' view: 'livetvguideprovider.html'
} }
}, { }, {
path: 'livetvsettings.html', path: 'recordings',
pageProps: { pageProps: {
controller: 'livetvsettings', controller: 'livetvsettings',
view: 'livetvsettings.html' view: 'livetvsettings.html'
} }
}, { }, {
path: 'livetvstatus.html', path: 'livetv',
pageProps: { pageProps: {
controller: 'livetvstatus', controller: 'livetvstatus',
view: 'livetvstatus.html' view: 'livetvstatus.html'
} }
}, { }, {
path: 'livetvtuner.html', path: 'livetv/tuner',
pageProps: { pageProps: {
controller: 'livetvtuner', controller: 'livetvtuner',
view: 'livetvtuner.html' view: 'livetvtuner.html'
} }
}, { }, {
path: 'installedplugins.html', path: 'plugins',
pageProps: { pageProps: {
controller: 'dashboard/plugins/installed/index', controller: 'dashboard/plugins/installed/index',
view: 'dashboard/plugins/installed/index.html' view: 'dashboard/plugins/installed/index.html'
} }
}, { }, {
path: 'scheduledtask.html', path: 'tasks/edit',
pageProps: { pageProps: {
controller: 'dashboard/scheduledtasks/scheduledtask', controller: 'dashboard/scheduledtasks/scheduledtask',
view: 'dashboard/scheduledtasks/scheduledtask.html' view: 'dashboard/scheduledtasks/scheduledtask.html'
} }
}, { }, {
path: 'scheduledtasks.html', path: 'tasks',
pageProps: { pageProps: {
controller: 'dashboard/scheduledtasks/scheduledtasks', controller: 'dashboard/scheduledtasks/scheduledtasks',
view: 'dashboard/scheduledtasks/scheduledtasks.html' view: 'dashboard/scheduledtasks/scheduledtasks.html'
} }
}, { }, {
path: 'apikeys.html', path: 'keys',
pageProps: { pageProps: {
controller: 'dashboard/apikeys', controller: 'dashboard/apikeys',
view: 'dashboard/apikeys.html' view: 'dashboard/apikeys.html'
} }
}, { }, {
path: 'streamingsettings.html', path: 'playback/streaming',
pageProps: { pageProps: {
view: 'dashboard/streaming.html', view: 'dashboard/streaming.html',
controller: 'dashboard/streaming' controller: 'dashboard/streaming'

View File

@ -0,0 +1,40 @@
import type { Redirect } from 'components/router/Redirect';
export const REDIRECTS: Redirect[] = [
{ from: 'addplugin.html', to: '/dashboard/plugins/add' },
{ from: 'apikeys.html', to: '/dashboard/keys' },
{ from: 'availableplugins.html', to: '/dashboard/plugins/catalog' },
{ from: 'dashboard.html', to: '/dashboard' },
{ from: 'dashboardgeneral.html', to: '/dashboard/settings' },
{ from: 'device.html', to: '/dashboard/devices/edit' },
{ from: 'devices.html', to: '/dashboard/devices' },
{ from: 'dlnaprofile.html', to: '/dashboard/dlna/profiles/edit' },
{ from: 'dlnaprofiles.html', to: '/dashboard/dlna/profiles' },
{ from: 'dlnasettings.html', to: '/dashboard/dlna' },
{ from: 'edititemmetadata.html', to: '/metadata' },
{ from: 'encodingsettings.html', to: '/dashboard/playback/transcoding' },
{ from: 'installedplugins.html', to: '/dashboard/plugins' },
{ from: 'library.html', to: '/dashboard/libraries' },
{ from: 'librarydisplay.html', to: '/dashboard/libraries/display' },
{ from: 'livetvguideprovider.html', to: '/dashboard/livetv/guide' },
{ from: 'livetvsettings.html', to: '/dashboard/recordings' },
{ from: 'livetvstatus.html', to: '/dashboard/livetv' },
{ from: 'livetvtuner.html', to: '/dashboard/livetv/tuner' },
{ from: 'log.html', to: '/dashboard/logs' },
{ from: 'metadataimages.html', to: '/dashboard/libraries/metadata' },
{ from: 'metadatanfo.html', to: '/dashboard/libraries/nfo' },
{ from: 'networking.html', to: '/dashboard/networking' },
{ from: 'notificationsettings.html', to: '/dashboard/notifications' },
{ from: 'playbackconfiguration.html', to: '/dashboard/playback/resume' },
{ from: 'repositories.html', to: '/dashboard/plugins/repositories' },
{ from: 'scheduledtask.html', to: '/dashboard/tasks/edit' },
{ from: 'scheduledtasks.html', to: '/dashboard/tasks' },
{ from: 'serveractivity.html', to: '/dashboard/activity' },
{ from: 'streamingsettings.html', to: '/dashboard/playback/streaming' },
{ from: 'useredit.html', to: '/dashboard/users/profile' },
{ from: 'userlibraryaccess.html', to: '/dashboard/users/access' },
{ from: 'usernew.html', to: '/dashboard/users/add' },
{ from: 'userparentalcontrol.html', to: '/dashboard/users/parentalcontrol' },
{ from: 'userpassword.html', to: '/dashboard/users/password' },
{ from: 'userprofiles.html', to: '/dashboard/users' }
];

View File

@ -19,9 +19,9 @@ import { parseISO8601Date, toLocaleDateString, toLocaleTimeString } from 'script
import globalize from 'scripts/globalize'; import globalize from 'scripts/globalize';
import { toBoolean } from 'utils/string'; import { toBoolean } from 'utils/string';
import LogLevelChip from '../../components/activityTable/LogLevelChip'; import LogLevelChip from '../components/activityTable/LogLevelChip';
import OverviewCell from '../../components/activityTable/OverviewCell'; import OverviewCell from '../components/activityTable/OverviewCell';
import GridActionsCellLink from '../../components/GridActionsCellLink'; import GridActionsCellLink from '../components/dataGrid/GridActionsCellLink';
const DEFAULT_PAGE_SIZE = 25; const DEFAULT_PAGE_SIZE = 25;
const VIEW_PARAM = 'useractivity'; const VIEW_PARAM = 'useractivity';
@ -68,7 +68,7 @@ const Activity = () => {
sx={{ padding: 0 }} sx={{ padding: 0 }}
title={users[row.UserId]?.Name ?? undefined} title={users[row.UserId]?.Name ?? undefined}
component={Link} component={Link}
to={`/useredit.html?userId=${row.UserId}`} to={`/dashboard/users/profile?userId=${row.UserId}`}
> >
<UserAvatar user={users[row.UserId]} /> <UserAvatar user={users[row.UserId]} />
</IconButton> </IconButton>

View File

@ -9,7 +9,7 @@ const PluginLink = () => (
__html: `<a __html: `<a
is='emby-linkbutton' is='emby-linkbutton'
class='button-link' class='button-link'
href='#/addplugin.html?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173' href='#/dashboard/plugins/add?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173'
> >
${globalize.translate('GetThePlugin')} ${globalize.translate('GetThePlugin')}
</a>` </a>`

View File

@ -140,7 +140,7 @@ const UserNew: FunctionComponent = () => {
} }
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
Dashboard.navigate('useredit.html?userId=' + user.Id) Dashboard.navigate('/dashboard/users/profile?userId=' + user.Id)
.catch(err => { .catch(err => {
console.error('[usernew] failed to navigate to edit user page', err); console.error('[usernew] failed to navigate to edit user page', err);
}); });

View File

@ -85,21 +85,21 @@ const UserProfiles: FunctionComponent = () => {
callback: function (id: string) { callback: function (id: string) {
switch (id) { switch (id) {
case 'open': case 'open':
Dashboard.navigate('useredit.html?userId=' + userId) Dashboard.navigate('/dashboard/users/profile?userId=' + userId)
.catch(err => { .catch(err => {
console.error('[userprofiles] failed to navigate to user edit page', err); console.error('[userprofiles] failed to navigate to user edit page', err);
}); });
break; break;
case 'access': case 'access':
Dashboard.navigate('userlibraryaccess.html?userId=' + userId) Dashboard.navigate('/dashboard/users/access?userId=' + userId)
.catch(err => { .catch(err => {
console.error('[userprofiles] failed to navigate to user library page', err); console.error('[userprofiles] failed to navigate to user library page', err);
}); });
break; break;
case 'parentalcontrol': case 'parentalcontrol':
Dashboard.navigate('userparentalcontrol.html?userId=' + userId) Dashboard.navigate('/dashboard/users/parentalcontrol?userId=' + userId)
.catch(err => { .catch(err => {
console.error('[userprofiles] failed to navigate to parental control page', err); console.error('[userprofiles] failed to navigate to parental control page', err);
}); });
@ -146,7 +146,7 @@ const UserProfiles: FunctionComponent = () => {
}); });
(page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() { (page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
Dashboard.navigate('usernew.html') Dashboard.navigate('/dashboard/users/add')
.catch(err => { .catch(err => {
console.error('[userprofiles] failed to navigate to new user page', err); console.error('[userprofiles] failed to navigate to new user page', err);
}); });

View File

@ -32,7 +32,7 @@ const getCheckedElementDataIds = (elements: NodeListOf<Element>) => (
); );
function onSaveComplete() { function onSaveComplete() {
Dashboard.navigate('userprofiles.html') Dashboard.navigate('/dashboard/users')
.catch(err => { .catch(err => {
console.error('[useredit] failed to navigate to user profile', err); console.error('[useredit] failed to navigate to user profile', err);
}); });

View File

@ -1,16 +1,16 @@
import React from 'react'; import React from 'react';
import { Navigate, Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
import { REDIRECTS } from 'apps/stable/routes/_redirects'; import { REDIRECTS } from 'apps/stable/routes/_redirects';
import ConnectionRequired from 'components/ConnectionRequired'; import ConnectionRequired from 'components/ConnectionRequired';
import ServerContentPage from 'components/ServerContentPage';
import { toAsyncPageRoute } from 'components/router/AsyncRoute'; import { toAsyncPageRoute } from 'components/router/AsyncRoute';
import { toViewManagerPageRoute } from 'components/router/LegacyRoute'; import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
import { toRedirectRoute } from 'components/router/Redirect'; import { toRedirectRoute } from 'components/router/Redirect';
import AppLayout from './AppLayout'; import AppLayout from './AppLayout';
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes'; import { ASYNC_USER_ROUTES } from './routes/asyncRoutes';
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
const ExperimentalApp = () => { const ExperimentalApp = () => {
return ( return (
@ -22,16 +22,6 @@ const ExperimentalApp = () => {
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)} {LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
</Route> </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 */} {/* Public routes */}
<Route element={<ConnectionRequired isUserRequired={false} />}> <Route element={<ConnectionRequired isUserRequired={false} />}>
<Route index element={<Navigate replace to='/home.html' />} /> <Route index element={<Navigate replace to='/home.html' />} />
@ -42,6 +32,15 @@ const ExperimentalApp = () => {
{/* Redirects for old paths */} {/* Redirects for old paths */}
{REDIRECTS.map(toRedirectRoute)} {REDIRECTS.map(toRedirectRoute)}
{/* Ignore dashboard routes */}
{Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => (
<Route
key={key}
path={`/${path}/*`}
element={null}
/>
))}
</Routes> </Routes>
); );
}; };

View File

@ -10,11 +10,6 @@ $mui-bp-xl: 1536px;
position: relative; 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 // Hide some items from the user "settings" page that are in the drawer
#myPreferencesMenuPage { #myPreferencesMenuPage {
.lnkQuickConnectPreferences, .lnkQuickConnectPreferences,
@ -26,8 +21,7 @@ $mui-bp-xl: 1536px;
// Fix the padding of some pages // Fix the padding of some pages
.homePage.libraryPage, // Home page .homePage.libraryPage, // Home page
.libraryPage:not(.withTabs), // Tabless library pages .libraryPage:not(.withTabs) { // Tabless library pages
.content-primary.content-primary { // Dashboard pages
padding-top: 3.25rem !important; padding-top: 3.25rem !important;
} }

View File

@ -1,23 +1,15 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { Route, Routes, useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer'; import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from '../../routes/asyncRoutes'; import { ASYNC_USER_ROUTES } from '../../routes/asyncRoutes';
import { LEGACY_ADMIN_ROUTES, LEGACY_USER_ROUTES } from '../../routes/legacyRoutes'; import { 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 { isTabPath } from '../tabs/tabRoutes'; import { isTabPath } from '../tabs/tabRoutes';
export const DRAWER_WIDTH = 240; import MainDrawerContent from './MainDrawerContent';
const DRAWERLESS_ROUTES = [ const DRAWERLESS_ROUTES = [
'edititemmetadata.html', // metadata manager
'video' // video player 'video' // video player
]; ];
@ -26,77 +18,29 @@ const MAIN_DRAWER_ROUTES = [
...LEGACY_USER_ROUTES ...LEGACY_USER_ROUTES
].filter(route => !DRAWERLESS_ROUTES.includes(route.path)); ].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. */ /** Utility function to check if a path has a drawer. */
export const isDrawerPath = (path: string) => ( export const isDrawerPath = (path: string) => (
MAIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path) MAIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path)
|| ADMIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path)
); );
const Drawer: FC<ResponsiveDrawerProps> = ({ children, ...props }) => {
const location = useLocation();
const hasSecondaryToolBar = isTabPath(location.pathname);
return (
<ResponsiveDrawer
{...props}
hasSecondaryToolBar={hasSecondaryToolBar}
>
{children}
</ResponsiveDrawer>
);
};
const AppDrawer: FC<ResponsiveDrawerProps> = ({ const AppDrawer: FC<ResponsiveDrawerProps> = ({
open = false, open = false,
onClose, onClose,
onOpen onOpen
}) => ( }) => {
<Routes> const location = useLocation();
{ const hasSecondaryToolBar = isTabPath(location.pathname);
MAIN_DRAWER_ROUTES.map(route => (
<Route return (
key={route.path} <ResponsiveDrawer
path={route.path} hasSecondaryToolBar={hasSecondaryToolBar}
element={
<Drawer
open={open} open={open}
onClose={onClose} onClose={onClose}
onOpen={onOpen} onOpen={onOpen}
> >
<MainDrawerContent /> <MainDrawerContent />
</Drawer> </ResponsiveDrawer>
} );
/> };
))
}
{
ADMIN_DRAWER_ROUTES.map(route => (
<Route
key={route.path}
path={route.path}
element={
<Drawer
open={open}
onClose={onClose}
onOpen={onOpen}
>
<ServerDrawerSection />
<DevicesDrawerSection />
<LiveTvDrawerSection />
<AdvancedDrawerSection />
<PluginDrawerSection />
</Drawer>
}
/>
))
}
</Routes>
);
export default AppDrawer; export default AppDrawer;

View File

@ -150,7 +150,7 @@ const MainDrawerContent = () => {
} }
> >
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/dashboard.html'> <ListItemLink to='/dashboard'>
<ListItemIcon> <ListItemIcon>
<Dashboard /> <Dashboard />
</ListItemIcon> </ListItemIcon>
@ -158,7 +158,7 @@ const MainDrawerContent = () => {
</ListItemLink> </ListItemLink>
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemLink to='/edititemmetadata.html'> <ListItemLink to='/metadata'>
<ListItemIcon> <ListItemIcon>
<Edit /> <Edit />
</ListItemIcon> </ListItemIcon>

View File

@ -0,0 +1,34 @@
import React, { FC, useCallback } from 'react';
import { IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import globalize from 'scripts/globalize';
const NewCollectionButton: FC = () => {
const showCollectionEditor = useCallback(() => {
import('components/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);
});
}, []);
return (
<IconButton
title={globalize.translate('Add')}
className='paper-icon-button-light btnNewCollection autoSize'
onClick={showCollectionEditor}
>
<AddIcon />
</IconButton>
);
};
export default NewCollectionButton;

View File

@ -0,0 +1,57 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FC, useCallback } from 'react';
import { IconButton } from '@mui/material';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import { playbackManager } from 'components/playback/playbackmanager';
import globalize from 'scripts/globalize';
import { getFiltersQuery } from 'utils/items';
import { LibraryViewSettings } from 'types/library';
import { LibraryTab } from 'types/libraryTab';
interface PlayAllButtonProps {
item: BaseItemDto | undefined;
items: BaseItemDto[];
viewType: LibraryTab;
hasFilters: boolean;
libraryViewSettings: LibraryViewSettings
}
const PlayAllButton: FC<PlayAllButtonProps> = ({ item, items, viewType, hasFilters, libraryViewSettings }) => {
const play = useCallback(() => {
if (item && !hasFilters) {
playbackManager.play({
items: [item],
autoplay: true,
queryOptions: {
SortBy: [libraryViewSettings.SortBy],
SortOrder: [libraryViewSettings.SortOrder]
}
});
} else {
playbackManager.play({
items: items,
autoplay: true,
queryOptions: {
ParentId: item?.Id ?? undefined,
...getFiltersQuery(viewType, libraryViewSettings),
SortBy: [libraryViewSettings.SortBy],
SortOrder: [libraryViewSettings.SortOrder]
}
});
}
}, [hasFilters, item, items, libraryViewSettings, viewType]);
return (
<IconButton
title={globalize.translate('HeaderPlayAll')}
className='paper-icon-button-light btnPlay autoSize'
onClick={play}
>
<PlayArrowIcon />
</IconButton>
);
};
export default PlayAllButton;

View File

@ -0,0 +1,39 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FC, useCallback } from 'react';
import { IconButton } from '@mui/material';
import QueueIcon from '@mui/icons-material/Queue';
import { playbackManager } from 'components/playback/playbackmanager';
import globalize from 'scripts/globalize';
interface QueueButtonProps {
item: BaseItemDto | undefined
items: BaseItemDto[];
hasFilters: boolean;
}
const QueueButton: FC<QueueButtonProps> = ({ item, items, hasFilters }) => {
const queue = useCallback(() => {
if (item && !hasFilters) {
playbackManager.queue({
items: [item]
});
} else {
playbackManager.queue({
items: items
});
}
}, [hasFilters, item, items]);
return (
<IconButton
title={globalize.translate('AddToPlayQueue')}
className='paper-icon-button-light btnQueue autoSize'
onClick={queue}
>
<QueueIcon />
</IconButton>
);
};
export default QueueButton;

View File

@ -0,0 +1,49 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
import React, { FC, useCallback } from 'react';
import { IconButton } from '@mui/material';
import ShuffleIcon from '@mui/icons-material/Shuffle';
import { playbackManager } from 'components/playback/playbackmanager';
import globalize from 'scripts/globalize';
import { getFiltersQuery } from 'utils/items';
import { LibraryViewSettings } from 'types/library';
import { LibraryTab } from 'types/libraryTab';
interface ShuffleButtonProps {
item: BaseItemDto | undefined;
items: BaseItemDto[];
viewType: LibraryTab
hasFilters: boolean;
libraryViewSettings: LibraryViewSettings
}
const ShuffleButton: FC<ShuffleButtonProps> = ({ item, items, viewType, hasFilters, libraryViewSettings }) => {
const shuffle = useCallback(() => {
if (item && !hasFilters) {
playbackManager.shuffle(item);
} else {
playbackManager.play({
items: items,
autoplay: true,
queryOptions: {
ParentId: item?.Id ?? undefined,
...getFiltersQuery(viewType, libraryViewSettings),
SortBy: [ItemSortBy.Random]
}
});
}
}, [hasFilters, item, items, libraryViewSettings, viewType]);
return (
<IconButton
title={globalize.translate('Shuffle')}
className='paper-icon-button-light btnShuffle autoSize'
onClick={shuffle}
>
<ShuffleIcon />
</IconButton>
);
};
export default ShuffleButton;

View File

@ -98,7 +98,7 @@ const SortButton: FC<SortButtonProps> = ({
title={globalize.translate('Sort')} title={globalize.translate('Sort')}
sx={{ ml: 2 }} sx={{ ml: 2 }}
aria-describedby={id} aria-describedby={id}
className='paper-icon-button-light btnShuffle autoSize' className='paper-icon-button-light btnSort autoSize'
onClick={handleClick} onClick={handleClick}
> >
<SortByAlphaIcon /> <SortByAlphaIcon />

View File

@ -100,7 +100,7 @@ const ViewSettingsButton: FC<ViewSettingsButtonProps> = ({
title={globalize.translate('ButtonSelectView')} title={globalize.translate('ButtonSelectView')}
sx={{ ml: 2 }} sx={{ ml: 2 }}
aria-describedby={id} aria-describedby={id}
className='paper-icon-button-light btnShuffle autoSize' className='paper-icon-button-light btnSelectView autoSize'
onClick={handleClick} onClick={handleClick}
> >
<ViewComfyIcon /> <ViewComfyIcon />

View File

@ -1,12 +0,0 @@
import { AsyncRoute, AsyncRouteType } from 'components/router/AsyncRoute';
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'dashboard/activity', page: 'dashboard/activity', type: AsyncRouteType.Experimental },
{ 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' }
];

View File

@ -1,2 +1 @@
export * from './admin';
export * from './user'; export * from './user';

View File

@ -1,3 +1,2 @@
export * from './admin';
export * from './public'; export * from './public';
export * from './user'; export * from './user';

View File

@ -1,17 +1,17 @@
import React from 'react'; import React from 'react';
import { Navigate, Outlet, Route, Routes } from 'react-router-dom'; import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
import AppBody from 'components/AppBody'; import AppBody from 'components/AppBody';
import ServerContentPage from 'components/ServerContentPage';
import ConnectionRequired from 'components/ConnectionRequired'; import ConnectionRequired from 'components/ConnectionRequired';
import { toAsyncPageRoute } from 'components/router/AsyncRoute'; import { toAsyncPageRoute } from 'components/router/AsyncRoute';
import { toViewManagerPageRoute } from 'components/router/LegacyRoute'; 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';
import { REDIRECTS } from './routes/_redirects';
import { toRedirectRoute } from 'components/router/Redirect'; import { toRedirectRoute } from 'components/router/Redirect';
import { ASYNC_USER_ROUTES } from './routes/asyncRoutes';
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
import { REDIRECTS } from './routes/_redirects';
const Layout = () => ( const Layout = () => (
<AppBody> <AppBody>
<Outlet /> <Outlet />
@ -27,16 +27,6 @@ const StableApp = () => (
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)} {LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
</Route> </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 */} {/* Public routes */}
<Route path='/' element={<ConnectionRequired isUserRequired={false} />}> <Route path='/' element={<ConnectionRequired isUserRequired={false} />}>
<Route index element={<Navigate replace to='/home.html' />} /> <Route index element={<Navigate replace to='/home.html' />} />
@ -50,6 +40,15 @@ const StableApp = () => (
{/* Redirects for old paths */} {/* Redirects for old paths */}
{REDIRECTS.map(toRedirectRoute)} {REDIRECTS.map(toRedirectRoute)}
{/* Ignore dashboard routes */}
{Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => (
<Route
key={key}
path={`/${path}/*`}
element={null}
/>
))}
</Routes> </Routes>
); );

View File

@ -1,6 +1,5 @@
import type { Redirect } from 'components/router/Redirect'; import type { Redirect } from 'components/router/Redirect';
export const REDIRECTS: Redirect[] = [ export const REDIRECTS: Redirect[] = [
{ from: 'mypreferencesquickconnect.html', to: '/quickconnect' }, { from: 'mypreferencesquickconnect.html', to: '/quickconnect' }
{ from: 'serveractivity.html', to: '/dashboard/activity' }
]; ];

View File

@ -1,11 +0,0 @@
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' }
];

View File

@ -1,2 +1 @@
export * from './admin';
export * from './user'; export * from './user';

View File

@ -1,179 +0,0 @@
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: '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: 'dashboard/activity',
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'
}
}
];

View File

@ -1,3 +1,2 @@
export * from './admin';
export * from './public'; export * from './public';
export * from './user'; export * from './user';

View File

@ -5,24 +5,30 @@
*/ */
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import datetime from '../../scripts/datetime';
import imageLoader from '../images/imageLoader'; import cardBuilderUtils from './cardBuilderUtils';
import itemHelper from '../itemHelper'; import browser from 'scripts/browser';
import datetime from 'scripts/datetime';
import dom from 'scripts/dom';
import globalize from 'scripts/globalize';
import imageHelper from 'scripts/imagehelper';
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
import { randomInt } from 'utils/number';
import focusManager from '../focusManager'; import focusManager from '../focusManager';
import imageLoader from '../images/imageLoader';
import indicators from '../indicators/indicators'; import indicators from '../indicators/indicators';
import globalize from '../../scripts/globalize'; import itemHelper from '../itemHelper';
import layoutManager from '../layoutManager'; import layoutManager from '../layoutManager';
import dom from '../../scripts/dom';
import browser from '../../scripts/browser';
import { playbackManager } from '../playback/playbackmanager'; import { playbackManager } from '../playback/playbackmanager';
import itemShortcuts from '../shortcuts';
import imageHelper from '../../scripts/imagehelper';
import { randomInt } from '../../utils/number.ts';
import './card.scss';
import '../../elements/emby-button/paper-icon-button-light';
import '../guide/programs.scss';
import ServerConnections from '../ServerConnections';
import { appRouter } from '../router/appRouter'; import { appRouter } from '../router/appRouter';
import ServerConnections from '../ServerConnections';
import itemShortcuts from '../shortcuts';
import 'elements/emby-button/paper-icon-button-light';
import './card.scss';
import '../guide/programs.scss';
const enableFocusTransform = !browser.slow && !browser.edge; const enableFocusTransform = !browser.slow && !browser.edge;
@ -41,217 +47,6 @@ export function getCardsHtml(items, options) {
return buildCardsHtmlInternal(items, options); return buildCardsHtmlInternal(items, options);
} }
/**
* Computes the number of posters per row.
* @param {string} shape - Shape of the cards.
* @param {number} screenWidth - Width of the screen.
* @param {boolean} isOrientationLandscape - Flag for the orientation of the screen.
* @returns {number} Number of cards per row for an itemsContainer.
*/
function getPostersPerRow(shape, screenWidth, isOrientationLandscape) {
switch (shape) {
case 'portrait':
if (layoutManager.tv) {
return 100 / 16.66666667;
}
if (screenWidth >= 2200) {
return 100 / 10;
}
if (screenWidth >= 1920) {
return 100 / 11.1111111111;
}
if (screenWidth >= 1600) {
return 100 / 12.5;
}
if (screenWidth >= 1400) {
return 100 / 14.28571428571;
}
if (screenWidth >= 1200) {
return 100 / 16.66666667;
}
if (screenWidth >= 800) {
return 5;
}
if (screenWidth >= 700) {
return 4;
}
if (screenWidth >= 500) {
return 100 / 33.33333333;
}
return 100 / 33.33333333;
case 'square':
if (layoutManager.tv) {
return 100 / 16.66666667;
}
if (screenWidth >= 2200) {
return 100 / 10;
}
if (screenWidth >= 1920) {
return 100 / 11.1111111111;
}
if (screenWidth >= 1600) {
return 100 / 12.5;
}
if (screenWidth >= 1400) {
return 100 / 14.28571428571;
}
if (screenWidth >= 1200) {
return 100 / 16.66666667;
}
if (screenWidth >= 800) {
return 5;
}
if (screenWidth >= 700) {
return 4;
}
if (screenWidth >= 500) {
return 100 / 33.33333333;
}
return 2;
case 'banner':
if (screenWidth >= 2200) {
return 100 / 25;
}
if (screenWidth >= 1200) {
return 100 / 33.33333333;
}
if (screenWidth >= 800) {
return 2;
}
return 1;
case 'backdrop':
if (layoutManager.tv) {
return 100 / 25;
}
if (screenWidth >= 2500) {
return 6;
}
if (screenWidth >= 1600) {
return 5;
}
if (screenWidth >= 1200) {
return 4;
}
if (screenWidth >= 770) {
return 3;
}
if (screenWidth >= 420) {
return 2;
}
return 1;
case 'smallBackdrop':
if (screenWidth >= 1600) {
return 100 / 12.5;
}
if (screenWidth >= 1400) {
return 100 / 14.2857142857;
}
if (screenWidth >= 1200) {
return 100 / 16.66666667;
}
if (screenWidth >= 1000) {
return 5;
}
if (screenWidth >= 800) {
return 4;
}
if (screenWidth >= 500) {
return 100 / 33.33333333;
}
return 2;
case 'overflowSmallBackdrop':
if (layoutManager.tv) {
return 100 / 18.9;
}
if (isOrientationLandscape) {
if (screenWidth >= 800) {
return 100 / 15.5;
}
return 100 / 23.3;
} else {
if (screenWidth >= 540) {
return 100 / 30;
}
return 100 / 72;
}
case 'overflowPortrait':
if (layoutManager.tv) {
return 100 / 15.5;
}
if (isOrientationLandscape) {
if (screenWidth >= 1700) {
return 100 / 11.6;
}
return 100 / 15.5;
} else {
if (screenWidth >= 1400) {
return 100 / 15;
}
if (screenWidth >= 1200) {
return 100 / 18;
}
if (screenWidth >= 760) {
return 100 / 23;
}
if (screenWidth >= 400) {
return 100 / 31.5;
}
return 100 / 42;
}
case 'overflowSquare':
if (layoutManager.tv) {
return 100 / 15.5;
}
if (isOrientationLandscape) {
if (screenWidth >= 1700) {
return 100 / 11.6;
}
return 100 / 15.5;
} else {
if (screenWidth >= 1400) {
return 100 / 15;
}
if (screenWidth >= 1200) {
return 100 / 18;
}
if (screenWidth >= 760) {
return 100 / 23;
}
if (screenWidth >= 540) {
return 100 / 31.5;
}
return 100 / 42;
}
case 'overflowBackdrop':
if (layoutManager.tv) {
return 100 / 23.3;
}
if (isOrientationLandscape) {
if (screenWidth >= 1700) {
return 100 / 18.5;
}
return 100 / 23.3;
} else {
if (screenWidth >= 1800) {
return 100 / 23.5;
}
if (screenWidth >= 1400) {
return 100 / 30;
}
if (screenWidth >= 760) {
return 100 / 40;
}
if (screenWidth >= 640) {
return 100 / 56;
}
return 100 / 72;
}
default:
return 4;
}
}
/** /**
* Checks if the window is resizable. * Checks if the window is resizable.
* @param {number} windowWidth - Width of the device's screen. * @param {number} windowWidth - Width of the device's screen.
@ -278,7 +73,7 @@ function isResizable(windowWidth) {
* @returns {number} Width of the image for a card. * @returns {number} Width of the image for a card.
*/ */
function getImageWidth(shape, screenWidth, isOrientationLandscape) { function getImageWidth(shape, screenWidth, isOrientationLandscape) {
const imagesPerRow = getPostersPerRow(shape, screenWidth, isOrientationLandscape); const imagesPerRow = cardBuilderUtils.getPostersPerRow(shape, screenWidth, isOrientationLandscape, layoutManager.tv);
return Math.round(screenWidth / imagesPerRow); return Math.round(screenWidth / imagesPerRow);
} }
@ -301,16 +96,16 @@ function setCardData(items, options) {
options.shape = 'banner'; options.shape = 'banner';
options.coverImage = true; options.coverImage = true;
} else if (primaryImageAspectRatio >= 1.33) { } else if (primaryImageAspectRatio >= 1.33) {
options.shape = requestedShape === 'autooverflow' ? 'overflowBackdrop' : 'backdrop'; options.shape = getBackdropShape(requestedShape === 'autooverflow');
} else if (primaryImageAspectRatio > 0.71) { } else if (primaryImageAspectRatio > 0.71) {
options.shape = requestedShape === 'autooverflow' ? 'overflowSquare' : 'square'; options.shape = getSquareShape(requestedShape === 'autooverflow');
} else { } else {
options.shape = requestedShape === 'autooverflow' ? 'overflowPortrait' : 'portrait'; options.shape = getPortraitShape(requestedShape === 'autooverflow');
} }
} }
if (!options.shape) { if (!options.shape) {
options.shape = options.defaultShape || (requestedShape === 'autooverflow' ? 'overflowSquare' : 'square'); options.shape = options.defaultShape || getSquareShape(requestedShape === 'autooverflow');
} }
} }
@ -318,7 +113,7 @@ function setCardData(items, options) {
options.preferThumb = options.shape === 'backdrop' || options.shape === 'overflowBackdrop'; options.preferThumb = options.shape === 'backdrop' || options.shape === 'overflowBackdrop';
} }
options.uiAspect = getDesiredAspect(options.shape); options.uiAspect = cardBuilderUtils.getDesiredAspect(options.shape);
options.primaryImageAspectRatio = primaryImageAspectRatio; options.primaryImageAspectRatio = primaryImageAspectRatio;
if (!options.width && options.widths) { if (!options.width && options.widths) {
@ -460,30 +255,6 @@ function buildCardsHtmlInternal(items, options) {
return html; return html;
} }
/**
* Computes the aspect ratio for a card given its shape.
* @param {string} shape - Shape for which to get the aspect ratio.
* @returns {null|number} Ratio of the shape.
*/
function getDesiredAspect(shape) {
if (shape) {
shape = shape.toLowerCase();
if (shape.indexOf('portrait') !== -1) {
return (2 / 3);
}
if (shape.indexOf('backdrop') !== -1) {
return (16 / 9);
}
if (shape.indexOf('square') !== -1) {
return 1;
}
if (shape.indexOf('banner') !== -1) {
return (1000 / 185);
}
}
return null;
}
/** /**
* @typedef {Object} CardImageUrl * @typedef {Object} CardImageUrl
* @property {string} imgUrl - Image URL. * @property {string} imgUrl - Image URL.
@ -509,7 +280,7 @@ function getCardImageUrl(item, apiClient, options, shape) {
let imgUrl = null; let imgUrl = null;
let imgTag = null; let imgTag = null;
let coverImage = false; let coverImage = false;
const uiAspect = getDesiredAspect(shape); const uiAspect = cardBuilderUtils.getDesiredAspect(shape);
let imgType = null; let imgType = null;
let itemId = null; let itemId = null;

View File

@ -0,0 +1,173 @@
const ASPECT_RATIOS = {
portrait: (2 / 3),
backdrop: (16 / 9),
square: 1,
banner: (1000 / 185)
};
/**
* Computes the aspect ratio for a card given its shape.
* @param {string} shape - Shape for which to get the aspect ratio.
* @returns {null|number} Ratio of the shape.
*/
function getDesiredAspect(shape) {
if (!shape) {
return null;
}
shape = shape.toLowerCase();
if (shape.indexOf('portrait') !== -1) {
return ASPECT_RATIOS.portrait;
}
if (shape.indexOf('backdrop') !== -1) {
return ASPECT_RATIOS.backdrop;
}
if (shape.indexOf('square') !== -1) {
return ASPECT_RATIOS.square;
}
if (shape.indexOf('banner') !== -1) {
return ASPECT_RATIOS.banner;
}
return null;
}
/**
* Computes the number of posters per row.
* @param {string} shape - Shape of the cards.
* @param {number} screenWidth - Width of the screen.
* @param {boolean} isOrientationLandscape - Flag for the orientation of the screen.
* @param {boolean} isTV - Flag to denote if posters are rendered on a television screen.
* @returns {number} Number of cards per row for an itemsContainer.
*/
function getPostersPerRow(shape, screenWidth, isOrientationLandscape, isTV) {
switch (shape) {
case 'portrait': return postersPerRowPortrait(screenWidth, isTV);
case 'square': return postersPerRowSquare(screenWidth, isTV);
case 'banner': return postersPerRowBanner(screenWidth);
case 'backdrop': return postersPerRowBackdrop(screenWidth, isTV);
case 'smallBackdrop': return postersPerRowSmallBackdrop(screenWidth);
case 'overflowSmallBackdrop': return postersPerRowOverflowSmallBackdrop(screenWidth, isOrientationLandscape, isTV);
case 'overflowPortrait': return postersPerRowOverflowPortrait(screenWidth, isOrientationLandscape, isTV);
case 'overflowSquare': return postersPerRowOverflowSquare(screenWidth, isOrientationLandscape, isTV);
case 'overflowBackdrop': return postersPerRowOverflowBackdrop(screenWidth, isOrientationLandscape, isTV);
default: return 4;
}
}
const postersPerRowPortrait = (screenWidth, isTV) => {
switch (true) {
case isTV: return 100 / 16.66666667;
case screenWidth >= 2200: return 10;
case screenWidth >= 1920: return 100 / 11.1111111111;
case screenWidth >= 1600: return 8;
case screenWidth >= 1400: return 100 / 14.28571428571;
case screenWidth >= 1200: return 100 / 16.66666667;
case screenWidth >= 800: return 5;
case screenWidth >= 700: return 4;
case screenWidth >= 500: return 100 / 33.33333333;
default: return 100 / 33.33333333;
}
};
const postersPerRowSquare = (screenWidth, isTV) => {
switch (true) {
case isTV: return 100 / 16.66666667;
case screenWidth >= 2200: return 10;
case screenWidth >= 1920: return 100 / 11.1111111111;
case screenWidth >= 1600: return 8;
case screenWidth >= 1400: return 100 / 14.28571428571;
case screenWidth >= 1200: return 100 / 16.66666667;
case screenWidth >= 800: return 5;
case screenWidth >= 700: return 4;
case screenWidth >= 500: return 100 / 33.33333333;
default: return 2;
}
};
const postersPerRowBanner = (screenWidth) => {
switch (true) {
case screenWidth >= 2200: return 4;
case screenWidth >= 1200: return 100 / 33.33333333;
case screenWidth >= 800: return 2;
default: return 1;
}
};
const postersPerRowBackdrop = (screenWidth, isTV) => {
switch (true) {
case isTV: return 4;
case screenWidth >= 2500: return 6;
case screenWidth >= 1600: return 5;
case screenWidth >= 1200: return 4;
case screenWidth >= 770: return 3;
case screenWidth >= 420: return 2;
default: return 1;
}
};
function postersPerRowSmallBackdrop(screenWidth) {
switch (true) {
case screenWidth >= 1600: return 8;
case screenWidth >= 1400: return 100 / 14.2857142857;
case screenWidth >= 1200: return 100 / 16.66666667;
case screenWidth >= 1000: return 5;
case screenWidth >= 800: return 4;
case screenWidth >= 500: return 100 / 33.33333333;
default: return 2;
}
}
const postersPerRowOverflowSmallBackdrop = (screenWidth, isLandscape, isTV) => {
switch (true) {
case isTV: return 100 / 18.9;
case isLandscape && screenWidth >= 800: return 100 / 15.5;
case isLandscape: return 100 / 23.3;
case screenWidth >= 540: return 100 / 30;
default: return 100 / 72;
}
};
const postersPerRowOverflowPortrait = (screenWidth, isLandscape, isTV) => {
switch (true) {
case isTV: return 100 / 15.5;
case isLandscape && screenWidth >= 1700: return 100 / 11.6;
case isLandscape: return 100 / 15.5;
case screenWidth >= 1400: return 100 / 15;
case screenWidth >= 1200: return 100 / 18;
case screenWidth >= 760: return 100 / 23;
case screenWidth >= 400: return 100 / 31.5;
default: return 100 / 42;
}
};
const postersPerRowOverflowSquare = (screenWidth, isLandscape, isTV) => {
switch (true) {
case isTV: return 100 / 15.5;
case isLandscape && screenWidth >= 1700: return 100 / 11.6;
case isLandscape: return 100 / 15.5;
case screenWidth >= 1400: return 100 / 15;
case screenWidth >= 1200: return 100 / 18;
case screenWidth >= 760: return 100 / 23;
case screenWidth >= 540: return 100 / 31.5;
default: return 100 / 42;
}
};
const postersPerRowOverflowBackdrop = (screenWidth, isLandscape, isTV) => {
switch (true) {
case isTV: return 100 / 23.3;
case isLandscape && screenWidth >= 1700: return 100 / 18.5;
case isLandscape: return 100 / 23.3;
case screenWidth >= 1800: return 100 / 23.5;
case screenWidth >= 1400: return 100 / 30;
case screenWidth >= 760: return 100 / 40;
case screenWidth >= 640: return 100 / 56;
default: return 100 / 72;
}
};
export default {
getDesiredAspect,
getPostersPerRow
};

View File

@ -0,0 +1,417 @@
import { describe, expect, test } from 'vitest';
import cardBuilderUtils from './cardBuilderUtils';
describe('getDesiredAspect', () => {
test('"portrait" (case insensitive)', () => {
expect(cardBuilderUtils.getDesiredAspect('portrait')).toEqual((2 / 3));
expect(cardBuilderUtils.getDesiredAspect('PorTRaIt')).toEqual((2 / 3));
});
test('"backdrop" (case insensitive)', () => {
expect(cardBuilderUtils.getDesiredAspect('backdrop')).toEqual((16 / 9));
expect(cardBuilderUtils.getDesiredAspect('BaCkDroP')).toEqual((16 / 9));
});
test('"square" (case insensitive)', () => {
expect(cardBuilderUtils.getDesiredAspect('square')).toEqual(1);
expect(cardBuilderUtils.getDesiredAspect('sQuArE')).toEqual(1);
});
test('"banner" (case insensitive)', () => {
expect(cardBuilderUtils.getDesiredAspect('banner')).toEqual((1000 / 185));
expect(cardBuilderUtils.getDesiredAspect('BaNnEr')).toEqual((1000 / 185));
});
test('invalid shape', () => {
expect(cardBuilderUtils.getDesiredAspect('invalid')).toBeNull();
});
test('shape is not provided', () => {
expect(cardBuilderUtils.getDesiredAspect('')).toBeNull();
});
});
describe('getPostersPerRow', () => {
test('resolves to default of 4 posters per row if shape is not provided', () => {
expect(cardBuilderUtils.getPostersPerRow('', 0, false, false)).toEqual(4);
});
describe('portrait', () => {
const postersPerRowForPortrait = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('portrait', screenWidth, false, isTV));
test('television', () => {
expect(postersPerRowForPortrait(0, true)).toEqual(100 / 16.66666667);
});
test('screen width less than 500px', () => {
expect(postersPerRowForPortrait(100, false)).toEqual(100 / 33.33333333);
expect(postersPerRowForPortrait(499, false)).toEqual(100 / 33.33333333);
});
test('screen width greater or equal to 500px', () => {
expect(postersPerRowForPortrait(500, false)).toEqual(100 / 33.33333333);
expect(postersPerRowForPortrait(501, false)).toEqual(100 / 33.33333333);
});
test('screen width greater or equal to 700px', () => {
expect(postersPerRowForPortrait(700, false)).toEqual(4);
expect(postersPerRowForPortrait(701, false)).toEqual(4);
});
test('screen width greater or equal to 800px', () => {
expect(postersPerRowForPortrait(800, false)).toEqual(5);
expect(postersPerRowForPortrait(801, false)).toEqual(5);
});
test('screen width greater or equal to 1200px', () => {
expect(postersPerRowForPortrait(1200, false)).toEqual(100 / 16.66666667);
expect(postersPerRowForPortrait(1201, false)).toEqual(100 / 16.66666667);
});
test('screen width greater or equal to 1400px', () => {
expect(postersPerRowForPortrait(1400, false)).toEqual( 100 / 14.28571428571);
expect(postersPerRowForPortrait(1401, false)).toEqual( 100 / 14.28571428571);
});
test('screen width greater or equal to 1600px', () => {
expect(postersPerRowForPortrait(1600, false)).toEqual( 8);
expect(postersPerRowForPortrait(1601, false)).toEqual( 8);
});
test('screen width greater or equal to 1920px', () => {
expect(postersPerRowForPortrait(1920, false)).toEqual( 100 / 11.1111111111);
expect(postersPerRowForPortrait(1921, false)).toEqual( 100 / 11.1111111111);
});
test('screen width greater or equal to 2200px', () => {
expect(postersPerRowForPortrait(2200, false)).toEqual( 10);
expect(postersPerRowForPortrait(2201, false)).toEqual( 10);
});
});
describe('square', () => {
const postersPerRowForSquare = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('square', screenWidth, false, isTV));
test('television', () => {
expect(postersPerRowForSquare(0, true)).toEqual(100 / 16.66666667);
});
test('screen width less than 500px', () => {
expect(postersPerRowForSquare(100, false)).toEqual(2);
expect(postersPerRowForSquare(499, false)).toEqual(2);
});
test('screen width greater or equal to 500px', () => {
expect(postersPerRowForSquare(500, false)).toEqual(100 / 33.33333333);
expect(postersPerRowForSquare(501, false)).toEqual(100 / 33.33333333);
});
test('screen width greater or equal to 700px', () => {
expect(postersPerRowForSquare(700, false)).toEqual(4);
expect(postersPerRowForSquare(701, false)).toEqual(4);
});
test('screen width greater or equal to 800px', () => {
expect(postersPerRowForSquare(800, false)).toEqual(5);
expect(postersPerRowForSquare(801, false)).toEqual(5);
});
test('screen width greater or equal to 1200px', () => {
expect(postersPerRowForSquare(1200, false)).toEqual(100 / 16.66666667);
expect(postersPerRowForSquare(1201, false)).toEqual(100 / 16.66666667);
});
test('screen width greater or equal to 1400px', () => {
expect(postersPerRowForSquare(1400, false)).toEqual( 100 / 14.28571428571);
expect(postersPerRowForSquare(1401, false)).toEqual( 100 / 14.28571428571);
});
test('screen width greater or equal to 1600px', () => {
expect(postersPerRowForSquare(1600, false)).toEqual(8);
expect(postersPerRowForSquare(1601, false)).toEqual(8);
});
test('screen width greater or equal to 1920px', () => {
expect(postersPerRowForSquare(1920, false)).toEqual(100 / 11.1111111111);
expect(postersPerRowForSquare(1921, false)).toEqual(100 / 11.1111111111);
});
test('screen width greater or equal to 2200px', () => {
expect(postersPerRowForSquare(2200, false)).toEqual( 10);
expect(postersPerRowForSquare(2201, false)).toEqual( 10);
});
});
describe('banner', () => {
const postersPerRowForBanner = (screenWidth) => (cardBuilderUtils.getPostersPerRow('banner', screenWidth, false, false));
test('screen width less than 800px', () => {
expect(postersPerRowForBanner(799)).toEqual(1);
});
test('screen width greater than or equal to 800px', () => {
expect(postersPerRowForBanner(800)).toEqual(2);
expect(postersPerRowForBanner(801)).toEqual(2);
});
test('screen width greater than or equal to 1200px', () => {
expect(postersPerRowForBanner(1200)).toEqual(100 / 33.33333333);
expect(postersPerRowForBanner(1201)).toEqual(100 / 33.33333333);
});
test('screen width greater than or equal to 2200px', () => {
expect(postersPerRowForBanner(2200)).toEqual(4);
expect(postersPerRowForBanner(2201)).toEqual(4);
});
});
describe('backdrop', () => {
const postersPerRowForBackdrop = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('backdrop', screenWidth, false, isTV));
test('television', () => {
expect(postersPerRowForBackdrop(0, true)).toEqual(4);
});
test('screen width less than 420px', () => {
expect(postersPerRowForBackdrop(100, false)).toEqual(1);
expect(postersPerRowForBackdrop(419, false)).toEqual(1);
});
test('screen width greater or equal to 420px', () => {
expect(postersPerRowForBackdrop(420, false)).toEqual(2);
expect(postersPerRowForBackdrop(421, false)).toEqual(2);
});
test('screen width greater or equal to 770px', () => {
expect(postersPerRowForBackdrop(770, false)).toEqual(3);
expect(postersPerRowForBackdrop(771, false)).toEqual(3);
});
test('screen width greater or equal to 1200px', () => {
expect(postersPerRowForBackdrop(1200, false)).toEqual(4);
expect(postersPerRowForBackdrop(1201, false)).toEqual(4);
});
test('screen width greater or equal to 1600px', () => {
expect(postersPerRowForBackdrop(1600, false)).toEqual(5);
expect(postersPerRowForBackdrop(1601, false)).toEqual(5);
});
test('screen width greater or equal to 2500px', () => {
expect(postersPerRowForBackdrop(2500, false)).toEqual(6);
expect(postersPerRowForBackdrop(2501, false)).toEqual(6);
});
});
describe('small backdrop', () => {
const postersPerRowForSmallBackdrop = (screenWidth) => (cardBuilderUtils.getPostersPerRow('smallBackdrop', screenWidth, false, false));
test('screen width less than 500px', () => {
expect(postersPerRowForSmallBackdrop(100)).toEqual(2);
expect(postersPerRowForSmallBackdrop(499)).toEqual(2);
});
test('screen width greater or equal to 500px', () => {
expect(postersPerRowForSmallBackdrop(500)).toEqual(100 / 33.33333333);
expect(postersPerRowForSmallBackdrop(501)).toEqual(100 / 33.33333333);
});
test('screen width greater or equal to 800px', () => {
expect(postersPerRowForSmallBackdrop(800)).toEqual(4);
expect(postersPerRowForSmallBackdrop(801)).toEqual(4);
});
test('screen width greater or equal to 1000px', () => {
expect(postersPerRowForSmallBackdrop(1000)).toEqual(5);
expect(postersPerRowForSmallBackdrop(1001)).toEqual(5);
});
test('screen width greater or equal to 1200px', () => {
expect(postersPerRowForSmallBackdrop(1200)).toEqual(100 / 16.66666667);
expect(postersPerRowForSmallBackdrop(1201)).toEqual(100 / 16.66666667);
});
test('screen width greater or equal to 1400px', () => {
expect(postersPerRowForSmallBackdrop(1400)).toEqual(100 / 14.2857142857);
expect(postersPerRowForSmallBackdrop(1401)).toEqual(100 / 14.2857142857);
});
test('screen width greater or equal to 1600px', () => {
expect(postersPerRowForSmallBackdrop(1600)).toEqual(8);
expect(postersPerRowForSmallBackdrop(1601)).toEqual(8);
});
});
describe('overflow small backdrop', () => {
const postersPerRowForOverflowSmallBackdrop = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowSmallBackdrop', screenWidth, isLandscape, isTV));
test('television', () => {
expect(postersPerRowForOverflowSmallBackdrop(0, false, true)).toEqual( 100 / 18.9);
});
describe('non-landscape', () => {
test('screen width greater or equal to 540px', () => {
expect(postersPerRowForOverflowSmallBackdrop(540, false)).toEqual(100 / 30);
expect(postersPerRowForOverflowSmallBackdrop(541, false)).toEqual(100 / 30);
});
test('screen width is less than 540px', () => {
expect(postersPerRowForOverflowSmallBackdrop(539, false)).toEqual(100 / 72);
expect(postersPerRowForOverflowSmallBackdrop(100, false)).toEqual(100 / 72);
});
});
describe('landscape', () => {
test('screen width greater or equal to 800px', () => {
expect(postersPerRowForOverflowSmallBackdrop(800, true)).toEqual(100 / 15.5);
expect(postersPerRowForOverflowSmallBackdrop(801, true)).toEqual(100 / 15.5);
});
test('screen width is less than 800px', () => {
expect(postersPerRowForOverflowSmallBackdrop(799, true)).toEqual(100 / 23.3);
expect(postersPerRowForOverflowSmallBackdrop(100, true)).toEqual(100 / 23.3);
});
});
});
describe('overflow portrait', () => {
const postersPerRowForOverflowPortrait = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowPortrait', screenWidth, isLandscape, isTV));
test('television', () => {
expect(postersPerRowForOverflowPortrait(0, false, true)).toEqual( 100 / 15.5);
});
describe('non-landscape', () => {
test('screen width greater or equal to 1400px', () => {
expect(postersPerRowForOverflowPortrait(1400, false)).toEqual(100 / 15);
expect(postersPerRowForOverflowPortrait(1401, false)).toEqual(100 / 15);
});
test('screen width greater or equal to 1200px', () => {
expect(postersPerRowForOverflowPortrait(1200, false)).toEqual(100 / 18);
expect(postersPerRowForOverflowPortrait(1201, false)).toEqual(100 / 18);
});
test('screen width greater or equal to 760px', () => {
expect(postersPerRowForOverflowPortrait(760, false)).toEqual(100 / 23);
expect(postersPerRowForOverflowPortrait(761, false)).toEqual(100 / 23);
});
test('screen width greater or equal to 400px', () => {
expect(postersPerRowForOverflowPortrait(400, false)).toEqual(100 / 31.5);
expect(postersPerRowForOverflowPortrait(401, false)).toEqual(100 / 31.5);
});
test('screen width is less than 400px', () => {
expect(postersPerRowForOverflowPortrait(399, false)).toEqual(100 / 42);
expect(postersPerRowForOverflowPortrait(100, false)).toEqual(100 / 42);
});
});
describe('landscape', () => {
test('screen width greater or equal to 1700px', () => {
expect(postersPerRowForOverflowPortrait(1700, true)).toEqual(100 / 11.6);
expect(postersPerRowForOverflowPortrait(1701, true)).toEqual(100 / 11.6);
});
test('screen width is less than 1700px', () => {
expect(postersPerRowForOverflowPortrait(1699, true)).toEqual(100 / 15.5);
expect(postersPerRowForOverflowPortrait(100, true)).toEqual(100 / 15.5);
});
});
});
describe('overflow square', () => {
const postersPerRowForOverflowSquare = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowSquare', screenWidth, isLandscape, isTV));
test('television', () => {
expect(postersPerRowForOverflowSquare(0, false, true)).toEqual( 100 / 15.5);
});
describe('non-landscape', () => {
test('screen width greater or equal to 1400px', () => {
expect(postersPerRowForOverflowSquare(1400, false)).toEqual(100 / 15);
expect(postersPerRowForOverflowSquare(1401, false)).toEqual(100 / 15);
});
test('screen width greater or equal to 1200px', () => {
expect(postersPerRowForOverflowSquare(1200, false)).toEqual(100 / 18);
expect(postersPerRowForOverflowSquare(1201, false)).toEqual(100 / 18);
});
test('screen width greater or equal to 760px', () => {
expect(postersPerRowForOverflowSquare(760, false)).toEqual(100 / 23);
expect(postersPerRowForOverflowSquare(761, false)).toEqual(100 / 23);
});
test('screen width greater or equal to 540px', () => {
expect(postersPerRowForOverflowSquare(540, false)).toEqual(100 / 31.5);
expect(postersPerRowForOverflowSquare(541, false)).toEqual(100 / 31.5);
});
test('screen width is less than 540px', () => {
expect(postersPerRowForOverflowSquare(539, false)).toEqual(100 / 42);
expect(postersPerRowForOverflowSquare(100, false)).toEqual(100 / 42);
});
});
describe('landscape', () => {
test('screen width greater or equal to 1700px', () => {
expect(postersPerRowForOverflowSquare(1700, true)).toEqual(100 / 11.6);
expect(postersPerRowForOverflowSquare(1701, true)).toEqual(100 / 11.6);
});
test('screen width is less than 1700px', () => {
expect(postersPerRowForOverflowSquare(1699, true)).toEqual(100 / 15.5);
expect(postersPerRowForOverflowSquare(100, true)).toEqual(100 / 15.5);
});
});
});
describe('overflow backdrop', () => {
const postersPerRowForOverflowBackdrop = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowBackdrop', screenWidth, isLandscape, isTV));
test('television', () => {
expect(postersPerRowForOverflowBackdrop(0, false, true)).toEqual( 100 / 23.3);
});
describe('non-landscape', () => {
test('screen width greater or equal to 1800px', () => {
expect(postersPerRowForOverflowBackdrop(1800, false)).toEqual(100 / 23.5);
expect(postersPerRowForOverflowBackdrop(1801, false)).toEqual(100 / 23.5);
});
test('screen width greater or equal to 1400px', () => {
expect(postersPerRowForOverflowBackdrop(1400, false)).toEqual(100 / 30);
expect(postersPerRowForOverflowBackdrop(1401, false)).toEqual(100 / 30);
});
test('screen width greater or equal to 760px', () => {
expect(postersPerRowForOverflowBackdrop(760, false)).toEqual(100 / 40);
expect(postersPerRowForOverflowBackdrop(761, false)).toEqual(100 / 40);
});
test('screen width greater or equal to 640px', () => {
expect(postersPerRowForOverflowBackdrop(640, false)).toEqual(100 / 56);
expect(postersPerRowForOverflowBackdrop(641, false)).toEqual(100 / 56);
});
test('screen width is less than 640px', () => {
expect(postersPerRowForOverflowBackdrop(639, false)).toEqual(100 / 72);
expect(postersPerRowForOverflowBackdrop(100, false)).toEqual(100 / 72);
});
});
describe('landscape', () => {
test('screen width greater or equal to 1700px', () => {
expect(postersPerRowForOverflowBackdrop(1700, true)).toEqual(100 / 18.5);
expect(postersPerRowForOverflowBackdrop(1701, true)).toEqual(100 / 18.5);
});
test('screen width is less than 1700px', () => {
expect(postersPerRowForOverflowBackdrop(1699, true)).toEqual(100 / 23.3);
expect(postersPerRowForOverflowBackdrop(100, true)).toEqual(100 / 23.3);
});
});
});
});

View File

@ -10,28 +10,28 @@ const createLinkElement = (activeTab: string) => ({
is="emby-linkbutton" is="emby-linkbutton"
data-role="button" data-role="button"
class="${activeTab === 'useredit' ? 'ui-btn-active' : ''}" class="${activeTab === 'useredit' ? 'ui-btn-active' : ''}"
onclick="Dashboard.navigate('useredit.html', true);"> onclick="Dashboard.navigate('/dashboard/users/profile', true);">
${globalize.translate('Profile')} ${globalize.translate('Profile')}
</a> </a>
<a href="#" <a href="#"
is="emby-linkbutton" is="emby-linkbutton"
data-role="button" data-role="button"
class="${activeTab === 'userlibraryaccess' ? 'ui-btn-active' : ''}" class="${activeTab === 'userlibraryaccess' ? 'ui-btn-active' : ''}"
onclick="Dashboard.navigate('userlibraryaccess.html', true);"> onclick="Dashboard.navigate('/dashboard/users/access', true);">
${globalize.translate('TabAccess')} ${globalize.translate('TabAccess')}
</a> </a>
<a href="#" <a href="#"
is="emby-linkbutton" is="emby-linkbutton"
data-role="button" data-role="button"
class="${activeTab === 'userparentalcontrol' ? 'ui-btn-active' : ''}" class="${activeTab === 'userparentalcontrol' ? 'ui-btn-active' : ''}"
onclick="Dashboard.navigate('userparentalcontrol.html', true);"> onclick="Dashboard.navigate('/dashboard/users/parentalcontrol', true);">
${globalize.translate('TabParentalControl')} ${globalize.translate('TabParentalControl')}
</a> </a>
<a href="#" <a href="#"
is="emby-linkbutton" is="emby-linkbutton"
data-role="button" data-role="button"
class="${activeTab === 'userpassword' ? 'ui-btn-active' : ''}" class="${activeTab === 'userpassword' ? 'ui-btn-active' : ''}"
onclick="Dashboard.navigate('userpassword.html', true);"> onclick="Dashboard.navigate('/dashboard/users/password', true);">
${globalize.translate('HeaderPassword')} ${globalize.translate('HeaderPassword')}
</a>` </a>`
}); });

View File

@ -11,7 +11,7 @@ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl
__html: `<a __html: `<a
is="emby-linkbutton" is="emby-linkbutton"
class="cardContent" class="cardContent"
href="#/useredit.html?userId=${user.Id}" href="#/dashboard/users/profile?userId=${user.Id}"
> >
${renderImgUrl} ${renderImgUrl}
</a>` </a>`

View File

@ -1,50 +1,42 @@
import loading from './loading/loading'; import dom from 'scripts/dom';
import cardBuilder from './cardbuilder/cardBuilder'; import globalize from 'scripts/globalize';
import dom from '../scripts/dom'; import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
import { getParameterByName } from 'utils/url';
import { appHost } from './apphost'; import { appHost } from './apphost';
import cardBuilder from './cardbuilder/cardBuilder';
import imageLoader from './images/imageLoader'; import imageLoader from './images/imageLoader';
import globalize from '../scripts/globalize';
import layoutManager from './layoutManager'; import layoutManager from './layoutManager';
import { getParameterByName } from '../utils/url.ts'; import loading from './loading/loading';
import '../styles/scrollstyles.scss';
import '../elements/emby-itemscontainer/emby-itemscontainer'; import 'elements/emby-itemscontainer/emby-itemscontainer';
import 'styles/scrollstyles.scss';
function enableScrollX() { function enableScrollX() {
return !layoutManager.desktop; return !layoutManager.desktop;
} }
function getThumbShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
function getPosterShape() {
return enableScrollX() ? 'overflowPortrait' : 'portrait';
}
function getSquareShape() {
return enableScrollX() ? 'overflowSquare' : 'square';
}
function getSections() { function getSections() {
return [{ return [{
name: 'Movies', name: 'Movies',
types: 'Movie', types: 'Movie',
id: 'favoriteMovies', id: 'favoriteMovies',
shape: getPosterShape(), shape: getPortraitShape(enableScrollX()),
showTitle: false, showTitle: false,
overlayPlayButton: true overlayPlayButton: true
}, { }, {
name: 'Shows', name: 'Shows',
types: 'Series', types: 'Series',
id: 'favoriteShows', id: 'favoriteShows',
shape: getPosterShape(), shape: getPortraitShape(enableScrollX()),
showTitle: false, showTitle: false,
overlayPlayButton: true overlayPlayButton: true
}, { }, {
name: 'Episodes', name: 'Episodes',
types: 'Episode', types: 'Episode',
id: 'favoriteEpisode', id: 'favoriteEpisode',
shape: getThumbShape(), shape: getBackdropShape(enableScrollX()),
preferThumb: false, preferThumb: false,
showTitle: true, showTitle: true,
showParentTitle: true, showParentTitle: true,
@ -55,7 +47,7 @@ function getSections() {
name: 'Videos', name: 'Videos',
types: 'Video,MusicVideo', types: 'Video,MusicVideo',
id: 'favoriteVideos', id: 'favoriteVideos',
shape: getThumbShape(), shape: getBackdropShape(enableScrollX()),
preferThumb: true, preferThumb: true,
showTitle: true, showTitle: true,
overlayPlayButton: true, overlayPlayButton: true,
@ -65,7 +57,7 @@ function getSections() {
name: 'Artists', name: 'Artists',
types: 'MusicArtist', types: 'MusicArtist',
id: 'favoriteArtists', id: 'favoriteArtists',
shape: getSquareShape(), shape: getSquareShape(enableScrollX()),
preferThumb: false, preferThumb: false,
showTitle: true, showTitle: true,
overlayText: false, overlayText: false,
@ -77,7 +69,7 @@ function getSections() {
name: 'Albums', name: 'Albums',
types: 'MusicAlbum', types: 'MusicAlbum',
id: 'favoriteAlbums', id: 'favoriteAlbums',
shape: getSquareShape(), shape: getSquareShape(enableScrollX()),
preferThumb: false, preferThumb: false,
showTitle: true, showTitle: true,
overlayText: false, overlayText: false,
@ -89,7 +81,7 @@ function getSections() {
name: 'Songs', name: 'Songs',
types: 'Audio', types: 'Audio',
id: 'favoriteSongs', id: 'favoriteSongs',
shape: getSquareShape(), shape: getSquareShape(enableScrollX()),
preferThumb: false, preferThumb: false,
showTitle: true, showTitle: true,
overlayText: false, overlayText: false,

View File

@ -1,18 +1,23 @@
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import globalize from 'scripts/globalize';
import imageHelper from 'scripts/imagehelper';
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
import Dashboard from 'utils/dashboard';
import cardBuilder from '../cardbuilder/cardBuilder'; import cardBuilder from '../cardbuilder/cardBuilder';
import layoutManager from '../layoutManager';
import imageLoader from '../images/imageLoader'; import imageLoader from '../images/imageLoader';
import globalize from '../../scripts/globalize'; import layoutManager from '../layoutManager';
import { appRouter } from '../router/appRouter'; import { appRouter } from '../router/appRouter';
import imageHelper from '../../scripts/imagehelper';
import '../../elements/emby-button/paper-icon-button-light';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
import '../../elements/emby-scroller/emby-scroller';
import '../../elements/emby-button/emby-button';
import './homesections.scss';
import Dashboard from '../../utils/dashboard';
import ServerConnections from '../ServerConnections'; import ServerConnections from '../ServerConnections';
import 'elements/emby-button/paper-icon-button-light';
import 'elements/emby-itemscontainer/emby-itemscontainer';
import 'elements/emby-scroller/emby-scroller';
import 'elements/emby-button/emby-button';
import './homesections.scss';
export function getDefaultSection(index) { export function getDefaultSection(index) {
switch (index) { switch (index) {
case 0: case 0:
@ -94,7 +99,7 @@ export function loadSections(elem, apiClient, user, userSettings) {
const createNowLink = elem.querySelector('#button-createLibrary'); const createNowLink = elem.querySelector('#button-createLibrary');
if (createNowLink) { if (createNowLink) {
createNowLink.addEventListener('click', function () { createNowLink.addEventListener('click', function () {
Dashboard.navigate('library.html'); Dashboard.navigate('dashboard/libraries');
}); });
} }
} }
@ -169,18 +174,6 @@ function enableScrollX() {
return true; return true;
} }
function getSquareShape() {
return enableScrollX() ? 'overflowSquare' : 'square';
}
function getThumbShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
function getPortraitShape() {
return enableScrollX() ? 'overflowPortrait' : 'portrait';
}
function getLibraryButtonsHtml(items) { function getLibraryButtonsHtml(items) {
let html = ''; let html = '';
@ -244,11 +237,11 @@ function getLatestItemsHtmlFn(itemType, viewType) {
const cardLayout = false; const cardLayout = false;
let shape; let shape;
if (itemType === 'Channel' || viewType === 'movies' || viewType === 'books' || viewType === 'tvshows') { if (itemType === 'Channel' || viewType === 'movies' || viewType === 'books' || viewType === 'tvshows') {
shape = getPortraitShape(); shape = getPortraitShape(enableScrollX());
} else if (viewType === 'music' || viewType === 'homevideos') { } else if (viewType === 'music' || viewType === 'homevideos') {
shape = getSquareShape(); shape = getSquareShape(enableScrollX());
} else { } else {
shape = getThumbShape(); shape = getBackdropShape(enableScrollX());
} }
return cardBuilder.getCardsHtml({ return cardBuilder.getCardsHtml({
@ -345,7 +338,7 @@ export function loadLibraryTiles(elem, apiClient, user, userSettings, shape, use
html += cardBuilder.getCardsHtml({ html += cardBuilder.getCardsHtml({
items: userViews, items: userViews,
shape: getThumbShape(), shape: getBackdropShape(enableScrollX()),
showTitle: true, showTitle: true,
centerText: true, centerText: true,
overlayText: false, overlayText: false,
@ -423,7 +416,9 @@ function getItemsToResumeHtmlFn(useEpisodeImages, mediaType) {
items: items, items: items,
preferThumb: true, preferThumb: true,
inheritThumb: !useEpisodeImages, inheritThumb: !useEpisodeImages,
shape: (mediaType === 'Book') ? getPortraitShape() : getThumbShape(), shape: (mediaType === 'Book') ?
getPortraitShape(enableScrollX()) :
getBackdropShape(enableScrollX()),
overlayText: false, overlayText: false,
showTitle: true, showTitle: true,
showParentTitle: true, showParentTitle: true,
@ -471,7 +466,7 @@ function getOnNowItemsHtml(items) {
showChannelName: false, showChannelName: false,
showAirDateTime: false, showAirDateTime: false,
showAirEndTime: true, showAirEndTime: true,
defaultShape: getThumbShape(), defaultShape: getBackdropShape(enableScrollX()),
lines: 3, lines: 3,
overlayPlayButton: true overlayPlayButton: true
}); });
@ -614,7 +609,7 @@ function getNextUpItemsHtmlFn(useEpisodeImages) {
items: items, items: items,
preferThumb: true, preferThumb: true,
inheritThumb: !useEpisodeImages, inheritThumb: !useEpisodeImages,
shape: getThumbShape(), shape: getBackdropShape(enableScrollX()),
overlayText: false, overlayText: false,
showTitle: true, showTitle: true,
showParentTitle: true, showParentTitle: true,

View File

@ -444,7 +444,7 @@ function executeCommand(item, id, options) {
}); });
break; break;
case 'multiSelect': case 'multiSelect':
import('./multiSelect/multiSelect').then(({ startMultiSelect: startMultiSelect }) => { import('./multiSelect/multiSelect').then(({ startMultiSelect }) => {
const card = dom.parentWithClass(options.positionTo, 'card'); const card = dom.parentWithClass(options.positionTo, 'card');
startMultiSelect(card); startMultiSelect(card);
}); });

View File

@ -527,7 +527,7 @@ class AppRouter {
} }
if (item === 'manageserver') { if (item === 'manageserver') {
return '#/dashboard.html'; return '#/dashboard';
} }
if (item === 'recordedtv') { if (item === 'recordedtv') {

View File

@ -115,7 +115,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
<MenuItem <MenuItem
key='admin-dashboard-link' key='admin-dashboard-link'
component={Link} component={Link}
to='/dashboard.html' to='/dashboard'
onClick={onMenuClose} onClick={onMenuClose}
> >
@ -127,7 +127,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
<MenuItem <MenuItem
key='admin-metadata-link' key='admin-metadata-link'
component={Link} component={Link}
to='/edititemmetadata.html' to='/metadata'
onClick={onMenuClose} onClick={onMenuClose}
> >
<ListItemIcon> <ListItemIcon>

View File

@ -3,7 +3,7 @@
<div class="dashboardSections" style="padding-top:.5em;"> <div class="dashboardSections" style="padding-top:.5em;">
<div class="dashboardColumn dashboardColumn-2-60 dashboardColumn-3-46"> <div class="dashboardColumn dashboardColumn-2-60 dashboardColumn-3-46">
<div class="dashboardSection"> <div class="dashboardSection">
<a is="emby-linkbutton" href="#/dashboardgeneral.html" class="button-flat sectionTitleTextButton"> <a is="emby-linkbutton" href="#/dashboard/settings" class="button-flat sectionTitleTextButton">
<h3>${TabServer}</h3> <h3>${TabServer}</h3>
<span class="material-icons chevron_right" aria-hidden="true"></span> <span class="material-icons chevron_right" aria-hidden="true"></span>
</a> </a>
@ -33,7 +33,7 @@
</div> </div>
<div class="dashboardSection"> <div class="dashboardSection">
<a is="emby-linkbutton" href="#/devices.html" class="button-flat sectionTitleTextButton"> <a is="emby-linkbutton" href="#/dashboard/devices" class="button-flat sectionTitleTextButton">
<h3>${HeaderActiveDevices}</h3> <h3>${HeaderActiveDevices}</h3>
<span class="material-icons chevron_right" aria-hidden="true"></span> <span class="material-icons chevron_right" aria-hidden="true"></span>
</a> </a>
@ -70,7 +70,7 @@
</div> </div>
<div class="dashboardSection"> <div class="dashboardSection">
<a is="emby-linkbutton" href="#/dashboardgeneral.html" class="button-flat sectionTitleTextButton"> <a is="emby-linkbutton" href="#/dashboard/settings" class="button-flat sectionTitleTextButton">
<h3>${HeaderPaths}</h3> <h3>${HeaderPaths}</h3>
<span class="material-icons chevron_right" aria-hidden="true"></span> <span class="material-icons chevron_right" aria-hidden="true"></span>
</a> </a>

View File

@ -73,7 +73,7 @@ function showDeviceMenu(view, btn, deviceId) {
callback: function (id) { callback: function (id) {
switch (id) { switch (id) {
case 'open': case 'open':
Dashboard.navigate('device.html?id=' + deviceId); Dashboard.navigate('dashboard/devices/edit?id=' + deviceId);
break; break;
case 'delete': case 'delete':
@ -94,7 +94,7 @@ function load(page, devices) {
deviceHtml += '<div class="cardBox visualCardBox">'; deviceHtml += '<div class="cardBox visualCardBox">';
deviceHtml += '<div class="cardScalable">'; deviceHtml += '<div class="cardScalable">';
deviceHtml += '<div class="cardPadder cardPadder-backdrop"></div>'; deviceHtml += '<div class="cardPadder cardPadder-backdrop"></div>';
deviceHtml += `<a is="emby-linkbutton" href="#!/device.html?id=${escapeHtml(device.Id)}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`; deviceHtml += `<a is="emby-linkbutton" href="#/dashboard/devices/edit?id=${escapeHtml(device.Id)}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`;
// audit note: getDeviceIcon returns static text // audit note: getDeviceIcon returns static text
const iconUrl = imageHelper.getDeviceIcon(device); const iconUrl = imageHelper.getDeviceIcon(device);

View File

@ -264,7 +264,7 @@
<button is="emby-button" type="submit" class="raised button-submit block"> <button is="emby-button" type="submit" class="raised button-submit block">
<span>${Save}</span> <span>${Save}</span>
</button> </button>
<button is="emby-button" type="button" class="button-cancel raised block" onclick="Dashboard.navigate('dlnaprofiles.html');"> <button is="emby-button" type="button" class="button-cancel raised block" onclick="Dashboard.navigate('dashboard/dlna/profiles');">
<span>${ButtonCancel}</span> <span>${ButtonCancel}</span>
</button> </button>
</div> </div>

View File

@ -639,7 +639,7 @@ function saveProfile(page, profile) {
data: JSON.stringify(profile), data: JSON.stringify(profile),
contentType: 'application/json' contentType: 'application/json'
}).then(function () { }).then(function () {
Dashboard.navigate('dlnaprofiles.html'); Dashboard.navigate('dashboard/dlna/profiles');
}, Dashboard.processErrorResponse); }, Dashboard.processErrorResponse);
} }

View File

@ -8,7 +8,7 @@
<div class="verticalSection verticalSection-extrabottompadding"> <div class="verticalSection verticalSection-extrabottompadding">
<div class="sectionTitleContainer flex align-items-center"> <div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle">${HeaderCustomDlnaProfiles}</h2> <h2 class="sectionTitle">${HeaderCustomDlnaProfiles}</h2>
<a is="emby-linkbutton" href="#/dlnaprofile.html" class="fab submit" style="margin:0 0 0 1em"> <a is="emby-linkbutton" href="#/dashboard/dlna/profiles/edit" class="fab submit" style="margin:0 0 0 1em">
<span class="material-icons add" aria-hidden="true"></span> <span class="material-icons add" aria-hidden="true"></span>
</a> </a>
</div> </div>

View File

@ -40,7 +40,7 @@ function renderProfiles(page, element, profiles) {
html += '<div class="listItem listItem-border">'; html += '<div class="listItem listItem-border">';
html += '<span class="listItemIcon material-icons live_tv" aria-hidden="true"></span>'; html += '<span class="listItemIcon material-icons live_tv" aria-hidden="true"></span>';
html += '<div class="listItemBody two-line">'; html += '<div class="listItemBody two-line">';
html += "<a is='emby-linkbutton' style='padding:0;margin:0;' data-ripple='false' class='clearLink' href='#/dlnaprofile.html?id=" + profile.Id + "'>"; html += "<a is='emby-linkbutton' style='padding:0;margin:0;' data-ripple='false' class='clearLink' href='#/dashboard/dlna/profiles/edit?id=" + profile.Id + "'>";
html += '<div>' + escapeHtml(profile.Name) + '</div>'; html += '<div>' + escapeHtml(profile.Name) + '</div>';
html += '</a>'; html += '</a>';
html += '</div>'; html += '</div>';
@ -78,10 +78,10 @@ function deleteProfile(page, id) {
function getTabs() { function getTabs() {
return [{ return [{
href: '#/dlnasettings.html', href: '#/dashboard/dlna',
name: globalize.translate('Settings') name: globalize.translate('Settings')
}, { }, {
href: '#/dlnaprofiles.html', href: '#/dashboard/dlna/profiles',
name: globalize.translate('TabProfiles') name: globalize.translate('TabProfiles')
}]; }];
} }

View File

@ -37,10 +37,10 @@ function onSubmit() {
function getTabs() { function getTabs() {
return [{ return [{
href: '#/dlnasettings.html', href: '#/dashboard/dlna',
name: globalize.translate('Settings') name: globalize.translate('Settings')
}, { }, {
href: '#/dlnaprofiles.html', href: '#/dashboard/dlna/profiles',
name: globalize.translate('TabProfiles') name: globalize.translate('TabProfiles')
}]; }];
} }

View File

@ -167,13 +167,13 @@ function setDecodingCodecsVisible(context, value) {
function getTabs() { function getTabs() {
return [{ return [{
href: '#/encodingsettings.html', href: '#/dashboard/playback/transcoding',
name: globalize.translate('Transcoding') name: globalize.translate('Transcoding')
}, { }, {
href: '#/playbackconfiguration.html', href: '#/dashboard/playback/resume',
name: globalize.translate('ButtonResume') name: globalize.translate('ButtonResume')
}, { }, {
href: '#/streamingsettings.html', href: '#/dashboard/playback/streaming',
name: globalize.translate('TabStreaming') name: globalize.translate('TabStreaming')
}]; }];
} }

View File

@ -360,16 +360,16 @@ function getVirtualFolderHtml(page, virtualFolder, index) {
function getTabs() { function getTabs() {
return [{ return [{
href: '#/library.html', href: '#/dashboard/libraries',
name: globalize.translate('HeaderLibraries') name: globalize.translate('HeaderLibraries')
}, { }, {
href: '#/librarydisplay.html', href: '#/dashboard/libraries/display',
name: globalize.translate('Display') name: globalize.translate('Display')
}, { }, {
href: '#/metadataimages.html', href: '#/dashboard/libraries/metadata',
name: globalize.translate('Metadata') name: globalize.translate('Metadata')
}, { }, {
href: '#/metadatanfo.html', href: '#/dashboard/libraries/nfo',
name: globalize.translate('TabNfoSettings') name: globalize.translate('TabNfoSettings')
}]; }];
} }

View File

@ -7,16 +7,16 @@ import Dashboard from '../../utils/dashboard';
function getTabs() { function getTabs() {
return [{ return [{
href: '#/library.html', href: '#/dashboard/libraries',
name: globalize.translate('HeaderLibraries') name: globalize.translate('HeaderLibraries')
}, { }, {
href: '#/librarydisplay.html', href: '#/dashboard/libraries/display',
name: globalize.translate('Display') name: globalize.translate('Display')
}, { }, {
href: '#/metadataimages.html', href: '#/dashboard/libraries/metadata',
name: globalize.translate('Metadata') name: globalize.translate('Metadata')
}, { }, {
href: '#/metadatanfo.html', href: '#/dashboard/libraries/nfo',
name: globalize.translate('TabNfoSettings') name: globalize.translate('TabNfoSettings')
}]; }];
} }

View File

@ -88,16 +88,16 @@ function onSubmit() {
function getTabs() { function getTabs() {
return [{ return [{
href: '#/library.html', href: '#/dashboard/libraries',
name: globalize.translate('HeaderLibraries') name: globalize.translate('HeaderLibraries')
}, { }, {
href: '#/librarydisplay.html', href: '#/dashboard/libraries/display',
name: globalize.translate('Display') name: globalize.translate('Display')
}, { }, {
href: '#/metadataimages.html', href: '#/dashboard/libraries/metadata',
name: globalize.translate('Metadata') name: globalize.translate('Metadata')
}, { }, {
href: '#/metadatanfo.html', href: '#/dashboard/libraries/nfo',
name: globalize.translate('TabNfoSettings') name: globalize.translate('TabNfoSettings')
}]; }];
} }

View File

@ -46,16 +46,16 @@ function showConfirmMessage() {
function getTabs() { function getTabs() {
return [{ return [{
href: '#/library.html', href: '#/dashboard/libraries',
name: globalize.translate('HeaderLibraries') name: globalize.translate('HeaderLibraries')
}, { }, {
href: '#/librarydisplay.html', href: '#/dashboard/libraries/display',
name: globalize.translate('Display') name: globalize.translate('Display')
}, { }, {
href: '#/metadataimages.html', href: '#/dashboard/libraries/metadata',
name: globalize.translate('Metadata') name: globalize.translate('Metadata')
}, { }, {
href: '#/metadatanfo.html', href: '#/dashboard/libraries/nfo',
name: globalize.translate('TabNfoSettings') name: globalize.translate('TabNfoSettings')
}]; }];
} }

View File

@ -31,13 +31,13 @@ function onSubmit() {
function getTabs() { function getTabs() {
return [{ return [{
href: '#/encodingsettings.html', href: '#/dashboard/playback/transcoding',
name: globalize.translate('Transcoding') name: globalize.translate('Transcoding')
}, { }, {
href: '#/playbackconfiguration.html', href: '#/dashboard/playback/resume',
name: globalize.translate('ButtonResume') name: globalize.translate('ButtonResume')
}, { }, {
href: '#/streamingsettings.html', href: '#/dashboard/playback/streaming',
name: globalize.translate('TabStreaming') name: globalize.translate('TabStreaming')
}]; }];
} }

View File

@ -120,7 +120,7 @@ function onSearchBarType(searchBar) {
function getPluginHtml(plugin, options, installedPlugins) { function getPluginHtml(plugin, options, installedPlugins) {
let html = ''; let html = '';
let href = plugin.externalUrl ? plugin.externalUrl : '#/addplugin.html?name=' + encodeURIComponent(plugin.name) + '&guid=' + plugin.guid; let href = plugin.externalUrl ? plugin.externalUrl : '#/dashboard/plugins/add?name=' + encodeURIComponent(plugin.name) + '&guid=' + plugin.guid;
if (options.context) { if (options.context) {
href += '&context=' + options.context; href += '&context=' + options.context;
@ -161,13 +161,13 @@ function getPluginHtml(plugin, options, installedPlugins) {
function getTabs() { function getTabs() {
return [{ return [{
href: '#/installedplugins.html', href: '#/dashboard/plugins',
name: globalize.translate('TabMyPlugins') name: globalize.translate('TabMyPlugins')
}, { }, {
href: '#/availableplugins.html', href: '#/dashboard/plugins/catalog',
name: globalize.translate('TabCatalog') name: globalize.translate('TabCatalog')
}, { }, {
href: '#/repositories.html', href: '#/dashboard/plugins/repositories',
name: globalize.translate('TabRepositories') name: globalize.translate('TabRepositories')
}]; }];
} }

View File

@ -130,7 +130,7 @@ function populateList(page, plugins, pluginConfigurationPages) {
} else { } else {
html += '<div class="centerMessage">'; html += '<div class="centerMessage">';
html += '<h1>' + globalize.translate('MessageNoPluginsInstalled') + '</h1>'; html += '<h1>' + globalize.translate('MessageNoPluginsInstalled') + '</h1>';
html += '<p><a is="emby-linkbutton" class="button-link" href="#/availableplugins.html">'; html += '<p><a is="emby-linkbutton" class="button-link" href="#/dashboard/plugins/catalog">';
html += globalize.translate('MessageBrowsePluginCatalog'); html += globalize.translate('MessageBrowsePluginCatalog');
html += '</a></p>'; html += '</a></p>';
html += '</div>'; html += '</div>';
@ -221,13 +221,13 @@ function reloadList(page) {
function getTabs() { function getTabs() {
return [{ return [{
href: '#/installedplugins.html', href: '#/dashboard/plugins',
name: globalize.translate('TabMyPlugins') name: globalize.translate('TabMyPlugins')
}, { }, {
href: '#/availableplugins.html', href: '#/dashboard/plugins/catalog',
name: globalize.translate('TabCatalog') name: globalize.translate('TabCatalog')
}, { }, {
href: '#/repositories.html', href: '#/dashboard/plugins/repositories',
name: globalize.translate('TabRepositories') name: globalize.translate('TabRepositories')
}]; }];
} }

View File

@ -105,13 +105,13 @@ function getRepositoryElement(repository) {
function getTabs() { function getTabs() {
return [{ return [{
href: '#/installedplugins.html', href: '#/dashboard/plugins',
name: globalize.translate('TabMyPlugins') name: globalize.translate('TabMyPlugins')
}, { }, {
href: '#/availableplugins.html', href: '#/dashboard/plugins/catalog',
name: globalize.translate('TabCatalog') name: globalize.translate('TabCatalog')
}, { }, {
href: '#/repositories.html', href: '#/dashboard/plugins/repositories',
name: globalize.translate('TabRepositories') name: globalize.translate('TabRepositories')
}]; }];
} }

View File

@ -53,12 +53,12 @@ function populateList(page, tasks) {
html += '<div class="paperList">'; html += '<div class="paperList">';
} }
html += '<div class="listItem listItem-border scheduledTaskPaperIconItem" data-status="' + task.State + '">'; html += '<div class="listItem listItem-border scheduledTaskPaperIconItem" data-status="' + task.State + '">';
html += "<a is='emby-linkbutton' style='margin:0;padding:0;' class='clearLink listItemIconContainer' href='scheduledtask.html?id=" + task.Id + "'>"; html += "<a is='emby-linkbutton' style='margin:0;padding:0;' class='clearLink listItemIconContainer' href='/dashboard/tasks/edit?id=" + task.Id + "'>";
html += '<span class="material-icons listItemIcon schedule" aria-hidden="true"></span>'; html += '<span class="material-icons listItemIcon schedule" aria-hidden="true"></span>';
html += '</a>'; html += '</a>';
html += '<div class="listItemBody two-line">'; html += '<div class="listItemBody two-line">';
const textAlignStyle = globalize.getIsRTL() ? 'right' : 'left'; const textAlignStyle = globalize.getIsRTL() ? 'right' : 'left';
html += "<a class='clearLink' style='margin:0;padding:0;display:block;text-align:" + textAlignStyle + ";' is='emby-linkbutton' href='scheduledtask.html?id=" + task.Id + "'>"; html += "<a class='clearLink' style='margin:0;padding:0;display:block;text-align:" + textAlignStyle + ";' is='emby-linkbutton' href='/dashboard/tasks/edit?id=" + task.Id + "'>";
html += "<h3 class='listItemBodyText'>" + task.Name + '</h3>'; html += "<h3 class='listItemBodyText'>" + task.Name + '</h3>';
html += "<div class='secondary listItemBodyText' id='taskProgress" + task.Id + "'>" + getTaskProgressHtml(task) + '</div>'; html += "<div class='secondary listItemBodyText' id='taskProgress" + task.Id + "'>" + getTaskProgressHtml(task) + '</div>';
html += '</a>'; html += '</a>';

View File

@ -1,12 +0,0 @@
<div id="serverActivityPage" data-role="page" class="page type-interior serverActivityPage noSecondaryNavPage" data-title="${HeaderActivity}">
<div>
<div class="content-primary">
<div class="verticalSection">
<h2 class="sectionTitle"></h2>
</div>
<div class="readOnlyContent">
<div class="paperList activityItems" data-activitylimit="100"></div>
</div>
</div>
</div>
</div>

View File

@ -1,32 +0,0 @@
import ActivityLog from '../../components/activitylog';
import globalize from '../../scripts/globalize';
import { toBoolean } from '../../utils/string.ts';
export default function (view, params) {
let activityLog;
if (toBoolean(params.useractivity, true)) {
view.querySelector('.activityItems').setAttribute('data-useractivity', 'true');
view.querySelector('.sectionTitle').innerHTML = globalize.translate('HeaderActivity');
} else {
view.querySelector('.activityItems').setAttribute('data-useractivity', 'false');
view.querySelector('.sectionTitle').innerHTML = globalize.translate('Alerts');
}
view.addEventListener('viewshow', function () {
if (!activityLog) {
activityLog = new ActivityLog({
serverId: ApiClient.serverId(),
element: view.querySelector('.activityItems')
});
}
});
view.addEventListener('viewdestroy', function () {
if (activityLog) {
activityLog.destroy();
}
activityLog = null;
});
}

View File

@ -22,13 +22,13 @@ function onSubmit() {
function getTabs() { function getTabs() {
return [{ return [{
href: '#/encodingsettings.html', href: '#/dashboard/playback/transcoding',
name: globalize.translate('Transcoding') name: globalize.translate('Transcoding')
}, { }, {
href: '#/playbackconfiguration.html', href: '#/dashboard/playback/resume',
name: globalize.translate('ButtonResume') name: globalize.translate('ButtonResume')
}, { }, {
href: '#/streamingsettings.html', href: '#/dashboard/playback/streaming',
name: globalize.translate('TabStreaming') name: globalize.translate('TabStreaming')
}]; }];
} }

View File

@ -1,35 +1,25 @@
import { appRouter } from '../components/router/appRouter'; import { appHost } from 'components/apphost';
import cardBuilder from '../components/cardbuilder/cardBuilder'; import cardBuilder from 'components/cardbuilder/cardBuilder';
import dom from '../scripts/dom'; import focusManager from 'components/focusManager';
import globalize from '../scripts/globalize'; import layoutManager from 'components/layoutManager';
import { appHost } from '../components/apphost'; import { appRouter } from 'components/router/appRouter';
import layoutManager from '../components/layoutManager'; import ServerConnections from 'components/ServerConnections';
import focusManager from '../components/focusManager'; import dom from 'scripts/dom';
import '../elements/emby-itemscontainer/emby-itemscontainer'; import globalize from 'scripts/globalize';
import '../elements/emby-scroller/emby-scroller'; import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
import ServerConnections from '../components/ServerConnections';
import 'elements/emby-itemscontainer/emby-itemscontainer';
import 'elements/emby-scroller/emby-scroller';
function enableScrollX() { function enableScrollX() {
return true; return true;
} }
function getThumbShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
function getPosterShape() {
return enableScrollX() ? 'overflowPortrait' : 'portrait';
}
function getSquareShape() {
return enableScrollX() ? 'overflowSquare' : 'square';
}
function getSections() { function getSections() {
return [{ return [{
name: 'Movies', name: 'Movies',
types: 'Movie', types: 'Movie',
shape: getPosterShape(), shape: getPortraitShape(enableScrollX()),
showTitle: true, showTitle: true,
showYear: true, showYear: true,
overlayPlayButton: true, overlayPlayButton: true,
@ -38,7 +28,7 @@ function getSections() {
}, { }, {
name: 'Shows', name: 'Shows',
types: 'Series', types: 'Series',
shape: getPosterShape(), shape: getPortraitShape(enableScrollX()),
showTitle: true, showTitle: true,
showYear: true, showYear: true,
overlayPlayButton: true, overlayPlayButton: true,
@ -47,7 +37,7 @@ function getSections() {
}, { }, {
name: 'Episodes', name: 'Episodes',
types: 'Episode', types: 'Episode',
shape: getThumbShape(), shape: getBackdropShape(enableScrollX()),
preferThumb: false, preferThumb: false,
showTitle: true, showTitle: true,
showParentTitle: true, showParentTitle: true,
@ -57,7 +47,7 @@ function getSections() {
}, { }, {
name: 'Videos', name: 'Videos',
types: 'Video', types: 'Video',
shape: getThumbShape(), shape: getBackdropShape(enableScrollX()),
preferThumb: true, preferThumb: true,
showTitle: true, showTitle: true,
overlayPlayButton: true, overlayPlayButton: true,
@ -66,7 +56,7 @@ function getSections() {
}, { }, {
name: 'Collections', name: 'Collections',
types: 'BoxSet', types: 'BoxSet',
shape: getPosterShape(), shape: getPortraitShape(enableScrollX()),
showTitle: true, showTitle: true,
overlayPlayButton: true, overlayPlayButton: true,
overlayText: false, overlayText: false,
@ -74,7 +64,7 @@ function getSections() {
}, { }, {
name: 'Playlists', name: 'Playlists',
types: 'Playlist', types: 'Playlist',
shape: getSquareShape(), shape: getSquareShape(enableScrollX()),
preferThumb: false, preferThumb: false,
showTitle: true, showTitle: true,
overlayText: false, overlayText: false,
@ -85,7 +75,7 @@ function getSections() {
}, { }, {
name: 'People', name: 'People',
types: 'Person', types: 'Person',
shape: getPosterShape(), shape: getPortraitShape(enableScrollX()),
preferThumb: false, preferThumb: false,
showTitle: true, showTitle: true,
overlayText: false, overlayText: false,
@ -96,7 +86,7 @@ function getSections() {
}, { }, {
name: 'Artists', name: 'Artists',
types: 'MusicArtist', types: 'MusicArtist',
shape: getSquareShape(), shape: getSquareShape(enableScrollX()),
preferThumb: false, preferThumb: false,
showTitle: true, showTitle: true,
overlayText: false, overlayText: false,
@ -107,7 +97,7 @@ function getSections() {
}, { }, {
name: 'Albums', name: 'Albums',
types: 'MusicAlbum', types: 'MusicAlbum',
shape: getSquareShape(), shape: getSquareShape(enableScrollX()),
preferThumb: false, preferThumb: false,
showTitle: true, showTitle: true,
overlayText: false, overlayText: false,
@ -118,7 +108,7 @@ function getSections() {
}, { }, {
name: 'Songs', name: 'Songs',
types: 'Audio', types: 'Audio',
shape: getSquareShape(), shape: getSquareShape(enableScrollX()),
preferThumb: false, preferThumb: false,
showTitle: true, showTitle: true,
overlayText: false, overlayText: false,
@ -130,7 +120,7 @@ function getSections() {
}, { }, {
name: 'Books', name: 'Books',
types: 'Book', types: 'Book',
shape: getPosterShape(), shape: getPortraitShape(enableScrollX()),
showTitle: true, showTitle: true,
showYear: true, showYear: true,
overlayPlayButton: true, overlayPlayButton: true,

View File

@ -4,39 +4,42 @@ import { marked } from 'marked';
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import isEqual from 'lodash-es/isEqual'; import isEqual from 'lodash-es/isEqual';
import { appHost } from '../../components/apphost'; import { appHost } from 'components/apphost';
import loading from '../../components/loading/loading'; import { clearBackdrop, setBackdrops } from 'components/backdrop/backdrop';
import { appRouter } from '../../components/router/appRouter'; import cardBuilder from 'components/cardbuilder/cardBuilder';
import layoutManager from '../../components/layoutManager'; import confirm from 'components/confirm/confirm';
import Events from '../../utils/events.ts'; import imageLoader from 'components/images/imageLoader';
import * as userSettings from '../../scripts/settings/userSettings'; import itemContextMenu from 'components/itemContextMenu';
import cardBuilder from '../../components/cardbuilder/cardBuilder'; import itemHelper from 'components/itemHelper';
import datetime from '../../scripts/datetime'; import mediaInfo from 'components/mediainfo/mediainfo';
import mediaInfo from '../../components/mediainfo/mediainfo'; import layoutManager from 'components/layoutManager';
import { clearBackdrop, setBackdrops } from '../../components/backdrop/backdrop'; import listView from 'components/listview/listview';
import listView from '../../components/listview/listview'; import loading from 'components/loading/loading';
import itemContextMenu from '../../components/itemContextMenu'; import { playbackManager } from 'components/playback/playbackmanager';
import itemHelper from '../../components/itemHelper'; import { appRouter } from 'components/router/appRouter';
import dom from '../../scripts/dom'; import itemShortcuts from 'components/shortcuts';
import imageLoader from '../../components/images/imageLoader'; import ServerConnections from 'components/ServerConnections';
import libraryMenu from '../../scripts/libraryMenu'; import browser from 'scripts/browser';
import globalize from '../../scripts/globalize'; import datetime from 'scripts/datetime';
import browser from '../../scripts/browser'; import dom from 'scripts/dom';
import { playbackManager } from '../../components/playback/playbackmanager'; import { download } from 'scripts/fileDownloader';
import '../../styles/scrollstyles.scss'; import globalize from 'scripts/globalize';
import '../../elements/emby-itemscontainer/emby-itemscontainer'; import libraryMenu from 'scripts/libraryMenu';
import '../../elements/emby-checkbox/emby-checkbox'; import * as userSettings from 'scripts/settings/userSettings';
import '../../elements/emby-button/emby-button'; import { getPortraitShape, getSquareShape } from 'utils/card';
import '../../elements/emby-playstatebutton/emby-playstatebutton'; import Dashboard from 'utils/dashboard';
import '../../elements/emby-ratingbutton/emby-ratingbutton'; import Events from 'utils/events';
import '../../elements/emby-scroller/emby-scroller'; import { getItemBackdropImageUrl } from 'utils/jellyfin-apiclient/backdropImage';
import '../../elements/emby-select/emby-select';
import itemShortcuts from '../../components/shortcuts'; import 'elements/emby-itemscontainer/emby-itemscontainer';
import Dashboard from '../../utils/dashboard'; import 'elements/emby-checkbox/emby-checkbox';
import ServerConnections from '../../components/ServerConnections'; import 'elements/emby-button/emby-button';
import confirm from '../../components/confirm/confirm'; import 'elements/emby-playstatebutton/emby-playstatebutton';
import { download } from '../../scripts/fileDownloader'; import 'elements/emby-ratingbutton/emby-ratingbutton';
import { getItemBackdropImageUrl } from '../../utils/jellyfin-apiclient/backdropImage'; import 'elements/emby-scroller/emby-scroller';
import 'elements/emby-select/emby-select';
import 'styles/scrollstyles.scss';
function autoFocus(container) { function autoFocus(container) {
import('../../components/autoFocuser').then(({ default: autoFocuser }) => { import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
@ -1069,22 +1072,6 @@ function enableScrollX() {
return browser.mobile && window.screen.availWidth <= 1000; return browser.mobile && window.screen.availWidth <= 1000;
} }
function getPortraitShape(scrollX) {
if (scrollX == null) {
scrollX = enableScrollX();
}
return scrollX ? 'overflowPortrait' : 'portrait';
}
function getSquareShape(scrollX) {
if (scrollX == null) {
scrollX = enableScrollX();
}
return scrollX ? 'overflowSquare' : 'square';
}
function renderMoreFromSeason(view, item, apiClient) { function renderMoreFromSeason(view, item, apiClient) {
const section = view.querySelector('.moreFromSeasonSection'); const section = view.querySelector('.moreFromSeasonSection');

View File

@ -1,10 +1,12 @@
import loading from '../../components/loading/loading'; import cardBuilder from 'components/cardbuilder/cardBuilder';
import cardBuilder from '../../components/cardbuilder/cardBuilder'; import imageLoader from 'components/images/imageLoader';
import imageLoader from '../../components/images/imageLoader'; import loading from 'components/loading/loading';
import '../../scripts/livetvcomponents'; import { getBackdropShape } from 'utils/card';
import '../../components/listview/listview.scss'; import Dashboard from 'utils/dashboard';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
import Dashboard from '../../utils/dashboard'; import 'scripts/livetvcomponents';
import 'components/listview/listview.scss';
import 'elements/emby-itemscontainer/emby-itemscontainer';
function renderRecordings(elem, recordings, cardOptions, scrollX) { function renderRecordings(elem, recordings, cardOptions, scrollX) {
if (!elem) { if (!elem) {
@ -32,7 +34,7 @@ function renderRecordings(elem, recordings, cardOptions, scrollX) {
recordingItems.innerHTML = cardBuilder.getCardsHtml(Object.assign({ recordingItems.innerHTML = cardBuilder.getCardsHtml(Object.assign({
items: recordings, items: recordings,
shape: scrollX ? 'autooverflow' : 'auto', shape: scrollX ? 'autooverflow' : 'auto',
defaultShape: scrollX ? 'overflowBackdrop' : 'backdrop', defaultShape: getBackdropShape(scrollX),
showTitle: true, showTitle: true,
showParentTitle: true, showParentTitle: true,
coverImage: true, coverImage: true,

View File

@ -1,11 +1,13 @@
import layoutManager from '../../components/layoutManager'; import cardBuilder from 'components/cardbuilder/cardBuilder';
import cardBuilder from '../../components/cardbuilder/cardBuilder'; import imageLoader from 'components/images/imageLoader';
import imageLoader from '../../components/images/imageLoader'; import layoutManager from 'components/layoutManager';
import loading from '../../components/loading/loading'; import loading from 'components/loading/loading';
import '../../scripts/livetvcomponents'; import { getBackdropShape } from 'utils/card';
import '../../elements/emby-button/emby-button'; import Dashboard from 'utils/dashboard';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
import Dashboard from '../../utils/dashboard'; import 'elements/emby-button/emby-button';
import 'elements/emby-itemscontainer/emby-itemscontainer';
import 'scripts/livetvcomponents';
function enableScrollX() { function enableScrollX() {
return !layoutManager.desktop; return !layoutManager.desktop;
@ -50,15 +52,11 @@ function renderRecordings(elem, recordings, cardOptions) {
imageLoader.lazyChildren(recordingItems); imageLoader.lazyChildren(recordingItems);
} }
function getBackdropShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
function renderActiveRecordings(context, promise) { function renderActiveRecordings(context, promise) {
promise.then(function (result) { promise.then(function (result) {
renderRecordings(context.querySelector('#activeRecordings'), result.Items, { renderRecordings(context.querySelector('#activeRecordings'), result.Items, {
shape: enableScrollX() ? 'autooverflow' : 'auto', shape: enableScrollX() ? 'autooverflow' : 'auto',
defaultShape: getBackdropShape(), defaultShape: getBackdropShape(enableScrollX()),
showParentTitle: false, showParentTitle: false,
showParentTitleOrTitle: true, showParentTitleOrTitle: true,
showTitle: true, showTitle: true,

View File

@ -1,36 +1,25 @@
import layoutManager from '../../components/layoutManager'; import cardBuilder from 'components/cardbuilder/cardBuilder';
import * as userSettings from '../../scripts/settings/userSettings'; import imageLoader from 'components/images/imageLoader';
import inputManager from '../../scripts/inputManager'; import layoutManager from 'components/layoutManager';
import loading from '../../components/loading/loading'; import loading from 'components/loading/loading';
import globalize from '../../scripts/globalize'; import * as mainTabsManager from 'components/maintabsmanager';
import * as mainTabsManager from '../../components/maintabsmanager'; import globalize from 'scripts/globalize';
import cardBuilder from '../../components/cardbuilder/cardBuilder'; import inputManager from 'scripts/inputManager';
import imageLoader from '../../components/images/imageLoader'; import * as userSettings from 'scripts/settings/userSettings';
import '../../styles/scrollstyles.scss'; import { LibraryTab } from 'types/libraryTab';
import '../../elements/emby-itemscontainer/emby-itemscontainer'; import Dashboard from 'utils/dashboard';
import '../../elements/emby-tabs/emby-tabs'; import { getBackdropShape, getPortraitShape } from 'utils/card';
import '../../elements/emby-button/emby-button';
import { LibraryTab } from '../../types/libraryTab.ts'; import 'elements/emby-itemscontainer/emby-itemscontainer';
import Dashboard from '../../utils/dashboard'; import 'elements/emby-tabs/emby-tabs';
import 'elements/emby-button/emby-button';
import 'styles/scrollstyles.scss';
function enableScrollX() { function enableScrollX() {
return !layoutManager.desktop; return !layoutManager.desktop;
} }
function getBackdropShape() {
if (enableScrollX()) {
return 'overflowBackdrop';
}
return 'backdrop';
}
function getPortraitShape() {
if (enableScrollX()) {
return 'overflowPortrait';
}
return 'portrait';
}
function getLimit() { function getLimit() {
if (enableScrollX()) { if (enableScrollX()) {
return 12; return 12;
@ -96,7 +85,7 @@ function reload(page, enableFullRender) {
EnableImageTypes: 'Primary,Thumb' EnableImageTypes: 'Primary,Thumb'
}).then(function (result) { }).then(function (result) {
renderItems(page, result.Items, 'upcomingTvMovieItems', null, { renderItems(page, result.Items, 'upcomingTvMovieItems', null, {
shape: getPortraitShape(), shape: getPortraitShape(enableScrollX()),
preferThumb: null, preferThumb: null,
showParentTitle: false showParentTitle: false
}); });
@ -147,7 +136,7 @@ function renderItems(page, items, sectionClass, overlayButton, cardOptions) {
preferThumb: 'auto', preferThumb: 'auto',
inheritThumb: false, inheritThumb: false,
shape: enableScrollX() ? 'autooverflow' : 'auto', shape: enableScrollX() ? 'autooverflow' : 'auto',
defaultShape: getBackdropShape(), defaultShape: getBackdropShape(enableScrollX()),
showParentTitle: true, showParentTitle: true,
showTitle: true, showTitle: true,
centerText: true, centerText: true,

View File

@ -5,7 +5,7 @@ import { getParameterByName } from '../utils/url.ts';
import Events from '../utils/events.ts'; import Events from '../utils/events.ts';
function onListingsSubmitted() { function onListingsSubmitted() {
Dashboard.navigate('livetvstatus.html'); Dashboard.navigate('dashboard/livetv');
} }
function init(page, type, providerId) { function init(page, type, providerId) {

View File

@ -220,9 +220,9 @@ function getProviderName(providerId) {
function getProviderConfigurationUrl(providerId) { function getProviderConfigurationUrl(providerId) {
switch (providerId.toLowerCase()) { switch (providerId.toLowerCase()) {
case 'xmltv': case 'xmltv':
return '#/livetvguideprovider.html?type=xmltv'; return '#/dashboard/livetv/guide?type=xmltv';
case 'schedulesdirect': case 'schedulesdirect':
return '#/livetvguideprovider.html?type=schedulesdirect'; return '#/dashboard/livetv/guide?type=schedulesdirect';
} }
} }
@ -249,7 +249,7 @@ function addProvider(button) {
} }
function addDevice() { function addDevice() {
Dashboard.navigate('livetvtuner.html'); Dashboard.navigate('dashboard/livetv/tuner');
} }
function showDeviceMenu(button, tunerDeviceId) { function showDeviceMenu(button, tunerDeviceId) {
@ -274,7 +274,7 @@ function showDeviceMenu(button, tunerDeviceId) {
break; break;
case 'edit': case 'edit':
Dashboard.navigate('livetvtuner.html?id=' + tunerDeviceId); Dashboard.navigate('dashboard/livetv/tuner?id=' + tunerDeviceId);
} }
}); });
}); });
@ -290,7 +290,7 @@ function onDevicesListClick(e) {
if (btnCardOptions) { if (btnCardOptions) {
showDeviceMenu(btnCardOptions, id); showDeviceMenu(btnCardOptions, id);
} else { } else {
Dashboard.navigate('livetvtuner.html?id=' + id); Dashboard.navigate('dashboard/livetv/tuner?id=' + id);
} }
} }
} }

View File

@ -96,7 +96,7 @@ function submitForm(page) {
contentType: 'application/json' contentType: 'application/json'
}).then(function () { }).then(function () {
Dashboard.processServerConfigurationUpdateResult(); Dashboard.processServerConfigurationUpdateResult();
Dashboard.navigate('livetvstatus.html'); Dashboard.navigate('dashboard/livetv');
}, function () { }, function () {
loading.hide(); loading.hide();
Dashboard.alert({ Dashboard.alert({

View File

@ -1,12 +1,15 @@
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import layoutManager from '../../components/layoutManager';
import loading from '../../components/loading/loading'; import cardBuilder from 'components/cardbuilder/cardBuilder';
import * as userSettings from '../../scripts/settings/userSettings'; import layoutManager from 'components/layoutManager';
import cardBuilder from '../../components/cardbuilder/cardBuilder'; import lazyLoader from 'components/lazyLoader/lazyLoaderIntersectionObserver';
import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver'; import loading from 'components/loading/loading';
import globalize from '../../scripts/globalize'; import { appRouter } from 'components/router/appRouter';
import { appRouter } from '../../components/router/appRouter'; import globalize from 'scripts/globalize';
import '../../elements/emby-button/emby-button'; import * as userSettings from 'scripts/settings/userSettings';
import { getBackdropShape, getPortraitShape } from 'utils/card';
import 'elements/emby-button/emby-button';
export default function (view, params, tabContent) { export default function (view, params, tabContent) {
function getPageData() { function getPageData() {
@ -49,14 +52,6 @@ export default function (view, params, tabContent) {
return !layoutManager.desktop; return !layoutManager.desktop;
} }
function getThumbShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
function getPortraitShape() {
return enableScrollX() ? 'overflowPortrait' : 'portrait';
}
const fillItemsContainer = (entry) => { const fillItemsContainer = (entry) => {
const elem = entry.target; const elem = entry.target;
const id = elem.getAttribute('data-id'); const id = elem.getAttribute('data-id');
@ -85,7 +80,7 @@ export default function (view, params, tabContent) {
if (viewStyle == 'Thumb') { if (viewStyle == 'Thumb') {
cardBuilder.buildCards(result.Items, { cardBuilder.buildCards(result.Items, {
itemsContainer: elem, itemsContainer: elem,
shape: getThumbShape(), shape: getBackdropShape(enableScrollX()),
preferThumb: true, preferThumb: true,
showTitle: true, showTitle: true,
scalable: true, scalable: true,
@ -96,7 +91,7 @@ export default function (view, params, tabContent) {
} else if (viewStyle == 'ThumbCard') { } else if (viewStyle == 'ThumbCard') {
cardBuilder.buildCards(result.Items, { cardBuilder.buildCards(result.Items, {
itemsContainer: elem, itemsContainer: elem,
shape: getThumbShape(), shape: getBackdropShape(enableScrollX()),
preferThumb: true, preferThumb: true,
showTitle: true, showTitle: true,
scalable: true, scalable: true,
@ -107,7 +102,7 @@ export default function (view, params, tabContent) {
} else if (viewStyle == 'PosterCard') { } else if (viewStyle == 'PosterCard') {
cardBuilder.buildCards(result.Items, { cardBuilder.buildCards(result.Items, {
itemsContainer: elem, itemsContainer: elem,
shape: getPortraitShape(), shape: getPortraitShape(enableScrollX()),
showTitle: true, showTitle: true,
scalable: true, scalable: true,
centerText: false, centerText: false,
@ -117,7 +112,7 @@ export default function (view, params, tabContent) {
} else if (viewStyle == 'Poster') { } else if (viewStyle == 'Poster') {
cardBuilder.buildCards(result.Items, { cardBuilder.buildCards(result.Items, {
itemsContainer: elem, itemsContainer: elem,
shape: getPortraitShape(), shape: getPortraitShape(enableScrollX()),
scalable: true, scalable: true,
overlayMoreButton: true, overlayMoreButton: true,
allowBottomPadding: true, allowBottomPadding: true,

View File

@ -1,35 +1,29 @@
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import layoutManager from '../../components/layoutManager';
import inputManager from '../../scripts/inputManager';
import * as userSettings from '../../scripts/settings/userSettings';
import libraryMenu from '../../scripts/libraryMenu';
import * as mainTabsManager from '../../components/maintabsmanager';
import cardBuilder from '../../components/cardbuilder/cardBuilder';
import dom from '../../scripts/dom';
import imageLoader from '../../components/images/imageLoader';
import { playbackManager } from '../../components/playback/playbackmanager';
import globalize from '../../scripts/globalize';
import { LibraryTab } from '../../types/libraryTab.ts';
import Dashboard from '../../utils/dashboard';
import Events from '../../utils/events.ts';
import '../../elements/emby-scroller/emby-scroller'; import cardBuilder from 'components/cardbuilder/cardBuilder';
import '../../elements/emby-itemscontainer/emby-itemscontainer'; import imageLoader from 'components/images/imageLoader';
import '../../elements/emby-tabs/emby-tabs'; import layoutManager from 'components/layoutManager';
import '../../elements/emby-button/emby-button'; import * as mainTabsManager from 'components/maintabsmanager';
import { playbackManager } from 'components/playback/playbackmanager';
import dom from 'scripts/dom';
import globalize from 'scripts/globalize';
import inputManager from 'scripts/inputManager';
import libraryMenu from 'scripts/libraryMenu';
import * as userSettings from 'scripts/settings/userSettings';
import { LibraryTab } from 'types/libraryTab';
import { getBackdropShape, getPortraitShape } from 'utils/card';
import Dashboard from 'utils/dashboard';
import Events from 'utils/events';
import 'elements/emby-scroller/emby-scroller';
import 'elements/emby-itemscontainer/emby-itemscontainer';
import 'elements/emby-tabs/emby-tabs';
import 'elements/emby-button/emby-button';
function enableScrollX() { function enableScrollX() {
return !layoutManager.desktop; return !layoutManager.desktop;
} }
function getPortraitShape() {
return enableScrollX() ? 'overflowPortrait' : 'portrait';
}
function getThumbShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
function loadLatest(page, userId, parentId) { function loadLatest(page, userId, parentId) {
const options = { const options = {
IncludeItemTypes: 'Movie', IncludeItemTypes: 'Movie',
@ -45,7 +39,7 @@ function loadLatest(page, userId, parentId) {
const container = page.querySelector('#recentlyAddedItems'); const container = page.querySelector('#recentlyAddedItems');
cardBuilder.buildCards(items, { cardBuilder.buildCards(items, {
itemsContainer: container, itemsContainer: container,
shape: getPortraitShape(), shape: getPortraitShape(enableScrollX()),
scalable: true, scalable: true,
overlayPlayButton: true, overlayPlayButton: true,
allowBottomPadding: allowBottomPadding, allowBottomPadding: allowBottomPadding,
@ -87,7 +81,7 @@ function loadResume(page, userId, parentId) {
cardBuilder.buildCards(result.Items, { cardBuilder.buildCards(result.Items, {
itemsContainer: container, itemsContainer: container,
preferThumb: true, preferThumb: true,
shape: getThumbShape(), shape: getBackdropShape(enableScrollX()),
scalable: true, scalable: true,
overlayPlayButton: true, overlayPlayButton: true,
allowBottomPadding: allowBottomPadding, allowBottomPadding: allowBottomPadding,
@ -138,7 +132,7 @@ function getRecommendationHtml(recommendation) {
} }
html += cardBuilder.getCardsHtml(recommendation.Items, { html += cardBuilder.getCardsHtml(recommendation.Items, {
shape: getPortraitShape(), shape: getPortraitShape(enableScrollX()),
scalable: true, scalable: true,
overlayPlayButton: true, overlayPlayButton: true,
allowBottomPadding: allowBottomPadding, allowBottomPadding: allowBottomPadding,

View File

@ -1,22 +1,24 @@
import browser from '../../scripts/browser'; import cardBuilder from 'components/cardbuilder/cardBuilder';
import layoutManager from '../../components/layoutManager'; import imageLoader from 'components/images/imageLoader';
import * as userSettings from '../../scripts/settings/userSettings'; import layoutManager from 'components/layoutManager';
import inputManager from '../../scripts/inputManager'; import loading from 'components/loading/loading';
import loading from '../../components/loading/loading'; import * as mainTabsManager from 'components/maintabsmanager';
import cardBuilder from '../../components/cardbuilder/cardBuilder'; import browser from 'scripts/browser';
import dom from '../../scripts/dom'; import dom from 'scripts/dom';
import imageLoader from '../../components/images/imageLoader'; import globalize from 'scripts/globalize';
import libraryMenu from '../../scripts/libraryMenu'; import inputManager from 'scripts/inputManager';
import * as mainTabsManager from '../../components/maintabsmanager'; import libraryMenu from 'scripts/libraryMenu';
import globalize from '../../scripts/globalize'; import * as userSettings from 'scripts/settings/userSettings';
import { LibraryTab } from '../../types/libraryTab.ts'; import { LibraryTab } from 'types/libraryTab';
import Dashboard from '../../utils/dashboard'; import Dashboard from 'utils/dashboard';
import { getSquareShape } from 'utils/card';
import '../../styles/scrollstyles.scss'; import 'elements/emby-itemscontainer/emby-itemscontainer';
import '../../elements/emby-itemscontainer/emby-itemscontainer'; import 'elements/emby-tabs/emby-tabs';
import '../../elements/emby-tabs/emby-tabs'; import 'elements/emby-button/emby-button';
import '../../elements/emby-button/emby-button';
import '../../styles/flexstyles.scss'; import 'styles/flexstyles.scss';
import 'styles/scrollstyles.scss';
function itemsPerRow() { function itemsPerRow() {
const screenWidth = dom.getWindowSize().innerWidth; const screenWidth = dom.getWindowSize().innerWidth;
@ -40,10 +42,6 @@ function enableScrollX() {
return !layoutManager.desktop; return !layoutManager.desktop;
} }
function getSquareShape() {
return enableScrollX() ? 'overflowSquare' : 'square';
}
function loadLatest(page, parentId) { function loadLatest(page, parentId) {
loading.show(); loading.show();
const userId = ApiClient.getCurrentUserId(); const userId = ApiClient.getCurrentUserId();
@ -62,7 +60,7 @@ function loadLatest(page, parentId) {
items: items, items: items,
showUnplayedIndicator: false, showUnplayedIndicator: false,
showLatestItemsPopup: false, showLatestItemsPopup: false,
shape: getSquareShape(), shape: getSquareShape(enableScrollX()),
showTitle: true, showTitle: true,
showParentTitle: true, showParentTitle: true,
lazy: true, lazy: true,
@ -108,7 +106,7 @@ function loadRecentlyPlayed(page, parentId) {
itemsContainer.innerHTML = cardBuilder.getCardsHtml({ itemsContainer.innerHTML = cardBuilder.getCardsHtml({
items: result.Items, items: result.Items,
showUnplayedIndicator: false, showUnplayedIndicator: false,
shape: getSquareShape(), shape: getSquareShape(enableScrollX()),
showTitle: true, showTitle: true,
showParentTitle: true, showParentTitle: true,
action: 'instantmix', action: 'instantmix',
@ -150,7 +148,7 @@ function loadFrequentlyPlayed(page, parentId) {
itemsContainer.innerHTML = cardBuilder.getCardsHtml({ itemsContainer.innerHTML = cardBuilder.getCardsHtml({
items: result.Items, items: result.Items,
showUnplayedIndicator: false, showUnplayedIndicator: false,
shape: getSquareShape(), shape: getSquareShape(enableScrollX()),
showTitle: true, showTitle: true,
showParentTitle: true, showParentTitle: true,
action: 'instantmix', action: 'instantmix',

View File

@ -1,12 +1,15 @@
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import layoutManager from '../../components/layoutManager';
import loading from '../../components/loading/loading'; import cardBuilder from 'components/cardbuilder/cardBuilder';
import * as userSettings from '../../scripts/settings/userSettings'; import lazyLoader from 'components/lazyLoader/lazyLoaderIntersectionObserver';
import cardBuilder from '../../components/cardbuilder/cardBuilder'; import layoutManager from 'components/layoutManager';
import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver'; import loading from 'components/loading/loading';
import globalize from '../../scripts/globalize'; import { appRouter } from 'components/router/appRouter';
import { appRouter } from '../../components/router/appRouter'; import globalize from 'scripts/globalize';
import '../../elements/emby-button/emby-button'; import * as userSettings from 'scripts/settings/userSettings';
import { getBackdropShape, getPortraitShape } from 'utils/card';
import 'elements/emby-button/emby-button';
export default function (view, params, tabContent) { export default function (view, params, tabContent) {
function getPageData() { function getPageData() {
@ -49,14 +52,6 @@ export default function (view, params, tabContent) {
return !layoutManager.desktop; return !layoutManager.desktop;
} }
function getThumbShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
function getPortraitShape() {
return enableScrollX() ? 'overflowPortrait' : 'portrait';
}
function fillItemsContainer(entry) { function fillItemsContainer(entry) {
const elem = entry.target; const elem = entry.target;
const id = elem.getAttribute('data-id'); const id = elem.getAttribute('data-id');
@ -85,7 +80,7 @@ export default function (view, params, tabContent) {
if (viewStyle == 'Thumb') { if (viewStyle == 'Thumb') {
cardBuilder.buildCards(result.Items, { cardBuilder.buildCards(result.Items, {
itemsContainer: elem, itemsContainer: elem,
shape: getThumbShape(), shape: getBackdropShape(enableScrollX()),
preferThumb: true, preferThumb: true,
showTitle: true, showTitle: true,
scalable: true, scalable: true,
@ -96,7 +91,7 @@ export default function (view, params, tabContent) {
} else if (viewStyle == 'ThumbCard') { } else if (viewStyle == 'ThumbCard') {
cardBuilder.buildCards(result.Items, { cardBuilder.buildCards(result.Items, {
itemsContainer: elem, itemsContainer: elem,
shape: getThumbShape(), shape: getBackdropShape(enableScrollX()),
preferThumb: true, preferThumb: true,
showTitle: true, showTitle: true,
scalable: true, scalable: true,
@ -107,7 +102,7 @@ export default function (view, params, tabContent) {
} else if (viewStyle == 'PosterCard') { } else if (viewStyle == 'PosterCard') {
cardBuilder.buildCards(result.Items, { cardBuilder.buildCards(result.Items, {
itemsContainer: elem, itemsContainer: elem,
shape: getPortraitShape(), shape: getPortraitShape(enableScrollX()),
showTitle: true, showTitle: true,
scalable: true, scalable: true,
centerText: false, centerText: false,
@ -117,7 +112,7 @@ export default function (view, params, tabContent) {
} else if (viewStyle == 'Poster') { } else if (viewStyle == 'Poster') {
cardBuilder.buildCards(result.Items, { cardBuilder.buildCards(result.Items, {
itemsContainer: elem, itemsContainer: elem,
shape: getPortraitShape(), shape: getPortraitShape(enableScrollX()),
scalable: true, scalable: true,
showTitle: true, showTitle: true,
centerText: true, centerText: true,

View File

@ -1,21 +1,23 @@
import autoFocuser from 'components/autoFocuser';
import cardBuilder from 'components/cardbuilder/cardBuilder';
import layoutManager from 'components/layoutManager';
import loading from 'components/loading/loading';
import * as mainTabsManager from 'components/maintabsmanager';
import { playbackManager } from 'components/playback/playbackmanager';
import dom from 'scripts/dom';
import globalize from 'scripts/globalize';
import inputManager from 'scripts/inputManager';
import libraryMenu from 'scripts/libraryMenu';
import * as userSettings from 'scripts/settings/userSettings';
import { LibraryTab } from 'types/libraryTab';
import { getBackdropShape } from 'utils/card';
import Dashboard from 'utils/dashboard';
import Events from 'utils/events';
import inputManager from '../../scripts/inputManager'; import 'elements/emby-itemscontainer/emby-itemscontainer';
import libraryMenu from '../../scripts/libraryMenu'; import 'elements/emby-button/emby-button';
import layoutManager from '../../components/layoutManager';
import loading from '../../components/loading/loading'; import 'styles/scrollstyles.scss';
import dom from '../../scripts/dom';
import * as userSettings from '../../scripts/settings/userSettings';
import cardBuilder from '../../components/cardbuilder/cardBuilder';
import { playbackManager } from '../../components/playback/playbackmanager';
import * as mainTabsManager from '../../components/maintabsmanager';
import globalize from '../../scripts/globalize';
import '../../styles/scrollstyles.scss';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
import '../../elements/emby-button/emby-button';
import { LibraryTab } from '../../types/libraryTab.ts';
import Dashboard from '../../utils/dashboard';
import Events from '../../utils/events.ts';
import autoFocuser from '../../components/autoFocuser';
function getTabs() { function getTabs() {
return [{ return [{
@ -119,7 +121,7 @@ function loadResume(view, userId, parentId) {
itemsContainer: container, itemsContainer: container,
preferThumb: true, preferThumb: true,
inheritThumb: !userSettings.useEpisodeImagesInNextUpAndResume(), inheritThumb: !userSettings.useEpisodeImagesInNextUpAndResume(),
shape: getThumbShape(), shape: getBackdropShape(enableScrollX()),
scalable: true, scalable: true,
overlayPlayButton: true, overlayPlayButton: true,
allowBottomPadding: allowBottomPadding, allowBottomPadding: allowBottomPadding,
@ -217,10 +219,6 @@ function enableScrollX() {
return !layoutManager.desktop; return !layoutManager.desktop;
} }
function getThumbShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
export default function (view, params) { export default function (view, params) {
function onBeforeTabChange(e) { function onBeforeTabChange(e) {
preLoadTab(view, parseInt(e.detail.selectedTabIndex, 10)); preLoadTab(view, parseInt(e.detail.selectedTabIndex, 10));

View File

@ -1,11 +1,14 @@
import layoutManager from '../../components/layoutManager'; import cardBuilder from 'components/cardbuilder/cardBuilder';
import loading from '../../components/loading/loading'; import imageLoader from 'components/images/imageLoader';
import datetime from '../../scripts/datetime'; import layoutManager from 'components/layoutManager';
import cardBuilder from '../../components/cardbuilder/cardBuilder'; import loading from 'components/loading/loading';
import imageLoader from '../../components/images/imageLoader'; import datetime from 'scripts/datetime';
import globalize from '../../scripts/globalize'; import globalize from 'scripts/globalize';
import '../../styles/scrollstyles.scss'; import { getBackdropShape } from 'utils/card';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
import 'elements/emby-itemscontainer/emby-itemscontainer';
import 'styles/scrollstyles.scss';
function getUpcomingPromise(context, params) { function getUpcomingPromise(context, params) {
loading.show(); loading.show();
@ -40,10 +43,6 @@ function enableScrollX() {
return !layoutManager.desktop; return !layoutManager.desktop;
} }
function getThumbShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
function renderUpcoming(elem, items) { function renderUpcoming(elem, items) {
const groups = []; const groups = [];
let currentGroupName = ''; let currentGroupName = '';
@ -105,7 +104,7 @@ function renderUpcoming(elem, items) {
html += cardBuilder.getCardsHtml({ html += cardBuilder.getCardsHtml({
items: group.items, items: group.items,
showLocationTypeIndicator: false, showLocationTypeIndicator: false,
shape: getThumbShape(), shape: getBackdropShape(enableScrollX()),
showTitle: true, showTitle: true,
preferThumb: true, preferThumb: true,
lazy: true, lazy: true,

View File

@ -77,7 +77,7 @@
</div> </div>
<div class="adminSection verticalSection verticalSection-extrabottompadding hide"> <div class="adminSection verticalSection verticalSection-extrabottompadding hide">
<h2 class="sectionTitle" style="padding-left:.25em;">${HeaderAdmin}</h2> <h2 class="sectionTitle" style="padding-left:.25em;">${HeaderAdmin}</h2>
<a is="emby-linkbutton" href="#/dashboard.html" style="display:block;padding:0;margin:0;" class="listItem-border"> <a is="emby-linkbutton" href="#/dashboard" style="display:block;padding:0;margin:0;" class="listItem-border">
<div class="listItem"> <div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent dashboard" aria-hidden="true"></span> <span class="material-icons listItemIcon listItemIcon-transparent dashboard" aria-hidden="true"></span>
<div class="listItemBody"> <div class="listItemBody">
@ -85,7 +85,7 @@
</div> </div>
</div> </div>
</a> </a>
<a is="emby-linkbutton" href="#/edititemmetadata.html" style="display:block;padding:0;margin:0;" class="listItem-border"> <a is="emby-linkbutton" href="#/metadata" style="display:block;padding:0;margin:0;" class="listItem-border">
<div class="listItem"> <div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent mode_edit" aria-hidden="true"></span> <span class="material-icons listItemIcon listItemIcon-transparent mode_edit" aria-hidden="true"></span>
<div class="listItemBody"> <div class="listItemBody">

View File

@ -327,8 +327,8 @@ function refreshLibraryInfoInDrawer(user) {
html += '<h3 class="sidebarHeader">'; html += '<h3 class="sidebarHeader">';
html += globalize.translate('HeaderAdmin'); html += globalize.translate('HeaderAdmin');
html += '</h3>'; html += '</h3>';
html += `<a is="emby-linkbutton" class="navMenuOption lnkMediaFolder lnkManageServer" data-itemid="dashboard" href="#/dashboard.html"><span class="material-icons navMenuOptionIcon dashboard" aria-hidden="true"></span><span class="navMenuOptionText">${globalize.translate('TabDashboard')}</span></a>`; html += `<a is="emby-linkbutton" class="navMenuOption lnkMediaFolder lnkManageServer" data-itemid="dashboard" href="#/dashboard"><span class="material-icons navMenuOptionIcon dashboard" aria-hidden="true"></span><span class="navMenuOptionText">${globalize.translate('TabDashboard')}</span></a>`;
html += `<a is="emby-linkbutton" class="navMenuOption lnkMediaFolder editorViewMenu" data-itemid="editor" href="#/edititemmetadata.html"><span class="material-icons navMenuOptionIcon mode_edit" aria-hidden="true"></span><span class="navMenuOptionText">${globalize.translate('Metadata')}</span></a>`; html += `<a is="emby-linkbutton" class="navMenuOption lnkMediaFolder editorViewMenu" data-itemid="editor" href="#/metadata"><span class="material-icons navMenuOptionIcon mode_edit" aria-hidden="true"></span><span class="navMenuOptionText">${globalize.translate('Metadata')}</span></a>`;
html += '</div>'; html += '</div>';
} }
@ -376,249 +376,6 @@ function refreshLibraryInfoInDrawer(user) {
} }
} }
function refreshDashboardInfoInDrawer(page, apiClient) {
currentDrawerType = 'admin';
loadNavDrawer();
if (navDrawerScrollContainer.querySelector('.adminDrawerLogo')) {
updateDashboardMenuSelectedItem(page);
} else {
createDashboardMenu(page, apiClient);
}
}
function isUrlInCurrentView(url) {
return window.location.href.toString().toLowerCase().indexOf(url.toLowerCase()) !== -1;
}
function updateDashboardMenuSelectedItem(page) {
const links = navDrawerScrollContainer.querySelectorAll('.navMenuOption');
const currentViewId = page.id;
for (let i = 0, length = links.length; i < length; i++) {
let link = links[i];
let selected = false;
let pageIds = link.getAttribute('data-pageids');
if (pageIds) {
pageIds = pageIds.split('|');
selected = pageIds.indexOf(currentViewId) != -1;
}
let pageUrls = link.getAttribute('data-pageurls');
if (pageUrls) {
pageUrls = pageUrls.split('|');
selected = pageUrls.filter(isUrlInCurrentView).length > 0;
}
if (selected) {
link.classList.add('navMenuOption-selected');
let title = '';
link = link.querySelector('.navMenuOptionText') || link;
title += (link.innerText || link.textContent).trim();
LibraryMenu.setTitle(title);
} else {
link.classList.remove('navMenuOption-selected');
}
}
}
function createToolsMenuList(pluginItems) {
const links = [{
name: globalize.translate('TabServer')
}, {
name: globalize.translate('TabDashboard'),
href: '#/dashboard.html',
pageIds: ['dashboardPage'],
icon: 'dashboard'
}, {
name: globalize.translate('General'),
href: '#/dashboardgeneral.html',
pageIds: ['dashboardGeneralPage'],
icon: 'settings'
}, {
name: globalize.translate('HeaderUsers'),
href: '#/userprofiles.html',
pageIds: ['userProfilesPage', 'newUserPage', 'editUserPage', 'userLibraryAccessPage', 'userParentalControlPage', 'userPasswordPage'],
icon: 'people'
}, {
name: globalize.translate('HeaderLibraries'),
href: '#/library.html',
pageIds: ['mediaLibraryPage', 'librarySettingsPage', 'libraryDisplayPage', 'metadataImagesConfigurationPage', 'metadataNfoPage'],
icon: 'folder'
}, {
name: globalize.translate('TitlePlayback'),
icon: 'play_arrow',
href: '#/encodingsettings.html',
pageIds: ['encodingSettingsPage', 'playbackConfigurationPage', 'streamingSettingsPage']
}];
addPluginPagesToMainMenu(links, pluginItems, 'server');
links.push({
divider: true,
name: globalize.translate('HeaderDevices')
});
links.push({
name: globalize.translate('HeaderDevices'),
href: '#/devices.html',
pageIds: ['devicesPage', 'devicePage'],
icon: 'devices'
});
links.push({
name: globalize.translate('HeaderActivity'),
href: '#/dashboard/activity',
pageIds: ['serverActivityPage'],
icon: 'assessment'
});
links.push({
name: globalize.translate('DLNA'),
href: '#/dlnasettings.html',
pageIds: ['dlnaSettingsPage', 'dlnaProfilesPage', 'dlnaProfilePage'],
icon: 'input'
});
links.push({
divider: true,
name: globalize.translate('LiveTV')
});
links.push({
name: globalize.translate('LiveTV'),
href: '#/livetvstatus.html',
pageIds: ['liveTvStatusPage', 'liveTvTunerPage'],
icon: 'live_tv'
});
links.push({
name: globalize.translate('HeaderDVR'),
href: '#/livetvsettings.html',
pageIds: ['liveTvSettingsPage'],
icon: 'dvr'
});
addPluginPagesToMainMenu(links, pluginItems, 'livetv');
links.push({
divider: true,
name: globalize.translate('TabAdvanced')
});
links.push({
name: globalize.translate('TabNetworking'),
icon: 'cloud',
href: '#/networking.html',
pageIds: ['networkingPage']
});
links.push({
name: globalize.translate('HeaderApiKeys'),
icon: 'vpn_key',
href: '#/apikeys.html',
pageIds: ['apiKeysPage']
});
links.push({
name: globalize.translate('TabLogs'),
href: '#/log.html',
pageIds: ['logPage'],
icon: 'bug_report'
});
links.push({
name: globalize.translate('Notifications'),
icon: 'notifications',
href: '#/notificationsettings.html'
});
links.push({
name: globalize.translate('TabPlugins'),
icon: 'shopping_cart',
href: '#/installedplugins.html',
pageIds: ['pluginsPage', 'pluginCatalogPage']
});
links.push({
name: globalize.translate('TabScheduledTasks'),
href: '#/scheduledtasks.html',
pageIds: ['scheduledTasksPage', 'scheduledTaskPage'],
icon: 'schedule'
});
if (hasUnsortedPlugins(pluginItems)) {
links.push({
divider: true,
name: globalize.translate('TabPlugins')
});
addPluginPagesToMainMenu(links, pluginItems);
}
return links;
}
function hasUnsortedPlugins(pluginItems) {
for (const pluginItem of pluginItems) {
if (pluginItem.EnableInMainMenu && pluginItem.MenuSection === undefined) {
return true;
}
}
return false;
}
function addPluginPagesToMainMenu(links, pluginItems, section) {
for (const pluginItem of pluginItems) {
if (pluginItem.EnableInMainMenu && pluginItem.MenuSection === section) {
links.push({
name: pluginItem.DisplayName,
icon: pluginItem.MenuIcon || 'folder',
href: Dashboard.getPluginUrl(pluginItem.Name),
pageUrls: [Dashboard.getPluginUrl(pluginItem.Name)]
});
}
}
}
function getToolsMenuLinks(apiClient) {
return apiClient.getJSON(apiClient.getUrl('web/configurationpages') + '?pageType=PluginConfiguration&EnableInMainMenu=true').then(createToolsMenuList, function () {
return createToolsMenuList([]);
});
}
function getToolsLinkHtml(item) {
let menuHtml = '';
let pageIds = item.pageIds ? item.pageIds.join('|') : '';
pageIds = pageIds ? ' data-pageids="' + pageIds + '"' : '';
let pageUrls = item.pageUrls ? item.pageUrls.join('|') : '';
pageUrls = pageUrls ? ' data-pageurls="' + pageUrls + '"' : '';
menuHtml += '<a is="emby-linkbutton" class="navMenuOption" href="' + item.href + '"' + pageIds + pageUrls + '>';
if (item.icon) {
menuHtml += '<span class="material-icons navMenuOptionIcon ' + item.icon + '" aria-hidden="true"></span>';
}
menuHtml += '<span class="navMenuOptionText">';
menuHtml += escapeHtml(item.name);
menuHtml += '</span>';
return menuHtml + '</a>';
}
function getToolsMenuHtml(apiClient) {
return getToolsMenuLinks(apiClient).then(function (items) {
let menuHtml = '';
menuHtml += '<div class="drawerContent">';
for (const item of items) {
if (item.href) {
menuHtml += getToolsLinkHtml(item);
} else if (item.name) {
menuHtml += '<h3 class="sidebarHeader">';
menuHtml += escapeHtml(item.name);
menuHtml += '</h3>';
}
}
return menuHtml + '</div>';
});
}
function createDashboardMenu(page, apiClient) {
return getToolsMenuHtml(apiClient).then(function (toolsMenuHtml) {
let html = '';
html += '<a class="adminDrawerLogo clearLink" is="emby-linkbutton" href="#/home.html">';
html += '<img src="assets/img/icon-transparent.png" />';
html += '</a>';
html += toolsMenuHtml;
navDrawerScrollContainer.innerHTML = html;
updateDashboardMenuSelectedItem(page);
});
}
function onSidebarLinkClick() { function onSidebarLinkClick() {
const section = this.getElementsByClassName('sectionName')[0]; const section = this.getElementsByClassName('sectionName')[0];
const text = section ? section.innerHTML : this.innerHTML; const text = section ? section.innerHTML : this.innerHTML;
@ -1026,15 +783,8 @@ pageClassOn('pageshow', 'page', function (e) {
const isDashboardPage = page.classList.contains('type-interior'); const isDashboardPage = page.classList.contains('type-interior');
const isHomePage = page.classList.contains('homePage'); const isHomePage = page.classList.contains('homePage');
const isLibraryPage = !isDashboardPage && page.classList.contains('libraryPage'); const isLibraryPage = !isDashboardPage && page.classList.contains('libraryPage');
const apiClient = getCurrentApiClient();
if (isDashboardPage) { if (!isDashboardPage) {
if (mainDrawerButton) {
mainDrawerButton.classList.remove('hide');
}
refreshDashboardInfoInDrawer(page, apiClient);
} else {
if (mainDrawerButton) { if (mainDrawerButton) {
if (enableLibraryNavDrawer || (isHomePage && enableLibraryNavDrawerHome)) { if (enableLibraryNavDrawer || (isHomePage && enableLibraryNavDrawerHome)) {
mainDrawerButton.classList.remove('hide'); mainDrawerButton.classList.remove('hide');

View File

@ -1,15 +1,13 @@
import layoutManager from '../components/layoutManager'; import cardBuilder from 'components/cardbuilder/cardBuilder';
import layoutManager from 'components/layoutManager';
import { getBackdropShape } from 'utils/card';
import datetime from './datetime'; import datetime from './datetime';
import cardBuilder from '../components/cardbuilder/cardBuilder';
function enableScrollX() { function enableScrollX() {
return !layoutManager.desktop; return !layoutManager.desktop;
} }
function getBackdropShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
function getTimersHtml(timers, options) { function getTimersHtml(timers, options) {
options = options || {}; options = options || {};
@ -78,7 +76,7 @@ function getTimersHtml(timers, options) {
html += cardBuilder.getCardsHtml({ html += cardBuilder.getCardsHtml({
items: group.items, items: group.items,
shape: getBackdropShape(), shape: getBackdropShape(enableScrollX()),
showTitle: true, showTitle: true,
showParentTitleOrTitle: true, showParentTitleOrTitle: true,
showAirTime: true, showAirTime: true,

View File

@ -528,7 +528,7 @@
"Screenshots": "Снимки на екрана", "Screenshots": "Снимки на екрана",
"Search": "Търсене", "Search": "Търсене",
"SearchForCollectionInternetMetadata": "Търсене в интернет за картини и метаданни", "SearchForCollectionInternetMetadata": "Търсене в интернет за картини и метаданни",
"SearchForMissingMetadata": "Търсене за лисващи метаданни", "SearchForMissingMetadata": "Търсене за липсващи метаданни",
"SearchForSubtitles": "Търсене на субтитри", "SearchForSubtitles": "Търсене на субтитри",
"SendMessage": "Изпращане на съобщение", "SendMessage": "Изпращане на съобщение",
"SeriesYearToPresent": "{0} - Настояще", "SeriesYearToPresent": "{0} - Настояще",
@ -1166,11 +1166,11 @@
"MessageUnsetContentHelp": "Съдържанието ще се показва като обикновени папки. За най-добри резултати използвайте мениджъра на метаданни, за да зададете типовете съдържание на подпапките.", "MessageUnsetContentHelp": "Съдържанието ще се показва като обикновени папки. За най-добри резултати използвайте мениджъра на метаданни, за да зададете типовете съдържание на подпапките.",
"MessageUnableToConnectToServer": "В момента не можем да се свържем с избрания сървър. Моля, уверете се, че работи и опитайте отново.", "MessageUnableToConnectToServer": "В момента не можем да се свържем с избрания сървър. Моля, уверете се, че работи и опитайте отново.",
"MessageReenableUser": "Вижте по-долу, за да активирате отново", "MessageReenableUser": "Вижте по-долу, за да активирате отново",
"MessagePluginInstallDisclaimer": риставките, създадени от членове на общността, са чудесен начин да подобрите изживяването с Джелифин чрез допълнителните функции и предимства.Преди да инсталирате, имайте предвид ефектите, които те могат да имат върху вашия Джелифин сървър, като по-дълго време за сканиране на библиотеки, допълнителна обработка на заден фон и намалена стабилност на системата.", "MessagePluginInstallDisclaimer": РЕДУПРЕЖДЕНИЕ: Инсталирането на плъгин на трета страна носи рискове. Може да съдържа нестабилен или злонамерен код и може да се промени по всяко време. Инсталирайте само плъгини от автори на които имате доверие! Имайте предвид потенциалните ефекти, които може да има, включително заявки към външни услуги, по-дълги сканирания на библиотеки или допълнителна фонова обработка.",
"MessagePluginConfigurationRequiresLocalAccess": "За да конфигурирате тази приставка, моля, впишете се директно в локалния си сървър.", "MessagePluginConfigurationRequiresLocalAccess": "За да конфигурирате тази приставка, моля, впишете се директно в локалния си сървър.",
"MessagePleaseWait": "Моля,изчакайте. Това може да отнеме минута.", "MessagePleaseWait": "Моля,изчакайте. Това може да отнеме минута.",
"MessagePlayAccessRestricted": "Възпроизвеждането на това съдържание в момента е ограничено.Моля, свържете се с администратора на вашия сървър за повече информация.", "MessagePlayAccessRestricted": "Възпроизвеждането на това съдържание в момента е ограничено.Моля, свържете се с администратора на вашия сървър за повече информация.",
"MessagePasswordResetForUsers": "Следните потребители са занулили паролите си.Те вече могат да влязат с пин кодовете, използвани за извършване на нулирането.", "MessagePasswordResetForUsers": "Следните потребители са занулили паролите си.Те вече могат да влязат с ПИН кодовете, използвани за извършване на нулирането.",
"MessageNoTrailersFound": "За да подобрите филмовото изживяване инсталирайте канал за трейлъри,може да подредите няколко канала в библиотека.", "MessageNoTrailersFound": "За да подобрите филмовото изживяване инсталирайте канал за трейлъри,може да подредите няколко канала в библиотека.",
"MessageNoServersAvailable": "Не са намерени сървъри, използващи функцията за автоматично откриване на сървър.", "MessageNoServersAvailable": "Не са намерени сървъри, използващи функцията за автоматично откриване на сървър.",
"MessageNoMovieSuggestionsAvailable": "Понастоящем няма предложени филми. Започнете да гледате и оценявате филмите си, а след това се върнете, за да видите препоръките си.", "MessageNoMovieSuggestionsAvailable": "Понастоящем няма предложени филми. Започнете да гледате и оценявате филмите си, а след това се върнете, за да видите препоръките си.",
@ -1452,7 +1452,7 @@
"LabelAlbumArtMaxResHelp": "Максимална резолюция на изображенията предоставена чрез \"upnp:albumArtURI\" полето.", "LabelAlbumArtMaxResHelp": "Максимална резолюция на изображенията предоставена чрез \"upnp:albumArtURI\" полето.",
"KnownProxiesHelp": "Списък от IP ареси или хост имена на известни прокси сървъри, разделени със запетая, използвани при свързване с Jellyfin сървър. Това е задължително за да се използва правилнен \"X-Forwarded-For\" хедър. Изисква рестартиране след прилагане.", "KnownProxiesHelp": "Списък от IP ареси или хост имена на известни прокси сървъри, разделени със запетая, използвани при свързване с Jellyfin сървър. Това е задължително за да се използва правилнен \"X-Forwarded-For\" хедър. Изисква рестартиране след прилагане.",
"HomeVideosPhotos": "Домашни видеа и снимки", "HomeVideosPhotos": "Домашни видеа и снимки",
"DirectPlayHelp": "Основният файл е напълно съвместим с този клиент, което значи че го получавате без модификации.", "DirectPlayHelp": "Премахване на изображение",
"AllowTonemappingHelp": "Тоналното картографиране може да трансформира динамичния обхват на видеото от HDR към SDR, като същевременно запазва детайлите и цветовете на изображението, които са много важна информация за представяне на оригиналната сцена. В момента работи само с 10-битови HDR10HLG и DoVi видеоклипове. Това изисква съответното време за изпълнение от OpenCL или CUDA.", "AllowTonemappingHelp": "Тоналното картографиране може да трансформира динамичния обхват на видеото от HDR към SDR, като същевременно запазва детайлите и цветовете на изображението, които са много важна информация за представяне на оригиналната сцена. В момента работи само с 10-битови HDR10HLG и DoVi видеоклипове. Това изисква съответното време за изпълнение от OpenCL или CUDA.",
"LabelMaxAudiobookResumeHelp": "Приема се ,че файловете се възпроизведени до края , ако се спре след като оставащото време е по-малко от тази стойност.", "LabelMaxAudiobookResumeHelp": "Приема се ,че файловете се възпроизведени до края , ако се спре след като оставащото време е по-малко от тази стойност.",
"Experimental": "Експериментални", "Experimental": "Експериментални",
@ -1478,5 +1478,10 @@
"EnableAudioNormalizationHelp": "Нормализацията на звука ще усили сигналът за да поддържа средните честоти на желано ниво (-18dB).", "EnableAudioNormalizationHelp": "Нормализацията на звука ще усили сигналът за да поддържа средните честоти на желано ниво (-18dB).",
"EnableAudioNormalization": "Нормализация на звука", "EnableAudioNormalization": "Нормализация на звука",
"Unknown": "Неизвестен", "Unknown": "Неизвестен",
"LabelThrottleDelaySeconds": "Ограничи след" "LabelThrottleDelaySeconds": "Ограничи след",
"GetThePlugin": "Вземете приставката",
"LabelLocalCustomCss": "Персонализиран CSS код за стилизиране, който се отнася само за този клиент. Може да искате да деактивирате персонализирания CSS код на сървъра.",
"LabelOriginalName": "Оригинално име",
"LabelQuickConnectCode": "Код за бързо свързване",
"LabelMaxVideoResolution": "Максимално разрешена разделителна способност на транскодиране на видео"
} }

View File

@ -1336,7 +1336,7 @@
"EnableFasterAnimations": "Hurtigere animationer", "EnableFasterAnimations": "Hurtigere animationer",
"DisablePlugin": "Deaktiver", "DisablePlugin": "Deaktiver",
"EnablePlugin": "Aktiver", "EnablePlugin": "Aktiver",
"DirectPlayHelp": "Kilde filen er kompatibel med denne klient, og modtager filen uden brug af omkodning.", "DirectPlayHelp": "Kilde filen er kompatibel med denne klient og modtager filen uden brug af omkodning.",
"EnableEnhancedNvdecDecoder": "Aktiver forbedret NVDEC-dekoder", "EnableEnhancedNvdecDecoder": "Aktiver forbedret NVDEC-dekoder",
"MessagePlaybackError": "Der opstod en fejl under afspilning af denne fil på din Google Cast modtager.", "MessagePlaybackError": "Der opstod en fejl under afspilning af denne fil på din Google Cast modtager.",
"MessageChromecastConnectionError": "Din Google Cast modtager kan ikke komme i kontakt med Jellyfin serveren. Undersøg venligst forbindelsen og prøv igen.", "MessageChromecastConnectionError": "Din Google Cast modtager kan ikke komme i kontakt med Jellyfin serveren. Undersøg venligst forbindelsen og prøv igen.",
@ -1702,5 +1702,19 @@
"SubtitleCyan": "Cyan", "SubtitleCyan": "Cyan",
"SubtitleMagenta": "Magenta", "SubtitleMagenta": "Magenta",
"AllowCollectionManagement": "Tillad denne bruger at administrere samlinger", "AllowCollectionManagement": "Tillad denne bruger at administrere samlinger",
"AllowSegmentDeletion": "Slet segmenter" "AllowSegmentDeletion": "Slet segmenter",
"HeaderEpisodesStatus": "Episodestatus",
"GoHome": "Gå Hjem",
"EnableAudioNormalizationHelp": "Audionormalisering tilføjer en konstant forstærkning for at holde gennemsnittet på et ønsket niveau (-18 dB).",
"EnableAudioNormalization": "Audio Normalisering",
"GridView": "Gittervisning",
"HeaderConfirmRepositoryInstallation": "Bekræft installation af plugin-repositorium",
"BackdropScreensaver": "Screensaver baggrund",
"GetThePlugin": "Få pluginnet",
"AllowSegmentDeletionHelp": "Slet gamle segmenter, når de er blevet sendt til klienten. Dette forhindrer, at man skal gemme hele den transkodede fil på disken. Fungerer kun med throttling aktiveret. Slå dette fra, hvis du oplever afspilningsproblemer.",
"LabelThrottleDelaySeconds": "Begræns efter",
"LabelThrottleDelaySecondsHelp": "Tid i sekunder, hvorefter transcoderen vil blive begrænset. Skal være stor nok til, at klienten kan opretholde en sund buffer. Virker kun, hvis throttling er aktiveret.",
"LabelSegmentKeepSeconds": "Tid at gemme segmenter i",
"LabelSegmentKeepSecondsHelp": "Tid i sekunder, som segmenter skal gemmes i, før de overskrives. Skal være større end \"Begræns efter\". Virker kun, hvis sletning af segmenter er aktiveret.",
"HeaderGuestCast": "Gæstestjerner"
} }

View File

@ -1377,7 +1377,7 @@
"LabelColorSpace": "Farbraum", "LabelColorSpace": "Farbraum",
"MediaInfoColorSpace": "Farbraum", "MediaInfoColorSpace": "Farbraum",
"VideoAudio": "Videoton", "VideoAudio": "Videoton",
"AllowTonemappingHelp": "Tone-Mapping kann den Dynamikumfang eines Videos von HDR nach SDR wandeln und dabei die für die Darstellung der Originalszene sehr wichtigen Bilddetails und Farben beibehalten. Dies funktioniert zurzeit nur bei HDR10-, HLG- und Dolby-Vision-Videos und benötigt die entsprechende OpenCL- oder CUDA-Laufzeitumgebung.", "AllowTonemappingHelp": "Tone-Mapping kann den Dynamikumfang eines Videos von HDR nach SDR wandeln und dabei die für die Darstellung der Originalszene sehr wichtigen Bilddetails und Farben beibehalten. Dies funktioniert zurzeit nur bei HDR10, HLG und Dolby-Vision Videos und benötigt die entsprechende OpenCL- oder CUDA-Laufzeitumgebung.",
"TonemappingRangeHelp": "Wähle den Ausgabefarbraum aus. Auto ist derselbe wie der Eingabefarbraum.", "TonemappingRangeHelp": "Wähle den Ausgabefarbraum aus. Auto ist derselbe wie der Eingabefarbraum.",
"TonemappingAlgorithmHelp": "Das Tone-Mapping kann fein abgestimmt werden. Wenn du mit diesen Optionen nicht vertraut bist, behalte einfach den Standardwert bei. Der empfohlene Wert ist \"BT.2390\".", "TonemappingAlgorithmHelp": "Das Tone-Mapping kann fein abgestimmt werden. Wenn du mit diesen Optionen nicht vertraut bist, behalte einfach den Standardwert bei. Der empfohlene Wert ist \"BT.2390\".",
"LabelTonemappingAlgorithm": "Wähle den zu verwendenden Tone-Mapping-Algorithmus aus", "LabelTonemappingAlgorithm": "Wähle den zu verwendenden Tone-Mapping-Algorithmus aus",
@ -1407,7 +1407,7 @@
"QuickConnectDescription": "Für das Einloggen mit Quick Connect wähle den 'Quick Connect'-Knopf auf deinem Gerät, mit dem du dich anmelden möchtest, und gib den unten angezeigten Code ein.", "QuickConnectDescription": "Für das Einloggen mit Quick Connect wähle den 'Quick Connect'-Knopf auf deinem Gerät, mit dem du dich anmelden möchtest, und gib den unten angezeigten Code ein.",
"QuickConnectDeactivated": "Quick Connect wurde deaktiviert, bevor der Login verifiziert werden konnte", "QuickConnectDeactivated": "Quick Connect wurde deaktiviert, bevor der Login verifiziert werden konnte",
"QuickConnectAuthorizeFail": "Unbekannter Quick Connect-Code", "QuickConnectAuthorizeFail": "Unbekannter Quick Connect-Code",
"QuickConnectAuthorizeSuccess": "Anfrage autorisiert", "QuickConnectAuthorizeSuccess": "Das Gerät wurde erfolgreich authentifiziert!",
"QuickConnectAuthorizeCode": "Login Code {0} eingeben", "QuickConnectAuthorizeCode": "Login Code {0} eingeben",
"QuickConnectActivationSuccessful": "Erfolgreich aktiviert", "QuickConnectActivationSuccessful": "Erfolgreich aktiviert",
"EnableQuickConnect": "Quick Connect auf diesem Server aktivieren", "EnableQuickConnect": "Quick Connect auf diesem Server aktivieren",
@ -1733,7 +1733,7 @@
"PasswordRequiredForAdmin": "Für Admin Konten wird ein Passwort benötigt.", "PasswordRequiredForAdmin": "Für Admin Konten wird ein Passwort benötigt.",
"LabelEnableLUFSScan": "LUFS-Scan aktivieren", "LabelEnableLUFSScan": "LUFS-Scan aktivieren",
"LabelSyncPlayNoGroups": "Keine Gruppen verfügbar", "LabelSyncPlayNoGroups": "Keine Gruppen verfügbar",
"LabelEnableLUFSScanHelp": "Aktiviert den LUFS-Scan für Musik (Dies erfordert mehr Zeit und Ressourcen).", "LabelEnableLUFSScanHelp": "Clients können die Audio Wiedergabe normalisieren, um die selbe Lautstärke für mehrere Stücke zu bekommen.\nDies verlängert den Bibliotheksscan und benötigt mehr Ressourcen.",
"Notifications": "Benachrichtigungen", "Notifications": "Benachrichtigungen",
"NotificationsMovedMessage": "Die Benachrichtigungsfunktion wurde zum Webhook Plugin verschoben.", "NotificationsMovedMessage": "Die Benachrichtigungsfunktion wurde zum Webhook Plugin verschoben.",
"EnableAudioNormalizationHelp": "Die Audionormalisierung fügt eine konstante Verstärkung hinzu, um den Durchschnitt auf einem gewünschten Pegel zu halten (-18 dB).", "EnableAudioNormalizationHelp": "Die Audionormalisierung fügt eine konstante Verstärkung hinzu, um den Durchschnitt auf einem gewünschten Pegel zu halten (-18 dB).",
@ -1758,8 +1758,8 @@
"HeaderEpisodesStatus": "Episodenstatus", "HeaderEpisodesStatus": "Episodenstatus",
"AllowSegmentDeletion": "Segmente löschen", "AllowSegmentDeletion": "Segmente löschen",
"AllowSegmentDeletionHelp": "Alte Segmente löschen, nachdem sie zum Client gesendet wurden. Damit muss nicht die gesamte transkodierte Datei zwischengespeichert werden. Sollten Wiedergabeprobleme auftreten, kann diese Einstellung deaktiviert werden.", "AllowSegmentDeletionHelp": "Alte Segmente löschen, nachdem sie zum Client gesendet wurden. Damit muss nicht die gesamte transkodierte Datei zwischengespeichert werden. Sollten Wiedergabeprobleme auftreten, kann diese Einstellung deaktiviert werden.",
"LabelThrottleDelaySeconds": "Limitieren nach", "LabelThrottleDelaySeconds": "Drosseln nach",
"LabelThrottleDelaySecondsHelp": "Zeit, in Sekunden, nach der die Transkodierung limitiert wird. Muss groß genug sein um dem Client eine problemlose Wiedergabe zu ermöglichen. Funktioniert nur wenn \"Transkodierung drosseln\" aktiviert ist.", "LabelThrottleDelaySecondsHelp": "Zeit, in Sekunden, nach der die Transkodierung gedrosselt wird. Muss groß genug sein um dem Client eine problemlose Wiedergabe zu ermöglichen. Funktioniert nur wenn \"Transkodierung drosseln\" aktiviert ist.",
"LabelSegmentKeepSeconds": "Zeit um Segmente zu behalten", "LabelSegmentKeepSeconds": "Zeit um Segmente zu behalten",
"LabelSegmentKeepSecondsHelp": "Zeit, in Sekunden, in der Segmente nicht überschrieben werden dürfen. Muss größer sein als \"Limitieren nach\". Funktioniert nur wenn \"Segmente löschen\" aktiviert ist.", "LabelSegmentKeepSecondsHelp": "Zeit, in Sekunden, in der Segmente nicht überschrieben werden dürfen. Muss größer sein als \"Limitieren nach\". Funktioniert nur wenn \"Segmente löschen\" aktiviert ist.",
"LogoScreensaver": "Logo Bildschirmschoner", "LogoScreensaver": "Logo Bildschirmschoner",
@ -1769,5 +1769,12 @@
"GoHome": "Startseite", "GoHome": "Startseite",
"AiTranslated": "AI übersetzt", "AiTranslated": "AI übersetzt",
"MachineTranslated": "maschinenübersetzt", "MachineTranslated": "maschinenübersetzt",
"AllowAv1Encoding": "Encodierung ins AV1 Format erlauben" "AllowAv1Encoding": "Encodierung ins AV1 Format erlauben",
"LabelIsHearingImpaired": "Für Hörgeschädigte (SDH)",
"LabelBackdropScreensaverInterval": "Hintergrund-Bildschirmschoner-Intervall",
"BackdropScreensaver": "Hintergrund Bildschirmschoner",
"ForeignPartsOnly": "Erzwungen/Nur ausländische Teile",
"HearingImpairedShort": "BaFa/SDH",
"HeaderGuestCast": "Gast Stars",
"LabelBackdropScreensaverIntervalHelp": "Die Zeit in Sekunden zwischen dem Wechsel verschiedener Hintergrundbilder im Bildschirmschoner."
} }

Some files were not shown because too many files have changed in this diff Show More