mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge branch 'master' into master
This commit is contained in:
commit
21b0da8e9c
107 changed files with 4393 additions and 1789 deletions
23
.devcontainer/devcontainer.json
Normal file
23
.devcontainer/devcontainer.json
Normal 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"
|
||||
}
|
|
@ -66,6 +66,7 @@ module.exports = {
|
|||
'no-unused-expressions': ['off'],
|
||||
'@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
|
||||
'no-unused-private-class-members': ['error'],
|
||||
'no-useless-rename': ['error'],
|
||||
'no-useless-constructor': ['off'],
|
||||
'@typescript-eslint/no-useless-constructor': ['error'],
|
||||
'no-var': ['error'],
|
||||
|
|
3
.github/workflows/tsc.yml
vendored
3
.github/workflows/tsc.yml
vendored
|
@ -27,3 +27,6 @@ jobs:
|
|||
|
||||
- name: Run tsc
|
||||
run: npm run build:check
|
||||
|
||||
- name: Run test suite
|
||||
run: npm run test
|
||||
|
|
|
@ -66,6 +66,7 @@
|
|||
- [Fishbigger](https://github.com/fishbigger)
|
||||
- [sleepycatcoding](https://github.com/sleepycatcoding)
|
||||
- [TheMelmacian](https://github.com/TheMelmacian)
|
||||
- [tehciolo](https://github.com/tehciolo)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
|
|
@ -77,8 +77,9 @@ Jellyfin Web is the frontend used for most of the clients available for end user
|
|||
.
|
||||
└── src
|
||||
├── apps
|
||||
│ ├── experimental # New experimental app layout
|
||||
│ └── stable # Classic (stable) app layout
|
||||
│ ├── dashboard # Admin dashboard app layout and routes
|
||||
│ ├── experimental # New experimental app layout and routes
|
||||
│ └── stable # Classic (stable) app layout and routes
|
||||
├── assets # Static assets
|
||||
├── components # Higher order visual components and React components
|
||||
├── 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
|
||||
├── libraries # Third party libraries 🧹
|
||||
├── plugins # Client plugins
|
||||
├── routes # React routes/pages
|
||||
├── scripts # Random assortment of visual components and utilities 🐉
|
||||
├── strings # Translation files
|
||||
├── styles # Common app Sass stylesheets
|
||||
|
|
1674
package-lock.json
generated
1674
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -58,6 +58,7 @@
|
|||
"stylelint-scss": "5.0.0",
|
||||
"ts-loader": "9.4.4",
|
||||
"typescript": "5.0.4",
|
||||
"vitest": "0.34.6",
|
||||
"webpack": "5.88.1",
|
||||
"webpack-bundle-analyzer": "4.9.1",
|
||||
"webpack-cli": "5.1.4",
|
||||
|
@ -145,6 +146,8 @@
|
|||
"build:check": "tsc --noEmit",
|
||||
"escheck": "es-check",
|
||||
"lint": "eslint \"./\"",
|
||||
"test": "vitest --watch=false",
|
||||
"test:watch": "vitest",
|
||||
"stylelint": "npm run stylelint:css && npm run stylelint:scss",
|
||||
"stylelint:css": "stylelint \"src/**/*.css\"",
|
||||
"stylelint:scss": "stylelint --config=\".stylelintrc.scss.json\" \"src/**/*.scss\""
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import loadable from '@loadable/component';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { History } from '@remix-run/router';
|
||||
import React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
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 Backdrop from 'components/Backdrop';
|
||||
import { HistoryRouter } from 'components/router/HistoryRouter';
|
||||
|
@ -12,6 +14,7 @@ import { ApiProvider } from 'hooks/useApi';
|
|||
import { WebConfigProvider } from 'hooks/useWebConfig';
|
||||
import theme from 'themes/theme';
|
||||
|
||||
const DashboardApp = loadable(() => import('./apps/dashboard/App'));
|
||||
const ExperimentalApp = loadable(() => import('./apps/experimental/App'));
|
||||
const StableApp = loadable(() => import('./apps/stable/App'));
|
||||
|
||||
|
@ -21,16 +24,22 @@ const RootAppLayout = () => {
|
|||
const layoutMode = localStorage.getItem('layout');
|
||||
const isExperimentalLayout = layoutMode === 'experimental';
|
||||
|
||||
const location = useLocation();
|
||||
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
|
||||
.some(path => location.pathname.startsWith(`/${path}`));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Backdrop />
|
||||
<AppHeader isHidden={isExperimentalLayout} />
|
||||
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
|
||||
|
||||
{
|
||||
isExperimentalLayout ?
|
||||
<ExperimentalApp /> :
|
||||
<StableApp />
|
||||
}
|
||||
|
||||
<DashboardApp />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
66
src/apps/dashboard/App.tsx
Normal file
66
src/apps/dashboard/App.tsx
Normal 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;
|
108
src/apps/dashboard/AppLayout.tsx
Normal file
108
src/apps/dashboard/AppLayout.tsx
Normal 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;
|
22
src/apps/dashboard/AppOverrides.scss
Normal file
22
src/apps/dashboard/AppOverrides.scss
Normal 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;
|
||||
}
|
||||
}
|
29
src/apps/dashboard/components/drawer/AppDrawer.tsx
Normal file
29
src/apps/dashboard/components/drawer/AppDrawer.tsx
Normal 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;
|
|
@ -19,10 +19,10 @@ import ListItemLink from 'components/ListItemLink';
|
|||
import globalize from 'scripts/globalize';
|
||||
|
||||
const PLUGIN_PATHS = [
|
||||
'/installedplugins.html',
|
||||
'/availableplugins.html',
|
||||
'/repositories.html',
|
||||
'/addplugin.html',
|
||||
'/dashboard/plugins',
|
||||
'/dashboard/plugins/catalog',
|
||||
'/dashboard/plugins/repositories',
|
||||
'/dashboard/plugins/add',
|
||||
'/configurationpage'
|
||||
];
|
||||
|
||||
|
@ -41,7 +41,7 @@ const AdvancedDrawerSection = () => {
|
|||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/networking.html'>
|
||||
<ListItemLink to='/dashboard/networking'>
|
||||
<ListItemIcon>
|
||||
<Lan />
|
||||
</ListItemIcon>
|
||||
|
@ -49,7 +49,7 @@ const AdvancedDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/apikeys.html'>
|
||||
<ListItemLink to='/dashboard/keys'>
|
||||
<ListItemIcon>
|
||||
<VpnKey />
|
||||
</ListItemIcon>
|
||||
|
@ -57,7 +57,7 @@ const AdvancedDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/log.html'>
|
||||
<ListItemLink to='/dashboard/logs'>
|
||||
<ListItemIcon>
|
||||
<Article />
|
||||
</ListItemIcon>
|
||||
|
@ -65,7 +65,7 @@ const AdvancedDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/notificationsettings.html'>
|
||||
<ListItemLink to='/dashboard/notifications'>
|
||||
<ListItemIcon>
|
||||
<EditNotifications />
|
||||
</ListItemIcon>
|
||||
|
@ -73,7 +73,7 @@ const AdvancedDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/installedplugins.html' selected={false}>
|
||||
<ListItemLink to='/dashboard/plugins' selected={false}>
|
||||
<ListItemIcon>
|
||||
<Extension />
|
||||
</ListItemIcon>
|
||||
|
@ -83,19 +83,19 @@ const AdvancedDrawerSection = () => {
|
|||
</ListItem>
|
||||
<Collapse in={isPluginSectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/installedplugins.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/plugins' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabMyPlugins')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/availableplugins.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/plugins/catalog' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabCatalog')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/repositories.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/plugins/repositories' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabRepositories')} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
</Collapse>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/scheduledtasks.html'>
|
||||
<ListItemLink to='/dashboard/tasks'>
|
||||
<ListItemIcon>
|
||||
<Schedule />
|
||||
</ListItemIcon>
|
|
@ -12,8 +12,8 @@ import ListItemLink from 'components/ListItemLink';
|
|||
import globalize from 'scripts/globalize';
|
||||
|
||||
const DLNA_PATHS = [
|
||||
'/dlnasettings.html',
|
||||
'/dlnaprofiles.html'
|
||||
'/dashboard/dlna',
|
||||
'/dashboard/dlna/profiles'
|
||||
];
|
||||
|
||||
const DevicesDrawerSection = () => {
|
||||
|
@ -31,7 +31,7 @@ const DevicesDrawerSection = () => {
|
|||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/devices.html'>
|
||||
<ListItemLink to='/dashboard/devices'>
|
||||
<ListItemIcon>
|
||||
<Devices />
|
||||
</ListItemIcon>
|
||||
|
@ -47,7 +47,7 @@ const DevicesDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dlnasettings.html' selected={false}>
|
||||
<ListItemLink to='/dashboard/dlna' selected={false}>
|
||||
<ListItemIcon>
|
||||
<Input />
|
||||
</ListItemIcon>
|
||||
|
@ -57,10 +57,10 @@ const DevicesDrawerSection = () => {
|
|||
</ListItem>
|
||||
<Collapse in={isDlnaSectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/dlnasettings.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/dlna' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Settings')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dlnaprofiles.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/dlna/profiles' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabProfiles')} />
|
||||
</ListItemLink>
|
||||
</List>
|
|
@ -20,7 +20,7 @@ const LiveTvDrawerSection = () => {
|
|||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/livetvstatus.html'>
|
||||
<ListItemLink to='/dashboard/livetv'>
|
||||
<ListItemIcon>
|
||||
<LiveTv />
|
||||
</ListItemIcon>
|
||||
|
@ -28,7 +28,7 @@ const LiveTvDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/livetvsettings.html'>
|
||||
<ListItemLink to='/dashboard/recordings'>
|
||||
<ListItemIcon>
|
||||
<Dvr />
|
||||
</ListItemIcon>
|
|
@ -12,16 +12,16 @@ import ListItemLink from 'components/ListItemLink';
|
|||
import globalize from 'scripts/globalize';
|
||||
|
||||
const LIBRARY_PATHS = [
|
||||
'/library.html',
|
||||
'/librarydisplay.html',
|
||||
'/metadataimages.html',
|
||||
'/metadatanfo.html'
|
||||
'/dashboard/libraries',
|
||||
'/dashboard/libraries/display',
|
||||
'/dashboard/libraries/metadata',
|
||||
'/dashboard/libraries/nfo'
|
||||
];
|
||||
|
||||
const PLAYBACK_PATHS = [
|
||||
'/encodingsettings.html',
|
||||
'/playbackconfiguration.html',
|
||||
'/streamingsettings.html'
|
||||
'/dashboard/playback/transcoding',
|
||||
'/dashboard/playback/resume',
|
||||
'/dashboard/playback/streaming'
|
||||
];
|
||||
|
||||
const ServerDrawerSection = () => {
|
||||
|
@ -40,7 +40,7 @@ const ServerDrawerSection = () => {
|
|||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard.html'>
|
||||
<ListItemLink to='/dashboard'>
|
||||
<ListItemIcon>
|
||||
<Dashboard />
|
||||
</ListItemIcon>
|
||||
|
@ -48,7 +48,7 @@ const ServerDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboardgeneral.html'>
|
||||
<ListItemLink to='/dashboard/settings'>
|
||||
<ListItemIcon>
|
||||
<Settings />
|
||||
</ListItemIcon>
|
||||
|
@ -56,7 +56,7 @@ const ServerDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/userprofiles.html'>
|
||||
<ListItemLink to='/dashboard/users'>
|
||||
<ListItemIcon>
|
||||
<People />
|
||||
</ListItemIcon>
|
||||
|
@ -64,7 +64,7 @@ const ServerDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/library.html' selected={false}>
|
||||
<ListItemLink to='/dashboard/libraries' selected={false}>
|
||||
<ListItemIcon>
|
||||
<LibraryAdd />
|
||||
</ListItemIcon>
|
||||
|
@ -74,22 +74,22 @@ const ServerDrawerSection = () => {
|
|||
</ListItem>
|
||||
<Collapse in={isLibrarySectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/library.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/libraries' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('HeaderLibraries')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/librarydisplay.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/libraries/display' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Display')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/metadataimages.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/libraries/metadata' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Metadata')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/metadatanfo.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/libraries/nfo' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabNfoSettings')} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
</Collapse>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/encodingsettings.html' selected={false}>
|
||||
<ListItemLink to='/dashboard/playback/transcoding' selected={false}>
|
||||
<ListItemIcon>
|
||||
<PlayCircle />
|
||||
</ListItemIcon>
|
||||
|
@ -99,13 +99,13 @@ const ServerDrawerSection = () => {
|
|||
</ListItem>
|
||||
<Collapse in={isPlaybackSectionOpen} timeout='auto' unmountOnExit>
|
||||
<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')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/playbackconfiguration.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/playback/resume' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('ButtonResume')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/streamingsettings.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/playback/streaming' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabStreaming')} />
|
||||
</ListItemLink>
|
||||
</List>
|
12
src/apps/dashboard/routes/_asyncRoutes.ts
Normal file
12
src/apps/dashboard/routes/_asyncRoutes.ts
Normal 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' }
|
||||
];
|
|
@ -1,170 +1,164 @@
|
|||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
import type { LegacyRoute } from 'components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||
{
|
||||
path: 'dashboard.html',
|
||||
path: '/dashboard',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dashboard',
|
||||
view: 'dashboard/dashboard.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dashboardgeneral.html',
|
||||
path: 'settings',
|
||||
pageProps: {
|
||||
controller: 'dashboard/general',
|
||||
view: 'dashboard/general.html'
|
||||
}
|
||||
}, {
|
||||
path: 'networking.html',
|
||||
path: 'networking',
|
||||
pageProps: {
|
||||
controller: 'dashboard/networking',
|
||||
view: 'dashboard/networking.html'
|
||||
}
|
||||
}, {
|
||||
path: 'devices.html',
|
||||
path: 'devices',
|
||||
pageProps: {
|
||||
controller: 'dashboard/devices/devices',
|
||||
view: 'dashboard/devices/devices.html'
|
||||
}
|
||||
}, {
|
||||
path: 'device.html',
|
||||
path: 'devices/edit',
|
||||
pageProps: {
|
||||
controller: 'dashboard/devices/device',
|
||||
view: 'dashboard/devices/device.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dlnaprofile.html',
|
||||
path: 'dlna/profiles/edit',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dlna/profile',
|
||||
view: 'dashboard/dlna/profile.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dlnaprofiles.html',
|
||||
path: 'dlna/profiles',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dlna/profiles',
|
||||
view: 'dashboard/dlna/profiles.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dlnasettings.html',
|
||||
path: 'dlna',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dlna/settings',
|
||||
view: 'dashboard/dlna/settings.html'
|
||||
}
|
||||
}, {
|
||||
path: 'addplugin.html',
|
||||
path: 'plugins/add',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/add/index',
|
||||
view: 'dashboard/plugins/add/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'library.html',
|
||||
path: 'libraries',
|
||||
pageProps: {
|
||||
controller: 'dashboard/library',
|
||||
view: 'dashboard/library.html'
|
||||
}
|
||||
}, {
|
||||
path: 'librarydisplay.html',
|
||||
path: 'libraries/display',
|
||||
pageProps: {
|
||||
controller: 'dashboard/librarydisplay',
|
||||
view: 'dashboard/librarydisplay.html'
|
||||
}
|
||||
}, {
|
||||
path: 'edititemmetadata.html',
|
||||
pageProps: {
|
||||
controller: 'edititemmetadata',
|
||||
view: 'edititemmetadata.html'
|
||||
}
|
||||
}, {
|
||||
path: 'encodingsettings.html',
|
||||
path: 'playback/transcoding',
|
||||
pageProps: {
|
||||
controller: 'dashboard/encodingsettings',
|
||||
view: 'dashboard/encodingsettings.html'
|
||||
}
|
||||
}, {
|
||||
path: 'log.html',
|
||||
path: 'logs',
|
||||
pageProps: {
|
||||
controller: 'dashboard/logs',
|
||||
view: 'dashboard/logs.html'
|
||||
}
|
||||
}, {
|
||||
path: 'metadataimages.html',
|
||||
path: 'libraries/metadata',
|
||||
pageProps: {
|
||||
controller: 'dashboard/metadataImages',
|
||||
view: 'dashboard/metadataimages.html'
|
||||
}
|
||||
}, {
|
||||
path: 'metadatanfo.html',
|
||||
path: 'libraries/nfo',
|
||||
pageProps: {
|
||||
controller: 'dashboard/metadatanfo',
|
||||
view: 'dashboard/metadatanfo.html'
|
||||
}
|
||||
}, {
|
||||
path: 'playbackconfiguration.html',
|
||||
path: 'playback/resume',
|
||||
pageProps: {
|
||||
controller: 'dashboard/playback',
|
||||
view: 'dashboard/playback.html'
|
||||
}
|
||||
}, {
|
||||
path: 'availableplugins.html',
|
||||
path: 'plugins/catalog',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/available/index',
|
||||
view: 'dashboard/plugins/available/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'repositories.html',
|
||||
path: 'plugins/repositories',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/repositories/index',
|
||||
view: 'dashboard/plugins/repositories/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvguideprovider.html',
|
||||
path: 'livetv/guide',
|
||||
pageProps: {
|
||||
controller: 'livetvguideprovider',
|
||||
view: 'livetvguideprovider.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvsettings.html',
|
||||
path: 'recordings',
|
||||
pageProps: {
|
||||
controller: 'livetvsettings',
|
||||
view: 'livetvsettings.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvstatus.html',
|
||||
path: 'livetv',
|
||||
pageProps: {
|
||||
controller: 'livetvstatus',
|
||||
view: 'livetvstatus.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvtuner.html',
|
||||
path: 'livetv/tuner',
|
||||
pageProps: {
|
||||
controller: 'livetvtuner',
|
||||
view: 'livetvtuner.html'
|
||||
}
|
||||
}, {
|
||||
path: 'installedplugins.html',
|
||||
path: 'plugins',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/installed/index',
|
||||
view: 'dashboard/plugins/installed/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'scheduledtask.html',
|
||||
path: 'tasks/edit',
|
||||
pageProps: {
|
||||
controller: 'dashboard/scheduledtasks/scheduledtask',
|
||||
view: 'dashboard/scheduledtasks/scheduledtask.html'
|
||||
}
|
||||
}, {
|
||||
path: 'scheduledtasks.html',
|
||||
path: 'tasks',
|
||||
pageProps: {
|
||||
controller: 'dashboard/scheduledtasks/scheduledtasks',
|
||||
view: 'dashboard/scheduledtasks/scheduledtasks.html'
|
||||
}
|
||||
}, {
|
||||
path: 'apikeys.html',
|
||||
path: 'keys',
|
||||
pageProps: {
|
||||
controller: 'dashboard/apikeys',
|
||||
view: 'dashboard/apikeys.html'
|
||||
}
|
||||
}, {
|
||||
path: 'streamingsettings.html',
|
||||
path: 'playback/streaming',
|
||||
pageProps: {
|
||||
view: 'dashboard/streaming.html',
|
||||
controller: 'dashboard/streaming'
|
40
src/apps/dashboard/routes/_redirects.ts
Normal file
40
src/apps/dashboard/routes/_redirects.ts
Normal 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' }
|
||||
];
|
|
@ -19,9 +19,9 @@ import { parseISO8601Date, toLocaleDateString, toLocaleTimeString } from 'script
|
|||
import globalize from 'scripts/globalize';
|
||||
import { toBoolean } from 'utils/string';
|
||||
|
||||
import LogLevelChip from '../../components/activityTable/LogLevelChip';
|
||||
import OverviewCell from '../../components/activityTable/OverviewCell';
|
||||
import GridActionsCellLink from '../../components/GridActionsCellLink';
|
||||
import LogLevelChip from '../components/activityTable/LogLevelChip';
|
||||
import OverviewCell from '../components/activityTable/OverviewCell';
|
||||
import GridActionsCellLink from '../components/dataGrid/GridActionsCellLink';
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 25;
|
||||
const VIEW_PARAM = 'useractivity';
|
||||
|
@ -68,7 +68,7 @@ const Activity = () => {
|
|||
sx={{ padding: 0 }}
|
||||
title={users[row.UserId]?.Name ?? undefined}
|
||||
component={Link}
|
||||
to={`/useredit.html?userId=${row.UserId}`}
|
||||
to={`/dashboard/users/profile?userId=${row.UserId}`}
|
||||
>
|
||||
<UserAvatar user={users[row.UserId]} />
|
||||
</IconButton>
|
|
@ -9,7 +9,7 @@ const PluginLink = () => (
|
|||
__html: `<a
|
||||
is='emby-linkbutton'
|
||||
class='button-link'
|
||||
href='#/addplugin.html?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173'
|
||||
href='#/dashboard/plugins/add?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173'
|
||||
>
|
||||
${globalize.translate('GetThePlugin')}
|
||||
</a>`
|
|
@ -140,7 +140,7 @@ const UserNew: FunctionComponent = () => {
|
|||
}
|
||||
|
||||
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 => {
|
||||
console.error('[usernew] failed to navigate to edit user page', err);
|
||||
});
|
|
@ -85,21 +85,21 @@ const UserProfiles: FunctionComponent = () => {
|
|||
callback: function (id: string) {
|
||||
switch (id) {
|
||||
case 'open':
|
||||
Dashboard.navigate('useredit.html?userId=' + userId)
|
||||
Dashboard.navigate('/dashboard/users/profile?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to user edit page', err);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'access':
|
||||
Dashboard.navigate('userlibraryaccess.html?userId=' + userId)
|
||||
Dashboard.navigate('/dashboard/users/access?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to user library page', err);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'parentalcontrol':
|
||||
Dashboard.navigate('userparentalcontrol.html?userId=' + userId)
|
||||
Dashboard.navigate('/dashboard/users/parentalcontrol?userId=' + userId)
|
||||
.catch(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() {
|
||||
Dashboard.navigate('usernew.html')
|
||||
Dashboard.navigate('/dashboard/users/add')
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to new user page', err);
|
||||
});
|
|
@ -32,7 +32,7 @@ const getCheckedElementDataIds = (elements: NodeListOf<Element>) => (
|
|||
);
|
||||
|
||||
function onSaveComplete() {
|
||||
Dashboard.navigate('userprofiles.html')
|
||||
Dashboard.navigate('/dashboard/users')
|
||||
.catch(err => {
|
||||
console.error('[useredit] failed to navigate to user profile', err);
|
||||
});
|
|
@ -1,16 +1,16 @@
|
|||
import React from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
||||
import { REDIRECTS } from 'apps/stable/routes/_redirects';
|
||||
import ConnectionRequired from 'components/ConnectionRequired';
|
||||
import ServerContentPage from 'components/ServerContentPage';
|
||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||
import { toRedirectRoute } from 'components/router/Redirect';
|
||||
|
||||
import AppLayout from './AppLayout';
|
||||
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
||||
import { ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
||||
|
||||
const ExperimentalApp = () => {
|
||||
return (
|
||||
|
@ -22,16 +22,6 @@ const ExperimentalApp = () => {
|
|||
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
|
||||
</Route>
|
||||
|
||||
{/* Admin routes */}
|
||||
<Route element={<ConnectionRequired isAdminRequired />}>
|
||||
{ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)}
|
||||
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
|
||||
|
||||
<Route path='configurationpage' element={
|
||||
<ServerContentPage view='/web/configurationpage' />
|
||||
} />
|
||||
</Route>
|
||||
|
||||
{/* Public routes */}
|
||||
<Route element={<ConnectionRequired isUserRequired={false} />}>
|
||||
<Route index element={<Navigate replace to='/home.html' />} />
|
||||
|
@ -42,6 +32,15 @@ const ExperimentalApp = () => {
|
|||
|
||||
{/* Redirects for old paths */}
|
||||
{REDIRECTS.map(toRedirectRoute)}
|
||||
|
||||
{/* Ignore dashboard routes */}
|
||||
{Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => (
|
||||
<Route
|
||||
key={key}
|
||||
path={`/${path}/*`}
|
||||
element={null}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,11 +10,6 @@ $mui-bp-xl: 1536px;
|
|||
position: relative;
|
||||
}
|
||||
|
||||
// Fix dashboard pages layout to work with drawer
|
||||
.dashboardDocument .skinBody {
|
||||
position: unset;
|
||||
}
|
||||
|
||||
// Hide some items from the user "settings" page that are in the drawer
|
||||
#myPreferencesMenuPage {
|
||||
.lnkQuickConnectPreferences,
|
||||
|
@ -26,8 +21,7 @@ $mui-bp-xl: 1536px;
|
|||
|
||||
// Fix the padding of some pages
|
||||
.homePage.libraryPage, // Home page
|
||||
.libraryPage:not(.withTabs), // Tabless library pages
|
||||
.content-primary.content-primary { // Dashboard pages
|
||||
.libraryPage:not(.withTabs) { // Tabless library pages
|
||||
padding-top: 3.25rem !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,15 @@
|
|||
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 { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from '../../routes/asyncRoutes';
|
||||
import { LEGACY_ADMIN_ROUTES, LEGACY_USER_ROUTES } from '../../routes/legacyRoutes';
|
||||
|
||||
import AdvancedDrawerSection from './dashboard/AdvancedDrawerSection';
|
||||
import DevicesDrawerSection from './dashboard/DevicesDrawerSection';
|
||||
import LiveTvDrawerSection from './dashboard/LiveTvDrawerSection';
|
||||
import PluginDrawerSection from './dashboard/PluginDrawerSection';
|
||||
import ServerDrawerSection from './dashboard/ServerDrawerSection';
|
||||
import MainDrawerContent from './MainDrawerContent';
|
||||
import { ASYNC_USER_ROUTES } from '../../routes/asyncRoutes';
|
||||
import { LEGACY_USER_ROUTES } from '../../routes/legacyRoutes';
|
||||
import { isTabPath } from '../tabs/tabRoutes';
|
||||
|
||||
export const DRAWER_WIDTH = 240;
|
||||
import MainDrawerContent from './MainDrawerContent';
|
||||
|
||||
const DRAWERLESS_ROUTES = [
|
||||
'edititemmetadata.html', // metadata manager
|
||||
'video' // video player
|
||||
];
|
||||
|
||||
|
@ -26,77 +18,29 @@ const MAIN_DRAWER_ROUTES = [
|
|||
...LEGACY_USER_ROUTES
|
||||
].filter(route => !DRAWERLESS_ROUTES.includes(route.path));
|
||||
|
||||
const ADMIN_DRAWER_ROUTES = [
|
||||
...ASYNC_ADMIN_ROUTES,
|
||||
...LEGACY_ADMIN_ROUTES,
|
||||
{ path: '/configurationpage' } // Plugin configuration page
|
||||
].filter(route => !DRAWERLESS_ROUTES.includes(route.path));
|
||||
|
||||
/** Utility function to check if a path has a drawer. */
|
||||
export const isDrawerPath = (path: string) => (
|
||||
MAIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path)
|
||||
|| ADMIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path)
|
||||
);
|
||||
|
||||
const Drawer: FC<ResponsiveDrawerProps> = ({ children, ...props }) => {
|
||||
const location = useLocation();
|
||||
const hasSecondaryToolBar = isTabPath(location.pathname);
|
||||
|
||||
return (
|
||||
<ResponsiveDrawer
|
||||
{...props}
|
||||
hasSecondaryToolBar={hasSecondaryToolBar}
|
||||
>
|
||||
{children}
|
||||
</ResponsiveDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
||||
open = false,
|
||||
onClose,
|
||||
onOpen
|
||||
}) => (
|
||||
<Routes>
|
||||
{
|
||||
MAIN_DRAWER_ROUTES.map(route => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={
|
||||
<Drawer
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
<MainDrawerContent />
|
||||
</Drawer>
|
||||
}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{
|
||||
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>
|
||||
);
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const hasSecondaryToolBar = isTabPath(location.pathname);
|
||||
|
||||
return (
|
||||
<ResponsiveDrawer
|
||||
hasSecondaryToolBar={hasSecondaryToolBar}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
<MainDrawerContent />
|
||||
</ResponsiveDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppDrawer;
|
||||
|
|
|
@ -150,7 +150,7 @@ const MainDrawerContent = () => {
|
|||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard.html'>
|
||||
<ListItemLink to='/dashboard'>
|
||||
<ListItemIcon>
|
||||
<Dashboard />
|
||||
</ListItemIcon>
|
||||
|
@ -158,7 +158,7 @@ const MainDrawerContent = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/edititemmetadata.html'>
|
||||
<ListItemLink to='/metadata'>
|
||||
<ListItemIcon>
|
||||
<Edit />
|
||||
</ListItemIcon>
|
||||
|
|
|
@ -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;
|
57
src/apps/experimental/components/library/PlayAllButton.tsx
Normal file
57
src/apps/experimental/components/library/PlayAllButton.tsx
Normal 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;
|
39
src/apps/experimental/components/library/QueueButton.tsx
Normal file
39
src/apps/experimental/components/library/QueueButton.tsx
Normal 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;
|
49
src/apps/experimental/components/library/ShuffleButton.tsx
Normal file
49
src/apps/experimental/components/library/ShuffleButton.tsx
Normal 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;
|
|
@ -98,7 +98,7 @@ const SortButton: FC<SortButtonProps> = ({
|
|||
title={globalize.translate('Sort')}
|
||||
sx={{ ml: 2 }}
|
||||
aria-describedby={id}
|
||||
className='paper-icon-button-light btnShuffle autoSize'
|
||||
className='paper-icon-button-light btnSort autoSize'
|
||||
onClick={handleClick}
|
||||
>
|
||||
<SortByAlphaIcon />
|
||||
|
|
|
@ -100,7 +100,7 @@ const ViewSettingsButton: FC<ViewSettingsButtonProps> = ({
|
|||
title={globalize.translate('ButtonSelectView')}
|
||||
sx={{ ml: 2 }}
|
||||
aria-describedby={id}
|
||||
className='paper-icon-button-light btnShuffle autoSize'
|
||||
className='paper-icon-button-light btnSelectView autoSize'
|
||||
onClick={handleClick}
|
||||
>
|
||||
<ViewComfyIcon />
|
||||
|
|
|
@ -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' }
|
||||
];
|
|
@ -1,2 +1 @@
|
|||
export * from './admin';
|
||||
export * from './user';
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
export * from './admin';
|
||||
export * from './public';
|
||||
export * from './user';
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import React from 'react';
|
||||
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
||||
import AppBody from 'components/AppBody';
|
||||
import ServerContentPage from 'components/ServerContentPage';
|
||||
import ConnectionRequired from 'components/ConnectionRequired';
|
||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||
|
||||
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
||||
import { REDIRECTS } from './routes/_redirects';
|
||||
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 = () => (
|
||||
<AppBody>
|
||||
<Outlet />
|
||||
|
@ -27,16 +27,6 @@ const StableApp = () => (
|
|||
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
|
||||
</Route>
|
||||
|
||||
{/* Admin routes */}
|
||||
<Route path='/' element={<ConnectionRequired isAdminRequired />}>
|
||||
{ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)}
|
||||
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
|
||||
|
||||
<Route path='configurationpage' element={
|
||||
<ServerContentPage view='/web/configurationpage' />
|
||||
} />
|
||||
</Route>
|
||||
|
||||
{/* Public routes */}
|
||||
<Route path='/' element={<ConnectionRequired isUserRequired={false} />}>
|
||||
<Route index element={<Navigate replace to='/home.html' />} />
|
||||
|
@ -50,6 +40,15 @@ const StableApp = () => (
|
|||
|
||||
{/* Redirects for old paths */}
|
||||
{REDIRECTS.map(toRedirectRoute)}
|
||||
|
||||
{/* Ignore dashboard routes */}
|
||||
{Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => (
|
||||
<Route
|
||||
key={key}
|
||||
path={`/${path}/*`}
|
||||
element={null}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import type { Redirect } from 'components/router/Redirect';
|
||||
|
||||
export const REDIRECTS: Redirect[] = [
|
||||
{ from: 'mypreferencesquickconnect.html', to: '/quickconnect' },
|
||||
{ from: 'serveractivity.html', to: '/dashboard/activity' }
|
||||
{ from: 'mypreferencesquickconnect.html', to: '/quickconnect' }
|
||||
];
|
||||
|
|
|
@ -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' }
|
||||
];
|
|
@ -1,2 +1 @@
|
|||
export * from './admin';
|
||||
export * from './user';
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
];
|
|
@ -1,3 +1,2 @@
|
|||
export * from './admin';
|
||||
export * from './public';
|
||||
export * from './user';
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import escapeHtml from 'escape-html';
|
||||
|
||||
import cardBuilderUtils from './cardBuilderUtils';
|
||||
import browser from 'scripts/browser';
|
||||
import datetime from 'scripts/datetime';
|
||||
import dom from 'scripts/dom';
|
||||
|
@ -32,11 +33,11 @@ import '../guide/programs.scss';
|
|||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
|
||||
/**
|
||||
* Generate the HTML markup for cards for a set of items.
|
||||
* @param items - The items used to generate cards.
|
||||
* @param options - The options of the cards.
|
||||
* @returns {string} The HTML markup for the cards.
|
||||
*/
|
||||
* Generate the HTML markup for cards for a set of items.
|
||||
* @param items - The items used to generate cards.
|
||||
* @param [options] - The options of the cards.
|
||||
* @returns {string} The HTML markup for the cards.
|
||||
*/
|
||||
export function getCardsHtml(items, options) {
|
||||
if (arguments.length === 1) {
|
||||
options = arguments[0];
|
||||
|
@ -47,221 +48,10 @@ export function getCardsHtml(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.
|
||||
* @param {number} windowWidth - Width of the device's screen.
|
||||
* @returns {boolean} - Result of the check.
|
||||
*/
|
||||
* Checks if the window is resizable.
|
||||
* @param {number} windowWidth - Width of the device's screen.
|
||||
* @returns {boolean} - Result of the check.
|
||||
*/
|
||||
function isResizable(windowWidth) {
|
||||
const screen = window.screen;
|
||||
if (screen) {
|
||||
|
@ -276,22 +66,22 @@ function isResizable(windowWidth) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the width of a card's image according to the shape and amount of cards per row.
|
||||
* @param {string} shape - Shape of the card.
|
||||
* @param {number} screenWidth - Width of the screen.
|
||||
* @param {boolean} isOrientationLandscape - Flag for the orientation of the screen.
|
||||
* @returns {number} Width of the image for a card.
|
||||
*/
|
||||
* Gets the width of a card's image according to the shape and amount of cards per row.
|
||||
* @param {string} shape - Shape of the card.
|
||||
* @param {number} screenWidth - Width of the screen.
|
||||
* @param {boolean} isOrientationLandscape - Flag for the orientation of the screen.
|
||||
* @returns {number} Width of the image for a card.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the options for a card.
|
||||
* @param {Object} items - A set of items.
|
||||
* @param {Object} options - Options for handling the items.
|
||||
*/
|
||||
* Normalizes the options for a card.
|
||||
* @param {Object} items - A set of items.
|
||||
* @param {Object} options - Options for handling the items.
|
||||
*/
|
||||
function setCardData(items, options) {
|
||||
options.shape = options.shape || 'auto';
|
||||
|
||||
|
@ -323,7 +113,7 @@ function setCardData(items, options) {
|
|||
options.preferThumb = options.shape === 'backdrop' || options.shape === 'overflowBackdrop';
|
||||
}
|
||||
|
||||
options.uiAspect = getDesiredAspect(options.shape);
|
||||
options.uiAspect = cardBuilderUtils.getDesiredAspect(options.shape);
|
||||
options.primaryImageAspectRatio = primaryImageAspectRatio;
|
||||
|
||||
if (!options.width && options.widths) {
|
||||
|
@ -348,11 +138,11 @@ function setCardData(items, options) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Generates the internal HTML markup for cards.
|
||||
* @param {Object} items - Items for which to generate the markup.
|
||||
* @param {Object} options - Options for generating the markup.
|
||||
* @returns {string} The internal HTML markup of the cards.
|
||||
*/
|
||||
* Generates the internal HTML markup for cards.
|
||||
* @param {Object} items - Items for which to generate the markup.
|
||||
* @param {Object} options - Options for generating the markup.
|
||||
* @returns {string} The internal HTML markup of the cards.
|
||||
*/
|
||||
function buildCardsHtmlInternal(items, options) {
|
||||
let isVertical = false;
|
||||
|
||||
|
@ -466,44 +256,20 @@ function buildCardsHtmlInternal(items, options) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @property {string} imgUrl - Image URL.
|
||||
* @property {string} blurhash - Image blurhash.
|
||||
* @property {boolean} forceName - Force name.
|
||||
* @property {boolean} coverImage - Use cover style.
|
||||
*/
|
||||
* @typedef {Object} CardImageUrl
|
||||
* @property {string} imgUrl - Image URL.
|
||||
* @property {string} blurhash - Image blurhash.
|
||||
* @property {boolean} forceName - Force name.
|
||||
* @property {boolean} coverImage - Use cover style.
|
||||
*/
|
||||
|
||||
/** Get the URL of the card's image.
|
||||
* @param {Object} item - Item for which to generate a card.
|
||||
* @param {Object} apiClient - API client object.
|
||||
* @param {Object} options - Options of the card.
|
||||
* @param {string} shape - Shape of the desired image.
|
||||
* @returns {CardImageUrl} Object representing the URL of the card's image.
|
||||
*/
|
||||
* @param {Object} item - Item for which to generate a card.
|
||||
* @param {Object} apiClient - API client object.
|
||||
* @param {Object} options - Options of the card.
|
||||
* @param {string} shape - Shape of the desired image.
|
||||
* @returns {CardImageUrl} Object representing the URL of the card's image.
|
||||
*/
|
||||
function getCardImageUrl(item, apiClient, options, shape) {
|
||||
item = item.ProgramInfo || item;
|
||||
|
||||
|
@ -514,7 +280,7 @@ function getCardImageUrl(item, apiClient, options, shape) {
|
|||
let imgUrl = null;
|
||||
let imgTag = null;
|
||||
let coverImage = false;
|
||||
const uiAspect = getDesiredAspect(shape);
|
||||
const uiAspect = cardBuilderUtils.getDesiredAspect(shape);
|
||||
let imgType = null;
|
||||
let itemId = null;
|
||||
|
||||
|
@ -646,10 +412,10 @@ function getCardImageUrl(item, apiClient, options, shape) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Generates an index used to select the default color of a card based on a string.
|
||||
* @param {?string} [str] - String to use for generating the index.
|
||||
* @returns {number} Index of the color.
|
||||
*/
|
||||
* Generates an index used to select the default color of a card based on a string.
|
||||
* @param {?string} [str] - String to use for generating the index.
|
||||
* @returns {number} Index of the color.
|
||||
*/
|
||||
function getDefaultColorIndex(str) {
|
||||
const numRandomColors = 5;
|
||||
|
||||
|
@ -669,16 +435,16 @@ function getDefaultColorIndex(str) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Generates the HTML markup for a card's text.
|
||||
* @param {Array} lines - Array containing the text lines.
|
||||
* @param {string} cssClass - Base CSS class to use for the lines.
|
||||
* @param {boolean} forceLines - Flag to force the rendering of all lines.
|
||||
* @param {boolean} isOuterFooter - Flag to mark the text lines as outer footer.
|
||||
* @param {string} cardLayout - DEPRECATED
|
||||
* @param {boolean} addRightMargin - Flag to add a right margin to the text.
|
||||
* @param {number} maxLines - Maximum number of lines to render.
|
||||
* @returns {string} HTML markup for the card's text.
|
||||
*/
|
||||
* Generates the HTML markup for a card's text.
|
||||
* @param {Array} lines - Array containing the text lines.
|
||||
* @param {string} cssClass - Base CSS class to use for the lines.
|
||||
* @param {boolean} forceLines - Flag to force the rendering of all lines.
|
||||
* @param {boolean} isOuterFooter - Flag to mark the text lines as outer footer.
|
||||
* @param {string} cardLayout - DEPRECATED
|
||||
* @param {boolean} addRightMargin - Flag to add a right margin to the text.
|
||||
* @param {number} maxLines - Maximum number of lines to render.
|
||||
* @returns {string} HTML markup for the card's text.
|
||||
*/
|
||||
function getCardTextLines(lines, cssClass, forceLines, isOuterFooter, cardLayout, addRightMargin, maxLines) {
|
||||
let html = '';
|
||||
|
||||
|
@ -722,21 +488,21 @@ function getCardTextLines(lines, cssClass, forceLines, isOuterFooter, cardLayout
|
|||
}
|
||||
|
||||
/**
|
||||
* Determines if the item is live TV.
|
||||
* @param {Object} item - Item to use for the check.
|
||||
* @returns {boolean} Flag showing if the item is live TV.
|
||||
*/
|
||||
* Determines if the item is live TV.
|
||||
* @param {Object} item - Item to use for the check.
|
||||
* @returns {boolean} Flag showing if the item is live TV.
|
||||
*/
|
||||
function isUsingLiveTvNaming(item) {
|
||||
return item.Type === 'Program' || item.Type === 'Timer' || item.Type === 'Recording';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the air time text for the item based on the given times.
|
||||
* @param {object} item - Item used to generate the air time text.
|
||||
* @param {boolean} showAirDateTime - ISO8601 date for the start of the show.
|
||||
* @param {boolean} showAirEndTime - ISO8601 date for the end of the show.
|
||||
* @returns {string} The air time text for the item based on the given dates.
|
||||
*/
|
||||
* Returns the air time text for the item based on the given times.
|
||||
* @param {object} item - Item used to generate the air time text.
|
||||
* @param {boolean} showAirDateTime - ISO8601 date for the start of the show.
|
||||
* @param {boolean} showAirEndTime - ISO8601 date for the end of the show.
|
||||
* @returns {string} The air time text for the item based on the given dates.
|
||||
*/
|
||||
function getAirTimeText(item, showAirDateTime, showAirEndTime) {
|
||||
let airTimeText = '';
|
||||
|
||||
|
@ -763,16 +529,16 @@ function getAirTimeText(item, showAirDateTime, showAirEndTime) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Generates the HTML markup for the card's footer text.
|
||||
* @param {Object} item - Item used to generate the footer text.
|
||||
* @param {Object} apiClient - API client instance.
|
||||
* @param {Object} options - Options used to generate the footer text.
|
||||
* @param {string} footerClass - CSS classes of the footer element.
|
||||
* @param {string} progressHtml - HTML markup of the progress bar element.
|
||||
* @param {Object} flags - Various flags for the footer
|
||||
* @param {Object} urls - Various urls for the footer
|
||||
* @returns {string} HTML markup of the card's footer text element.
|
||||
*/
|
||||
* Generates the HTML markup for the card's footer text.
|
||||
* @param {Object} item - Item used to generate the footer text.
|
||||
* @param {Object} apiClient - API client instance.
|
||||
* @param {Object} options - Options used to generate the footer text.
|
||||
* @param {string} footerClass - CSS classes of the footer element.
|
||||
* @param {string} progressHtml - HTML markup of the progress bar element.
|
||||
* @param {Object} flags - Various flags for the footer
|
||||
* @param {Object} urls - Various urls for the footer
|
||||
* @returns {string} HTML markup of the card's footer text element.
|
||||
*/
|
||||
function getCardFooterText(item, apiClient, options, footerClass, progressHtml, flags, urls) {
|
||||
item = item.ProgramInfo || item;
|
||||
let html = '';
|
||||
|
@ -1005,12 +771,12 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml,
|
|||
}
|
||||
|
||||
/**
|
||||
* Generates the HTML markup for the action button.
|
||||
* @param {Object} item - Item used to generate the action button.
|
||||
* @param {string} text - Text of the action button.
|
||||
* @param {string} serverId - ID of the server.
|
||||
* @returns {string} HTML markup of the action button.
|
||||
*/
|
||||
* Generates the HTML markup for the action button.
|
||||
* @param {Object} item - Item used to generate the action button.
|
||||
* @param {string} text - Text of the action button.
|
||||
* @param {string} serverId - ID of the server.
|
||||
* @returns {string} HTML markup of the action button.
|
||||
*/
|
||||
function getTextActionButton(item, text, serverId) {
|
||||
if (!text) {
|
||||
text = itemHelper.getDisplayName(item);
|
||||
|
@ -1031,11 +797,11 @@ function getTextActionButton(item, text, serverId) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Generates HTML markup for the item count indicator.
|
||||
* @param {Object} options - Options used to generate the item count.
|
||||
* @param {Object} item - Item used to generate the item count.
|
||||
* @returns {string} HTML markup for the item count indicator.
|
||||
*/
|
||||
* Generates HTML markup for the item count indicator.
|
||||
* @param {Object} options - Options used to generate the item count.
|
||||
* @param {Object} item - Item used to generate the item count.
|
||||
* @returns {string} HTML markup for the item count indicator.
|
||||
*/
|
||||
function getItemCountsHtml(options, item) {
|
||||
const counts = [];
|
||||
let childText;
|
||||
|
@ -1113,8 +879,8 @@ function getItemCountsHtml(options, item) {
|
|||
let refreshIndicatorLoaded;
|
||||
|
||||
/**
|
||||
* Imports the refresh indicator element.
|
||||
*/
|
||||
* Imports the refresh indicator element.
|
||||
*/
|
||||
function importRefreshIndicator() {
|
||||
if (!refreshIndicatorLoaded) {
|
||||
refreshIndicatorLoaded = true;
|
||||
|
@ -1123,22 +889,22 @@ function importRefreshIndicator() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the default background class for a card based on a string.
|
||||
* @param {?string} [str] - Text used to generate the background class.
|
||||
* @returns {string} CSS classes for default card backgrounds.
|
||||
*/
|
||||
* Returns the default background class for a card based on a string.
|
||||
* @param {?string} [str] - Text used to generate the background class.
|
||||
* @returns {string} CSS classes for default card backgrounds.
|
||||
*/
|
||||
export function getDefaultBackgroundClass(str) {
|
||||
return 'defaultCardBackground defaultCardBackground' + getDefaultColorIndex(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the HTML markup for an individual card.
|
||||
* @param {number} index - Index of the card
|
||||
* @param {object} item - Item used to generate the card.
|
||||
* @param {object} apiClient - API client instance.
|
||||
* @param {object} options - Options used to generate the card.
|
||||
* @returns {string} HTML markup for the generated card.
|
||||
*/
|
||||
* Builds the HTML markup for an individual card.
|
||||
* @param {number} index - Index of the card
|
||||
* @param {object} item - Item used to generate the card.
|
||||
* @param {object} apiClient - API client instance.
|
||||
* @param {object} options - Options used to generate the card.
|
||||
* @returns {string} HTML markup for the generated card.
|
||||
*/
|
||||
function buildCard(index, item, apiClient, options) {
|
||||
let action = options.action || 'link';
|
||||
|
||||
|
@ -1445,11 +1211,11 @@ function buildCard(index, item, apiClient, options) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Generates HTML markup for the card overlay.
|
||||
* @param {object} item - Item used to generate the card overlay.
|
||||
* @param {string} action - Action assigned to the overlay.
|
||||
* @returns {string} HTML markup of the card overlay.
|
||||
*/
|
||||
* Generates HTML markup for the card overlay.
|
||||
* @param {object} item - Item used to generate the card overlay.
|
||||
* @param {string} action - Action assigned to the overlay.
|
||||
* @returns {string} HTML markup of the card overlay.
|
||||
*/
|
||||
function getHoverMenuHtml(item, action) {
|
||||
let html = '';
|
||||
|
||||
|
@ -1487,11 +1253,11 @@ function getHoverMenuHtml(item, action) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Generates the text or icon used for default card backgrounds.
|
||||
* @param {object} item - Item used to generate the card overlay.
|
||||
* @param {object} options - Options used to generate the card overlay.
|
||||
* @returns {string} HTML markup of the card overlay.
|
||||
*/
|
||||
* Generates the text or icon used for default card backgrounds.
|
||||
* @param {object} item - Item used to generate the card overlay.
|
||||
* @param {object} options - Options used to generate the card overlay.
|
||||
* @returns {string} HTML markup of the card overlay.
|
||||
*/
|
||||
export function getDefaultText(item, options) {
|
||||
if (item.CollectionType) {
|
||||
return '<span class="cardImageIcon material-icons ' + imageHelper.getLibraryIcon(item.CollectionType) + '" aria-hidden="true"></span>';
|
||||
|
@ -1535,10 +1301,10 @@ export function getDefaultText(item, options) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Builds a set of cards and inserts them into the page.
|
||||
* @param {Array} items - Array of items used to build the cards.
|
||||
* @param {options} options - Options of the cards to build.
|
||||
*/
|
||||
* Builds a set of cards and inserts them into the page.
|
||||
* @param {Array} items - Array of items used to build the cards.
|
||||
* @param {options} options - Options of the cards to build.
|
||||
*/
|
||||
export function buildCards(items, options) {
|
||||
// Abort if the container has been disposed
|
||||
if (!document.body.contains(options.itemsContainer)) {
|
||||
|
@ -1579,11 +1345,11 @@ export function buildCards(items, options) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Ensures the indicators for a card exist and creates them if they don't exist.
|
||||
* @param {HTMLDivElement} card - DOM element of the card.
|
||||
* @param {HTMLDivElement} indicatorsElem - DOM element of the indicators.
|
||||
* @returns {HTMLDivElement} - DOM element of the indicators.
|
||||
*/
|
||||
* Ensures the indicators for a card exist and creates them if they don't exist.
|
||||
* @param {HTMLDivElement} card - DOM element of the card.
|
||||
* @param {HTMLDivElement} indicatorsElem - DOM element of the indicators.
|
||||
* @returns {HTMLDivElement} - DOM element of the indicators.
|
||||
*/
|
||||
function ensureIndicators(card, indicatorsElem) {
|
||||
if (indicatorsElem) {
|
||||
return indicatorsElem;
|
||||
|
@ -1602,10 +1368,10 @@ function ensureIndicators(card, indicatorsElem) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Adds user data to the card such as progress indicators and played status.
|
||||
* @param {HTMLDivElement} card - DOM element of the card.
|
||||
* @param {Object} userData - User data to apply to the card.
|
||||
*/
|
||||
* Adds user data to the card such as progress indicators and played status.
|
||||
* @param {HTMLDivElement} card - DOM element of the card.
|
||||
* @param {Object} userData - User data to apply to the card.
|
||||
*/
|
||||
function updateUserData(card, userData) {
|
||||
const type = card.getAttribute('data-type');
|
||||
const enableCountIndicator = type === 'Series' || type === 'BoxSet' || type === 'Season';
|
||||
|
@ -1681,10 +1447,10 @@ function updateUserData(card, userData) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Handles when user data has changed.
|
||||
* @param {Object} userData - User data to apply to the card.
|
||||
* @param {HTMLElement} scope - DOM element to use as a scope when selecting cards.
|
||||
*/
|
||||
* Handles when user data has changed.
|
||||
* @param {Object} userData - User data to apply to the card.
|
||||
* @param {HTMLElement} scope - DOM element to use as a scope when selecting cards.
|
||||
*/
|
||||
export function onUserDataChanged(userData, scope) {
|
||||
const cards = (scope || document.body).querySelectorAll('.card-withuserdata[data-id="' + userData.ItemId + '"]');
|
||||
|
||||
|
@ -1694,11 +1460,11 @@ export function onUserDataChanged(userData, scope) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Handles when a timer has been created.
|
||||
* @param {string} programId - ID of the program.
|
||||
* @param {string} newTimerId - ID of the new timer.
|
||||
* @param {HTMLElement} itemsContainer - DOM element of the itemsContainer.
|
||||
*/
|
||||
* Handles when a timer has been created.
|
||||
* @param {string} programId - ID of the program.
|
||||
* @param {string} newTimerId - ID of the new timer.
|
||||
* @param {HTMLElement} itemsContainer - DOM element of the itemsContainer.
|
||||
*/
|
||||
export function onTimerCreated(programId, newTimerId, itemsContainer) {
|
||||
const cells = itemsContainer.querySelectorAll('.card[data-id="' + programId + '"]');
|
||||
|
||||
|
@ -1714,10 +1480,10 @@ export function onTimerCreated(programId, newTimerId, itemsContainer) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Handles when a timer has been cancelled.
|
||||
* @param {string} timerId - ID of the cancelled timer.
|
||||
* @param {HTMLElement} itemsContainer - DOM element of the itemsContainer.
|
||||
*/
|
||||
* Handles when a timer has been cancelled.
|
||||
* @param {string} timerId - ID of the cancelled timer.
|
||||
* @param {HTMLElement} itemsContainer - DOM element of the itemsContainer.
|
||||
*/
|
||||
export function onTimerCancelled(timerId, itemsContainer) {
|
||||
const cells = itemsContainer.querySelectorAll('.card[data-timerid="' + timerId + '"]');
|
||||
|
||||
|
@ -1731,10 +1497,10 @@ export function onTimerCancelled(timerId, itemsContainer) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Handles when a series timer has been cancelled.
|
||||
* @param {string} cancelledTimerId - ID of the cancelled timer.
|
||||
* @param {HTMLElement} itemsContainer - DOM element of the itemsContainer.
|
||||
*/
|
||||
* Handles when a series timer has been cancelled.
|
||||
* @param {string} cancelledTimerId - ID of the cancelled timer.
|
||||
* @param {HTMLElement} itemsContainer - DOM element of the itemsContainer.
|
||||
*/
|
||||
export function onSeriesTimerCancelled(cancelledTimerId, itemsContainer) {
|
||||
const cells = itemsContainer.querySelectorAll('.card[data-seriestimerid="' + cancelledTimerId + '"]');
|
||||
|
||||
|
|
173
src/components/cardbuilder/cardBuilderUtils.js
Normal file
173
src/components/cardbuilder/cardBuilderUtils.js
Normal 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
|
||||
};
|
417
src/components/cardbuilder/cardBuilderUtils.test.js
Normal file
417
src/components/cardbuilder/cardBuilderUtils.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -10,28 +10,28 @@ const createLinkElement = (activeTab: string) => ({
|
|||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'useredit' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('useredit.html', true);">
|
||||
onclick="Dashboard.navigate('/dashboard/users/profile', true);">
|
||||
${globalize.translate('Profile')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userlibraryaccess' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('userlibraryaccess.html', true);">
|
||||
onclick="Dashboard.navigate('/dashboard/users/access', true);">
|
||||
${globalize.translate('TabAccess')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userparentalcontrol' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('userparentalcontrol.html', true);">
|
||||
onclick="Dashboard.navigate('/dashboard/users/parentalcontrol', true);">
|
||||
${globalize.translate('TabParentalControl')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userpassword' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('userpassword.html', true);">
|
||||
onclick="Dashboard.navigate('/dashboard/users/password', true);">
|
||||
${globalize.translate('HeaderPassword')}
|
||||
</a>`
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl
|
|||
__html: `<a
|
||||
is="emby-linkbutton"
|
||||
class="cardContent"
|
||||
href="#/useredit.html?userId=${user.Id}"
|
||||
href="#/dashboard/users/profile?userId=${user.Id}"
|
||||
>
|
||||
${renderImgUrl}
|
||||
</a>`
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import escapeHtml from 'escape-html';
|
||||
|
||||
import globalize from 'scripts/globalize';
|
||||
import imageHelper from 'scripts/imagehelper';
|
||||
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
|
||||
import { DEFAULT_SECTIONS, HomeSectionType } from 'types/homeSectionType';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
|
||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||
import imageLoader from '../images/imageLoader';
|
||||
import layoutManager from '../layoutManager';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import { loadRecordings } from './sections/activeRecordings';
|
||||
import { loadLibraryButtons } from './sections/libraryButtons';
|
||||
import { loadLibraryTiles } from './sections/libraryTiles';
|
||||
import { loadLiveTV } from './sections/liveTv';
|
||||
import { loadNextUp } from './sections/nextUp';
|
||||
import { loadRecentlyAdded } from './sections/recentlyAdded';
|
||||
import { loadResume } from './sections/resume';
|
||||
|
||||
import 'elements/emby-button/paper-icon-button-light';
|
||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
@ -19,26 +18,8 @@ import 'elements/emby-button/emby-button';
|
|||
import './homesections.scss';
|
||||
|
||||
export function getDefaultSection(index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return 'smalllibrarytiles';
|
||||
case 1:
|
||||
return 'resume';
|
||||
case 2:
|
||||
return 'resumeaudio';
|
||||
case 3:
|
||||
return 'resumebook';
|
||||
case 4:
|
||||
return 'livetv';
|
||||
case 5:
|
||||
return 'nextup';
|
||||
case 6:
|
||||
return 'latestmedia';
|
||||
case 7:
|
||||
return 'none';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
if (index < 0 || index > DEFAULT_SECTIONS.length) return '';
|
||||
return DEFAULT_SECTIONS[index];
|
||||
}
|
||||
|
||||
function getAllSectionsToShow(userSettings, sectionCount) {
|
||||
|
@ -99,7 +80,7 @@ export function loadSections(elem, apiClient, user, userSettings) {
|
|||
const createNowLink = elem.querySelector('#button-createLibrary');
|
||||
if (createNowLink) {
|
||||
createNowLink.addEventListener('click', function () {
|
||||
Dashboard.navigate('library.html');
|
||||
Dashboard.navigate('dashboard/libraries');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -138,29 +119,36 @@ export function resume(elem, options) {
|
|||
function loadSection(page, apiClient, user, userSettings, userViews, allSections, index) {
|
||||
const section = allSections[index];
|
||||
const elem = page.querySelector('.section' + index);
|
||||
const options = { enableOverflow: enableScrollX() };
|
||||
|
||||
if (section === 'latestmedia') {
|
||||
loadRecentlyAdded(elem, apiClient, user, userViews);
|
||||
} else if (section === 'librarytiles' || section === 'smalllibrarytiles' || section === 'smalllibrarytiles-automobile' || section === 'librarytiles-automobile') {
|
||||
loadLibraryTiles(elem, apiClient, user, userSettings, 'smallBackdrop', userViews);
|
||||
} else if (section === 'librarybuttons') {
|
||||
loadlibraryButtons(elem, apiClient, user, userSettings, userViews);
|
||||
} else if (section === 'resume') {
|
||||
return loadResume(elem, apiClient, 'HeaderContinueWatching', 'Video', userSettings);
|
||||
} else if (section === 'resumeaudio') {
|
||||
return loadResume(elem, apiClient, 'HeaderContinueListening', 'Audio', userSettings);
|
||||
} else if (section === 'activerecordings') {
|
||||
loadLatestLiveTvRecordings(elem, true, apiClient);
|
||||
} else if (section === 'nextup') {
|
||||
loadNextUp(elem, apiClient, userSettings);
|
||||
} else if (section === 'onnow' || section === 'livetv') {
|
||||
return loadOnNow(elem, apiClient, user);
|
||||
} else if (section === 'resumebook') {
|
||||
return loadResume(elem, apiClient, 'HeaderContinueReading', 'Book', userSettings);
|
||||
} else {
|
||||
elem.innerHTML = '';
|
||||
return Promise.resolve();
|
||||
switch (section) {
|
||||
case HomeSectionType.ActiveRecordings:
|
||||
loadRecordings(elem, true, apiClient, options);
|
||||
break;
|
||||
case HomeSectionType.LatestMedia:
|
||||
loadRecentlyAdded(elem, apiClient, user, userViews, options);
|
||||
break;
|
||||
case HomeSectionType.LibraryButtons:
|
||||
loadLibraryButtons(elem, userViews);
|
||||
break;
|
||||
case HomeSectionType.LiveTv:
|
||||
return loadLiveTV(elem, apiClient, user, options);
|
||||
case HomeSectionType.NextUp:
|
||||
loadNextUp(elem, apiClient, userSettings, options);
|
||||
break;
|
||||
case HomeSectionType.Resume:
|
||||
return loadResume(elem, apiClient, 'HeaderContinueWatching', 'Video', userSettings, options);
|
||||
case HomeSectionType.ResumeAudio:
|
||||
return loadResume(elem, apiClient, 'HeaderContinueListening', 'Audio', userSettings, options);
|
||||
case HomeSectionType.ResumeBook:
|
||||
return loadResume(elem, apiClient, 'HeaderContinueReading', 'Book', userSettings, options);
|
||||
case HomeSectionType.SmallLibraryTiles:
|
||||
loadLibraryTiles(elem, userViews, options);
|
||||
break;
|
||||
default:
|
||||
elem.innerHTML = '';
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
@ -174,573 +162,11 @@ function enableScrollX() {
|
|||
return true;
|
||||
}
|
||||
|
||||
function getLibraryButtonsHtml(items) {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="verticalSection verticalSection-extrabottompadding">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + globalize.translate('HeaderMyMedia') + '</h2>';
|
||||
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x" data-multiselect="false">';
|
||||
|
||||
// library card background images
|
||||
for (let i = 0, length = items.length; i < length; i++) {
|
||||
const item = items[i];
|
||||
const icon = imageHelper.getLibraryIcon(item.CollectionType);
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(item) + '" class="raised homeLibraryButton"><span class="material-icons homeLibraryIcon ' + icon + '" aria-hidden="true"></span><span class="homeLibraryText">' + escapeHtml(item.Name) + '</span></a>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function loadlibraryButtons(elem, apiClient, user, userSettings, userViews) {
|
||||
elem.classList.remove('verticalSection');
|
||||
const html = getLibraryButtonsHtml(userViews);
|
||||
|
||||
elem.innerHTML = html;
|
||||
imageLoader.lazyChildren(elem);
|
||||
}
|
||||
|
||||
function getFetchLatestItemsFn(serverId, parentId, collectionType) {
|
||||
return function () {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
let limit = 16;
|
||||
|
||||
if (enableScrollX()) {
|
||||
if (collectionType === 'music') {
|
||||
limit = 30;
|
||||
}
|
||||
} else if (collectionType === 'tvshows') {
|
||||
limit = 5;
|
||||
} else if (collectionType === 'music') {
|
||||
limit = 9;
|
||||
} else {
|
||||
limit = 8;
|
||||
}
|
||||
|
||||
const options = {
|
||||
Limit: limit,
|
||||
Fields: 'PrimaryImageAspectRatio,BasicSyncInfo,Path',
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Backdrop,Thumb',
|
||||
ParentId: parentId
|
||||
};
|
||||
|
||||
return apiClient.getLatestItems(options);
|
||||
};
|
||||
}
|
||||
|
||||
function getLatestItemsHtmlFn(itemType, viewType) {
|
||||
return function (items) {
|
||||
const cardLayout = false;
|
||||
let shape;
|
||||
if (itemType === 'Channel' || viewType === 'movies' || viewType === 'books' || viewType === 'tvshows') {
|
||||
shape = getPortraitShape(enableScrollX());
|
||||
} else if (viewType === 'music' || viewType === 'homevideos') {
|
||||
shape = getSquareShape(enableScrollX());
|
||||
} else {
|
||||
shape = getBackdropShape(enableScrollX());
|
||||
}
|
||||
|
||||
return cardBuilder.getCardsHtml({
|
||||
items: items,
|
||||
shape: shape,
|
||||
preferThumb: viewType !== 'movies' && viewType !== 'tvshows' && itemType !== 'Channel' && viewType !== 'music' ? 'auto' : null,
|
||||
showUnplayedIndicator: false,
|
||||
showChildCountIndicator: true,
|
||||
context: 'home',
|
||||
overlayText: false,
|
||||
centerText: !cardLayout,
|
||||
overlayPlayButton: viewType !== 'photos',
|
||||
allowBottomPadding: !enableScrollX() && !cardLayout,
|
||||
cardLayout: cardLayout,
|
||||
showTitle: viewType !== 'photos',
|
||||
showYear: viewType === 'movies' || viewType === 'tvshows' || !viewType,
|
||||
showParentTitle: viewType === 'music' || viewType === 'tvshows' || !viewType || (cardLayout && (viewType === 'tvshows')),
|
||||
lines: 2
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function renderLatestSection(elem, apiClient, user, parent) {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||
if (!layoutManager.tv) {
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(parent, {
|
||||
section: 'latest'
|
||||
}) + '" class="more button-flat button-flat-mini sectionTitleTextButton">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||
html += globalize.translate('LatestFromLibrary', escapeHtml(parent.Name));
|
||||
html += '</h2>';
|
||||
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
} else {
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('LatestFromLibrary', escapeHtml(parent.Name)) + '</h2>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (enableScrollX()) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer focuscontainer-x padded-left padded-right vertical-wrap">';
|
||||
}
|
||||
|
||||
if (enableScrollX()) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
elem.innerHTML = html;
|
||||
|
||||
const itemsContainer = elem.querySelector('.itemsContainer');
|
||||
itemsContainer.fetchData = getFetchLatestItemsFn(apiClient.serverId(), parent.Id, parent.CollectionType);
|
||||
itemsContainer.getItemsHtml = getLatestItemsHtmlFn(parent.Type, parent.CollectionType);
|
||||
itemsContainer.parentContainer = elem;
|
||||
}
|
||||
|
||||
function loadRecentlyAdded(elem, apiClient, user, userViews) {
|
||||
elem.classList.remove('verticalSection');
|
||||
const excludeViewTypes = ['playlists', 'livetv', 'boxsets', 'channels'];
|
||||
|
||||
for (let i = 0, length = userViews.length; i < length; i++) {
|
||||
const item = userViews[i];
|
||||
if (user.Configuration.LatestItemsExcludes.indexOf(item.Id) !== -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (excludeViewTypes.indexOf(item.CollectionType || []) !== -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const frag = document.createElement('div');
|
||||
frag.classList.add('verticalSection');
|
||||
frag.classList.add('hide');
|
||||
elem.appendChild(frag);
|
||||
|
||||
renderLatestSection(frag, apiClient, user, item);
|
||||
}
|
||||
}
|
||||
|
||||
export function loadLibraryTiles(elem, apiClient, user, userSettings, shape, userViews) {
|
||||
let html = '';
|
||||
if (userViews.length) {
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + globalize.translate('HeaderMyMedia') + '</h2>';
|
||||
if (enableScrollX()) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right focuscontainer-x vertical-wrap">';
|
||||
}
|
||||
|
||||
html += cardBuilder.getCardsHtml({
|
||||
items: userViews,
|
||||
shape: getBackdropShape(enableScrollX()),
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
overlayText: false,
|
||||
lazy: true,
|
||||
transition: false,
|
||||
allowBottomPadding: !enableScrollX()
|
||||
});
|
||||
|
||||
if (enableScrollX()) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
elem.innerHTML = html;
|
||||
imageLoader.lazyChildren(elem);
|
||||
}
|
||||
|
||||
const dataMonitorHints = {
|
||||
'Audio': 'audioplayback,markplayed',
|
||||
'Video': 'videoplayback,markplayed'
|
||||
};
|
||||
|
||||
function loadResume(elem, apiClient, headerText, mediaType, userSettings) {
|
||||
let html = '';
|
||||
|
||||
const dataMonitor = dataMonitorHints[mediaType] || 'markplayed';
|
||||
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + globalize.translate(headerText) + '</h2>';
|
||||
if (enableScrollX()) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += `<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x" data-monitor="${dataMonitor}">`;
|
||||
} else {
|
||||
html += `<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x" data-monitor="${dataMonitor}">`;
|
||||
}
|
||||
|
||||
if (enableScrollX()) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
elem.classList.add('hide');
|
||||
elem.innerHTML = html;
|
||||
|
||||
const itemsContainer = elem.querySelector('.itemsContainer');
|
||||
itemsContainer.fetchData = getItemsToResumeFn(mediaType, apiClient.serverId());
|
||||
itemsContainer.getItemsHtml = getItemsToResumeHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), mediaType);
|
||||
itemsContainer.parentContainer = elem;
|
||||
}
|
||||
|
||||
function getItemsToResumeFn(mediaType, serverId) {
|
||||
return function () {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
const limit = enableScrollX() ? 12 : 5;
|
||||
|
||||
const options = {
|
||||
Limit: limit,
|
||||
Recursive: true,
|
||||
Fields: 'PrimaryImageAspectRatio,BasicSyncInfo',
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Backdrop,Thumb',
|
||||
EnableTotalRecordCount: false,
|
||||
MediaTypes: mediaType
|
||||
};
|
||||
|
||||
return apiClient.getResumableItems(apiClient.getCurrentUserId(), options);
|
||||
};
|
||||
}
|
||||
|
||||
function getItemsToResumeHtmlFn(useEpisodeImages, mediaType) {
|
||||
return function (items) {
|
||||
const cardLayout = false;
|
||||
return cardBuilder.getCardsHtml({
|
||||
items: items,
|
||||
preferThumb: true,
|
||||
inheritThumb: !useEpisodeImages,
|
||||
shape: (mediaType === 'Book') ?
|
||||
getPortraitShape(enableScrollX()) :
|
||||
getBackdropShape(enableScrollX()),
|
||||
overlayText: false,
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
lazy: true,
|
||||
showDetailsMenu: true,
|
||||
overlayPlayButton: true,
|
||||
context: 'home',
|
||||
centerText: !cardLayout,
|
||||
allowBottomPadding: false,
|
||||
cardLayout: cardLayout,
|
||||
showYear: true,
|
||||
lines: 2
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getOnNowFetchFn(serverId) {
|
||||
return function () {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
return apiClient.getLiveTvRecommendedPrograms({
|
||||
userId: apiClient.getCurrentUserId(),
|
||||
IsAiring: true,
|
||||
limit: 24,
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Thumb,Backdrop',
|
||||
EnableTotalRecordCount: false,
|
||||
Fields: 'ChannelInfo,PrimaryImageAspectRatio'
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getOnNowItemsHtml(items) {
|
||||
return cardBuilder.getCardsHtml({
|
||||
items: items,
|
||||
preferThumb: 'auto',
|
||||
inheritThumb: false,
|
||||
shape: (enableScrollX() ? 'autooverflow' : 'auto'),
|
||||
showParentTitleOrTitle: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
coverImage: true,
|
||||
overlayText: false,
|
||||
allowBottomPadding: !enableScrollX(),
|
||||
showAirTime: true,
|
||||
showChannelName: false,
|
||||
showAirDateTime: false,
|
||||
showAirEndTime: true,
|
||||
defaultShape: getBackdropShape(enableScrollX()),
|
||||
lines: 3,
|
||||
overlayPlayButton: true
|
||||
});
|
||||
}
|
||||
|
||||
function loadOnNow(elem, apiClient, user) {
|
||||
if (!user.Policy.EnableLiveTvAccess) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return apiClient.getLiveTvRecommendedPrograms({
|
||||
userId: apiClient.getCurrentUserId(),
|
||||
IsAiring: true,
|
||||
limit: 1,
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Thumb,Backdrop',
|
||||
EnableTotalRecordCount: false,
|
||||
Fields: 'ChannelInfo,PrimaryImageAspectRatio'
|
||||
}).then(function (result) {
|
||||
let html = '';
|
||||
if (result.Items.length) {
|
||||
elem.classList.remove('padded-left');
|
||||
elem.classList.remove('padded-right');
|
||||
elem.classList.remove('padded-bottom');
|
||||
elem.classList.remove('verticalSection');
|
||||
|
||||
html += '<div class="verticalSection">';
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('LiveTV') + '</h2>';
|
||||
html += '</div>';
|
||||
|
||||
if (enableScrollX()) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true" data-scrollbuttons="false">';
|
||||
html += '<div class="padded-top padded-bottom scrollSlider focuscontainer-x">';
|
||||
} else {
|
||||
html += '<div class="padded-top padded-bottom focuscontainer-x">';
|
||||
}
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId: apiClient.serverId(),
|
||||
section: 'programs'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Programs') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId: apiClient.serverId(),
|
||||
section: 'guide'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Guide') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId: apiClient.serverId(),
|
||||
section: 'channels'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Channels') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('recordedtv', {
|
||||
serverId: apiClient.serverId()
|
||||
}) + '" class="raised"><span>' + globalize.translate('Recordings') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId: apiClient.serverId(),
|
||||
section: 'dvrschedule'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Schedule') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId: apiClient.serverId(),
|
||||
section: 'seriesrecording'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Series') + '</span></a>';
|
||||
|
||||
html += '</div>';
|
||||
if (enableScrollX()) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="verticalSection">';
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||
|
||||
if (!layoutManager.tv) {
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId: apiClient.serverId(),
|
||||
section: 'onnow'
|
||||
}) + '" class="more button-flat button-flat-mini sectionTitleTextButton">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||
html += globalize.translate('HeaderOnNow');
|
||||
html += '</h2>';
|
||||
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
} else {
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('HeaderOnNow') + '</h2>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (enableScrollX()) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x">';
|
||||
}
|
||||
|
||||
if (enableScrollX()) {
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
elem.innerHTML = html;
|
||||
|
||||
const itemsContainer = elem.querySelector('.itemsContainer');
|
||||
itemsContainer.parentContainer = elem;
|
||||
itemsContainer.fetchData = getOnNowFetchFn(apiClient.serverId());
|
||||
itemsContainer.getItemsHtml = getOnNowItemsHtml;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getNextUpFetchFn(serverId, userSettings) {
|
||||
return function () {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
const oldestDateForNextUp = new Date();
|
||||
oldestDateForNextUp.setDate(oldestDateForNextUp.getDate() - userSettings.maxDaysForNextUp());
|
||||
return apiClient.getNextUpEpisodes({
|
||||
Limit: enableScrollX() ? 24 : 15,
|
||||
Fields: 'PrimaryImageAspectRatio,DateCreated,BasicSyncInfo,Path,MediaSourceCount',
|
||||
UserId: apiClient.getCurrentUserId(),
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
||||
EnableTotalRecordCount: false,
|
||||
DisableFirstEpisode: false,
|
||||
NextUpDateCutoff: oldestDateForNextUp.toISOString(),
|
||||
EnableRewatching: userSettings.enableRewatchingInNextUp()
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getNextUpItemsHtmlFn(useEpisodeImages) {
|
||||
return function (items) {
|
||||
const cardLayout = false;
|
||||
return cardBuilder.getCardsHtml({
|
||||
items: items,
|
||||
preferThumb: true,
|
||||
inheritThumb: !useEpisodeImages,
|
||||
shape: getBackdropShape(enableScrollX()),
|
||||
overlayText: false,
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
lazy: true,
|
||||
overlayPlayButton: true,
|
||||
context: 'home',
|
||||
centerText: !cardLayout,
|
||||
allowBottomPadding: !enableScrollX(),
|
||||
cardLayout: cardLayout
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function loadNextUp(elem, apiClient, userSettings) {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||
if (!layoutManager.tv) {
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('nextup', {
|
||||
serverId: apiClient.serverId()
|
||||
}) + '" class="button-flat button-flat-mini sectionTitleTextButton">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||
html += globalize.translate('NextUp');
|
||||
html += '</h2>';
|
||||
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
} else {
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||
html += globalize.translate('NextUp');
|
||||
html += '</h2>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (enableScrollX()) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x" data-monitor="videoplayback,markplayed">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x" data-monitor="videoplayback,markplayed">';
|
||||
}
|
||||
|
||||
if (enableScrollX()) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
elem.classList.add('hide');
|
||||
elem.innerHTML = html;
|
||||
|
||||
const itemsContainer = elem.querySelector('.itemsContainer');
|
||||
itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings);
|
||||
itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume());
|
||||
itemsContainer.parentContainer = elem;
|
||||
}
|
||||
|
||||
function getLatestRecordingsFetchFn(serverId, activeRecordingsOnly) {
|
||||
return function () {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
return apiClient.getLiveTvRecordings({
|
||||
userId: apiClient.getCurrentUserId(),
|
||||
Limit: enableScrollX() ? 12 : 5,
|
||||
Fields: 'PrimaryImageAspectRatio,BasicSyncInfo',
|
||||
EnableTotalRecordCount: false,
|
||||
IsLibraryItem: activeRecordingsOnly ? null : false,
|
||||
IsInProgress: activeRecordingsOnly ? true : null
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getLatestRecordingItemsHtml(activeRecordingsOnly) {
|
||||
return function (items) {
|
||||
return cardBuilder.getCardsHtml({
|
||||
items: items,
|
||||
shape: enableScrollX() ? 'autooverflow' : 'auto',
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
coverImage: true,
|
||||
lazy: true,
|
||||
showDetailsMenu: true,
|
||||
centerText: true,
|
||||
overlayText: false,
|
||||
showYear: true,
|
||||
lines: 2,
|
||||
overlayPlayButton: !activeRecordingsOnly,
|
||||
allowBottomPadding: !enableScrollX(),
|
||||
preferThumb: true,
|
||||
cardLayout: false,
|
||||
overlayMoreButton: activeRecordingsOnly,
|
||||
action: activeRecordingsOnly ? 'none' : null,
|
||||
centerPlayButton: activeRecordingsOnly
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function loadLatestLiveTvRecordings(elem, activeRecordingsOnly, apiClient) {
|
||||
const title = activeRecordingsOnly ?
|
||||
globalize.translate('HeaderActiveRecordings') :
|
||||
globalize.translate('HeaderLatestRecordings');
|
||||
|
||||
let html = '';
|
||||
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + title + '</h2>';
|
||||
html += '</div>';
|
||||
|
||||
if (enableScrollX()) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x">';
|
||||
}
|
||||
|
||||
if (enableScrollX()) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
elem.classList.add('hide');
|
||||
elem.innerHTML = html;
|
||||
|
||||
const itemsContainer = elem.querySelector('.itemsContainer');
|
||||
itemsContainer.fetchData = getLatestRecordingsFetchFn(apiClient.serverId(), activeRecordingsOnly);
|
||||
itemsContainer.getItemsHtml = getLatestRecordingItemsHtml(activeRecordingsOnly);
|
||||
itemsContainer.parentContainer = elem;
|
||||
}
|
||||
|
||||
export default {
|
||||
loadLibraryTiles: loadLibraryTiles,
|
||||
getDefaultSection: getDefaultSection,
|
||||
loadSections: loadSections,
|
||||
destroySections: destroySections,
|
||||
pause: pause,
|
||||
resume: resume
|
||||
getDefaultSection,
|
||||
loadSections,
|
||||
destroySections,
|
||||
pause,
|
||||
resume
|
||||
};
|
||||
|
||||
|
|
92
src/components/homesections/sections/activeRecordings.ts
Normal file
92
src/components/homesections/sections/activeRecordings.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
import type { ApiClient } from 'jellyfin-apiclient';
|
||||
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import type { SectionContainerElement, SectionOptions } from './section';
|
||||
|
||||
function getLatestRecordingsFetchFn(
|
||||
serverId: string,
|
||||
activeRecordingsOnly: boolean,
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return function () {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
return apiClient.getLiveTvRecordings({
|
||||
userId: apiClient.getCurrentUserId(),
|
||||
Limit: enableOverflow ? 12 : 5,
|
||||
Fields: 'PrimaryImageAspectRatio,BasicSyncInfo',
|
||||
EnableTotalRecordCount: false,
|
||||
IsLibraryItem: activeRecordingsOnly ? null : false,
|
||||
IsInProgress: activeRecordingsOnly ? true : null
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getLatestRecordingItemsHtml(
|
||||
activeRecordingsOnly: boolean,
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return function (items: BaseItemDto[]) {
|
||||
return cardBuilder.getCardsHtml({
|
||||
items: items,
|
||||
shape: enableOverflow ? 'autooverflow' : 'auto',
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
coverImage: true,
|
||||
lazy: true,
|
||||
showDetailsMenu: true,
|
||||
centerText: true,
|
||||
overlayText: false,
|
||||
showYear: true,
|
||||
lines: 2,
|
||||
overlayPlayButton: !activeRecordingsOnly,
|
||||
allowBottomPadding: !enableOverflow,
|
||||
preferThumb: true,
|
||||
cardLayout: false,
|
||||
overlayMoreButton: activeRecordingsOnly,
|
||||
action: activeRecordingsOnly ? 'none' : null,
|
||||
centerPlayButton: activeRecordingsOnly
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function loadRecordings(
|
||||
elem: HTMLElement,
|
||||
activeRecordingsOnly: boolean,
|
||||
apiClient: ApiClient,
|
||||
options: SectionOptions
|
||||
) {
|
||||
const title = activeRecordingsOnly ?
|
||||
globalize.translate('HeaderActiveRecordings') :
|
||||
globalize.translate('HeaderLatestRecordings');
|
||||
|
||||
let html = '';
|
||||
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + title + '</h2>';
|
||||
html += '</div>';
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x">';
|
||||
}
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
elem.classList.add('hide');
|
||||
elem.innerHTML = html;
|
||||
|
||||
const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer');
|
||||
if (!itemsContainer) return;
|
||||
itemsContainer.fetchData = getLatestRecordingsFetchFn(apiClient.serverId(), activeRecordingsOnly, options);
|
||||
itemsContainer.getItemsHtml = getLatestRecordingItemsHtml(activeRecordingsOnly, options);
|
||||
itemsContainer.parentContainer = elem;
|
||||
}
|
36
src/components/homesections/sections/libraryButtons.ts
Normal file
36
src/components/homesections/sections/libraryButtons.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
import escapeHtml from 'escape-html';
|
||||
|
||||
import imageLoader from 'components/images/imageLoader';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import globalize from 'scripts/globalize';
|
||||
import imageHelper from 'scripts/imagehelper';
|
||||
|
||||
function getLibraryButtonsHtml(items: BaseItemDto[]) {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="verticalSection verticalSection-extrabottompadding">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + globalize.translate('HeaderMyMedia') + '</h2>';
|
||||
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x" data-multiselect="false">';
|
||||
|
||||
// library card background images
|
||||
for (let i = 0, length = items.length; i < length; i++) {
|
||||
const item = items[i];
|
||||
const icon = imageHelper.getLibraryIcon(item.CollectionType);
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(item) + '" class="raised homeLibraryButton"><span class="material-icons homeLibraryIcon ' + icon + '" aria-hidden="true"></span><span class="homeLibraryText">' + escapeHtml(item.Name) + '</span></a>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
export function loadLibraryButtons(elem: HTMLElement, userViews: BaseItemDto[]) {
|
||||
elem.classList.remove('verticalSection');
|
||||
const html = getLibraryButtonsHtml(userViews);
|
||||
|
||||
elem.innerHTML = html;
|
||||
imageLoader.lazyChildren(elem);
|
||||
}
|
46
src/components/homesections/sections/libraryTiles.ts
Normal file
46
src/components/homesections/sections/libraryTiles.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import imageLoader from 'components/images/imageLoader';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { getBackdropShape } from 'utils/card';
|
||||
|
||||
import type { SectionOptions } from './section';
|
||||
|
||||
export function loadLibraryTiles(
|
||||
elem: HTMLElement,
|
||||
userViews: BaseItemDto[],
|
||||
{
|
||||
enableOverflow
|
||||
}: SectionOptions
|
||||
) {
|
||||
let html = '';
|
||||
if (userViews.length) {
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + globalize.translate('HeaderMyMedia') + '</h2>';
|
||||
if (enableOverflow) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right focuscontainer-x vertical-wrap">';
|
||||
}
|
||||
|
||||
html += cardBuilder.getCardsHtml({
|
||||
items: userViews,
|
||||
shape: getBackdropShape(enableOverflow),
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
overlayText: false,
|
||||
lazy: true,
|
||||
transition: false,
|
||||
allowBottomPadding: !enableOverflow
|
||||
});
|
||||
|
||||
if (enableOverflow) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
elem.innerHTML = html;
|
||||
imageLoader.lazyChildren(elem);
|
||||
}
|
181
src/components/homesections/sections/liveTv.ts
Normal file
181
src/components/homesections/sections/liveTv.ts
Normal file
|
@ -0,0 +1,181 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
|
||||
import type { ApiClient } from 'jellyfin-apiclient';
|
||||
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { getBackdropShape } from 'utils/card';
|
||||
|
||||
import type { SectionContainerElement, SectionOptions } from './section';
|
||||
|
||||
function getOnNowFetchFn(
|
||||
serverId: string
|
||||
) {
|
||||
return function () {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
return apiClient.getLiveTvRecommendedPrograms({
|
||||
userId: apiClient.getCurrentUserId(),
|
||||
IsAiring: true,
|
||||
limit: 24,
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Thumb,Backdrop',
|
||||
EnableTotalRecordCount: false,
|
||||
Fields: 'ChannelInfo,PrimaryImageAspectRatio'
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getOnNowItemsHtmlFn(
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return (items: BaseItemDto[]) => (
|
||||
cardBuilder.getCardsHtml({
|
||||
items: items,
|
||||
preferThumb: 'auto',
|
||||
inheritThumb: false,
|
||||
shape: (enableOverflow ? 'autooverflow' : 'auto'),
|
||||
showParentTitleOrTitle: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
coverImage: true,
|
||||
overlayText: false,
|
||||
allowBottomPadding: !enableOverflow,
|
||||
showAirTime: true,
|
||||
showChannelName: false,
|
||||
showAirDateTime: false,
|
||||
showAirEndTime: true,
|
||||
defaultShape: getBackdropShape(enableOverflow),
|
||||
lines: 3,
|
||||
overlayPlayButton: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function buildSection(
|
||||
elem: HTMLElement,
|
||||
serverId: string,
|
||||
options: SectionOptions
|
||||
) {
|
||||
let html = '';
|
||||
|
||||
elem.classList.remove('padded-left');
|
||||
elem.classList.remove('padded-right');
|
||||
elem.classList.remove('padded-bottom');
|
||||
elem.classList.remove('verticalSection');
|
||||
|
||||
html += '<div class="verticalSection">';
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('LiveTV') + '</h2>';
|
||||
html += '</div>';
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true" data-scrollbuttons="false">';
|
||||
html += '<div class="padded-top padded-bottom scrollSlider focuscontainer-x">';
|
||||
} else {
|
||||
html += '<div class="padded-top padded-bottom focuscontainer-x">';
|
||||
}
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId,
|
||||
section: 'programs'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Programs') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId,
|
||||
section: 'guide'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Guide') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId,
|
||||
section: 'channels'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Channels') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('recordedtv', {
|
||||
serverId
|
||||
}) + '" class="raised"><span>' + globalize.translate('Recordings') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId,
|
||||
section: 'dvrschedule'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Schedule') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId,
|
||||
section: 'seriesrecording'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Series') + '</span></a>';
|
||||
|
||||
html += '</div>';
|
||||
if (options.enableOverflow) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="verticalSection">';
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||
|
||||
if (!layoutManager.tv) {
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId,
|
||||
section: 'onnow'
|
||||
}) + '" class="more button-flat button-flat-mini sectionTitleTextButton">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||
html += globalize.translate('HeaderOnNow');
|
||||
html += '</h2>';
|
||||
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
} else {
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('HeaderOnNow') + '</h2>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x">';
|
||||
}
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
elem.innerHTML = html;
|
||||
|
||||
const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer');
|
||||
if (!itemsContainer) return;
|
||||
itemsContainer.parentContainer = elem;
|
||||
itemsContainer.fetchData = getOnNowFetchFn(serverId);
|
||||
itemsContainer.getItemsHtml = getOnNowItemsHtmlFn(options);
|
||||
}
|
||||
|
||||
export function loadLiveTV(
|
||||
elem: HTMLElement,
|
||||
apiClient: ApiClient,
|
||||
user: UserDto,
|
||||
options: SectionOptions
|
||||
) {
|
||||
if (!user.Policy?.EnableLiveTvAccess) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return apiClient.getLiveTvRecommendedPrograms({
|
||||
userId: apiClient.getCurrentUserId(),
|
||||
IsAiring: true,
|
||||
limit: 1,
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Thumb,Backdrop',
|
||||
EnableTotalRecordCount: false,
|
||||
Fields: 'ChannelInfo,PrimaryImageAspectRatio'
|
||||
}).then(function (result) {
|
||||
if (result.Items?.length) {
|
||||
buildSection(elem, apiClient.serverId(), options);
|
||||
}
|
||||
});
|
||||
}
|
106
src/components/homesections/sections/nextUp.ts
Normal file
106
src/components/homesections/sections/nextUp.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
import type { ApiClient } from 'jellyfin-apiclient';
|
||||
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import globalize from 'scripts/globalize';
|
||||
import type { UserSettings } from 'scripts/settings/userSettings';
|
||||
import { getBackdropShape } from 'utils/card';
|
||||
|
||||
import type { SectionContainerElement, SectionOptions } from './section';
|
||||
|
||||
function getNextUpFetchFn(
|
||||
serverId: string,
|
||||
userSettings: UserSettings,
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return function () {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
const oldestDateForNextUp = new Date();
|
||||
oldestDateForNextUp.setDate(oldestDateForNextUp.getDate() - userSettings.maxDaysForNextUp());
|
||||
return apiClient.getNextUpEpisodes({
|
||||
Limit: enableOverflow ? 24 : 15,
|
||||
Fields: 'PrimaryImageAspectRatio,DateCreated,BasicSyncInfo,Path,MediaSourceCount',
|
||||
UserId: apiClient.getCurrentUserId(),
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
||||
EnableTotalRecordCount: false,
|
||||
DisableFirstEpisode: false,
|
||||
NextUpDateCutoff: oldestDateForNextUp.toISOString(),
|
||||
EnableRewatching: userSettings.enableRewatchingInNextUp()
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getNextUpItemsHtmlFn(
|
||||
useEpisodeImages: boolean,
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return function (items: BaseItemDto[]) {
|
||||
const cardLayout = false;
|
||||
return cardBuilder.getCardsHtml({
|
||||
items: items,
|
||||
preferThumb: true,
|
||||
inheritThumb: !useEpisodeImages,
|
||||
shape: getBackdropShape(enableOverflow),
|
||||
overlayText: false,
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
lazy: true,
|
||||
overlayPlayButton: true,
|
||||
context: 'home',
|
||||
centerText: !cardLayout,
|
||||
allowBottomPadding: !enableOverflow,
|
||||
cardLayout: cardLayout
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function loadNextUp(
|
||||
elem: HTMLElement,
|
||||
apiClient: ApiClient,
|
||||
userSettings: UserSettings,
|
||||
options: SectionOptions
|
||||
) {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||
if (!layoutManager.tv) {
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('nextup', {
|
||||
serverId: apiClient.serverId()
|
||||
}) + '" class="button-flat button-flat-mini sectionTitleTextButton">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||
html += globalize.translate('NextUp');
|
||||
html += '</h2>';
|
||||
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
} else {
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||
html += globalize.translate('NextUp');
|
||||
html += '</h2>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x" data-monitor="videoplayback,markplayed">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x" data-monitor="videoplayback,markplayed">';
|
||||
}
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
elem.classList.add('hide');
|
||||
elem.innerHTML = html;
|
||||
|
||||
const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer');
|
||||
if (!itemsContainer) return;
|
||||
itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings, options);
|
||||
itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), options);
|
||||
itemsContainer.parentContainer = elem;
|
||||
}
|
158
src/components/homesections/sections/recentlyAdded.ts
Normal file
158
src/components/homesections/sections/recentlyAdded.ts
Normal file
|
@ -0,0 +1,158 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
|
||||
import escapeHtml from 'escape-html';
|
||||
import type { ApiClient } from 'jellyfin-apiclient';
|
||||
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import globalize from 'scripts/globalize';
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
|
||||
|
||||
import type { SectionContainerElement, SectionOptions } from './section';
|
||||
|
||||
function getFetchLatestItemsFn(
|
||||
serverId: string,
|
||||
parentId: string | undefined,
|
||||
collectionType: string | null | undefined,
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return function () {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
let limit = 16;
|
||||
|
||||
if (enableOverflow) {
|
||||
if (collectionType === 'music') {
|
||||
limit = 30;
|
||||
}
|
||||
} else if (collectionType === 'tvshows') {
|
||||
limit = 5;
|
||||
} else if (collectionType === 'music') {
|
||||
limit = 9;
|
||||
} else {
|
||||
limit = 8;
|
||||
}
|
||||
|
||||
const options = {
|
||||
Limit: limit,
|
||||
Fields: 'PrimaryImageAspectRatio,BasicSyncInfo,Path',
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Backdrop,Thumb',
|
||||
ParentId: parentId
|
||||
};
|
||||
|
||||
return apiClient.getLatestItems(options);
|
||||
};
|
||||
}
|
||||
|
||||
function getLatestItemsHtmlFn(
|
||||
itemType: BaseItemKind | undefined,
|
||||
viewType: string | null | undefined,
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return function (items: BaseItemDto[]) {
|
||||
const cardLayout = false;
|
||||
let shape;
|
||||
if (itemType === 'Channel' || viewType === 'movies' || viewType === 'books' || viewType === 'tvshows') {
|
||||
shape = getPortraitShape(enableOverflow);
|
||||
} else if (viewType === 'music' || viewType === 'homevideos') {
|
||||
shape = getSquareShape(enableOverflow);
|
||||
} else {
|
||||
shape = getBackdropShape(enableOverflow);
|
||||
}
|
||||
|
||||
return cardBuilder.getCardsHtml({
|
||||
items: items,
|
||||
shape: shape,
|
||||
preferThumb: viewType !== 'movies' && viewType !== 'tvshows' && itemType !== 'Channel' && viewType !== 'music' ? 'auto' : null,
|
||||
showUnplayedIndicator: false,
|
||||
showChildCountIndicator: true,
|
||||
context: 'home',
|
||||
overlayText: false,
|
||||
centerText: !cardLayout,
|
||||
overlayPlayButton: viewType !== 'photos',
|
||||
allowBottomPadding: !enableOverflow && !cardLayout,
|
||||
cardLayout: cardLayout,
|
||||
showTitle: viewType !== 'photos',
|
||||
showYear: viewType === 'movies' || viewType === 'tvshows' || !viewType,
|
||||
showParentTitle: viewType === 'music' || viewType === 'tvshows' || !viewType || (cardLayout && (viewType === 'tvshows')),
|
||||
lines: 2
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function renderLatestSection(
|
||||
elem: HTMLElement,
|
||||
apiClient: ApiClient,
|
||||
user: UserDto,
|
||||
parent: BaseItemDto,
|
||||
options: SectionOptions
|
||||
) {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||
if (!layoutManager.tv) {
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(parent, {
|
||||
section: 'latest'
|
||||
}) + '" class="more button-flat button-flat-mini sectionTitleTextButton">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||
html += globalize.translate('LatestFromLibrary', escapeHtml(parent.Name));
|
||||
html += '</h2>';
|
||||
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
} else {
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('LatestFromLibrary', escapeHtml(parent.Name)) + '</h2>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer focuscontainer-x padded-left padded-right vertical-wrap">';
|
||||
}
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
elem.innerHTML = html;
|
||||
|
||||
const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer');
|
||||
if (!itemsContainer) return;
|
||||
itemsContainer.fetchData = getFetchLatestItemsFn(apiClient.serverId(), parent.Id, parent.CollectionType, options);
|
||||
itemsContainer.getItemsHtml = getLatestItemsHtmlFn(parent.Type, parent.CollectionType, options);
|
||||
itemsContainer.parentContainer = elem;
|
||||
}
|
||||
|
||||
export function loadRecentlyAdded(
|
||||
elem: HTMLElement,
|
||||
apiClient: ApiClient,
|
||||
user: UserDto,
|
||||
userViews: BaseItemDto[],
|
||||
options: SectionOptions
|
||||
) {
|
||||
elem.classList.remove('verticalSection');
|
||||
const excludeViewTypes = ['playlists', 'livetv', 'boxsets', 'channels'];
|
||||
const userExcludeItems = user.Configuration?.LatestItemsExcludes ?? [];
|
||||
|
||||
userViews.forEach(item => {
|
||||
if (!item.Id || userExcludeItems.indexOf(item.Id) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.CollectionType || excludeViewTypes.indexOf(item.CollectionType) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frag = document.createElement('div');
|
||||
frag.classList.add('verticalSection');
|
||||
frag.classList.add('hide');
|
||||
elem.appendChild(frag);
|
||||
|
||||
renderLatestSection(frag, apiClient, user, item, options);
|
||||
});
|
||||
}
|
105
src/components/homesections/sections/resume.ts
Normal file
105
src/components/homesections/sections/resume.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import type { ApiClient } from 'jellyfin-apiclient';
|
||||
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import globalize from 'scripts/globalize';
|
||||
import type { UserSettings } from 'scripts/settings/userSettings';
|
||||
import { getBackdropShape, getPortraitShape } from 'utils/card';
|
||||
|
||||
import type { SectionContainerElement, SectionOptions } from './section';
|
||||
|
||||
const dataMonitorHints: Record<string, string> = {
|
||||
Audio: 'audioplayback,markplayed',
|
||||
Video: 'videoplayback,markplayed'
|
||||
};
|
||||
|
||||
function getItemsToResumeFn(
|
||||
mediaType: BaseItemKind,
|
||||
serverId: string,
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return function () {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
const limit = enableOverflow ? 12 : 5;
|
||||
|
||||
const options = {
|
||||
Limit: limit,
|
||||
Recursive: true,
|
||||
Fields: 'PrimaryImageAspectRatio,BasicSyncInfo',
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Backdrop,Thumb',
|
||||
EnableTotalRecordCount: false,
|
||||
MediaTypes: mediaType
|
||||
};
|
||||
|
||||
return apiClient.getResumableItems(apiClient.getCurrentUserId(), options);
|
||||
};
|
||||
}
|
||||
|
||||
function getItemsToResumeHtmlFn(
|
||||
useEpisodeImages: boolean,
|
||||
mediaType: BaseItemKind,
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return function (items: BaseItemDto[]) {
|
||||
const cardLayout = false;
|
||||
return cardBuilder.getCardsHtml({
|
||||
items: items,
|
||||
preferThumb: true,
|
||||
inheritThumb: !useEpisodeImages,
|
||||
shape: (mediaType === 'Book') ?
|
||||
getPortraitShape(enableOverflow) :
|
||||
getBackdropShape(enableOverflow),
|
||||
overlayText: false,
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
lazy: true,
|
||||
showDetailsMenu: true,
|
||||
overlayPlayButton: true,
|
||||
context: 'home',
|
||||
centerText: !cardLayout,
|
||||
allowBottomPadding: false,
|
||||
cardLayout: cardLayout,
|
||||
showYear: true,
|
||||
lines: 2
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function loadResume(
|
||||
elem: HTMLElement,
|
||||
apiClient: ApiClient,
|
||||
titleLabel: string,
|
||||
mediaType: BaseItemKind,
|
||||
userSettings: UserSettings,
|
||||
options: SectionOptions
|
||||
) {
|
||||
let html = '';
|
||||
|
||||
const dataMonitor = dataMonitorHints[mediaType] ?? 'markplayed';
|
||||
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + globalize.translate(titleLabel) + '</h2>';
|
||||
if (options.enableOverflow) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += `<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x" data-monitor="${dataMonitor}">`;
|
||||
} else {
|
||||
html += `<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x" data-monitor="${dataMonitor}">`;
|
||||
}
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
elem.classList.add('hide');
|
||||
elem.innerHTML = html;
|
||||
|
||||
const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer');
|
||||
if (!itemsContainer) return;
|
||||
itemsContainer.fetchData = getItemsToResumeFn(mediaType, apiClient.serverId(), options);
|
||||
itemsContainer.getItemsHtml = getItemsToResumeHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), mediaType, options);
|
||||
itemsContainer.parentContainer = elem;
|
||||
}
|
12
src/components/homesections/sections/section.d.ts
vendored
Normal file
12
src/components/homesections/sections/section.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto-query-result';
|
||||
|
||||
export interface SectionOptions {
|
||||
enableOverflow: boolean
|
||||
}
|
||||
|
||||
export type SectionContainerElement = {
|
||||
fetchData: () => Promise<BaseItemDtoQueryResult | BaseItemDto[]>
|
||||
getItemsHtml: (items: BaseItemDto[]) => void
|
||||
parentContainer: HTMLElement
|
||||
} & Element;
|
|
@ -444,7 +444,7 @@ function executeCommand(item, id, options) {
|
|||
});
|
||||
break;
|
||||
case 'multiSelect':
|
||||
import('./multiSelect/multiSelect').then(({ startMultiSelect: startMultiSelect }) => {
|
||||
import('./multiSelect/multiSelect').then(({ startMultiSelect }) => {
|
||||
const card = dom.parentWithClass(options.positionTo, 'card');
|
||||
startMultiSelect(card);
|
||||
});
|
||||
|
|
|
@ -527,7 +527,7 @@ class AppRouter {
|
|||
}
|
||||
|
||||
if (item === 'manageserver') {
|
||||
return '#/dashboard.html';
|
||||
return '#/dashboard';
|
||||
}
|
||||
|
||||
if (item === 'recordedtv') {
|
||||
|
|
|
@ -49,7 +49,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
|||
const getDefaultParameters = useCallback(() => ({
|
||||
ParentId: parentId,
|
||||
searchTerm: query,
|
||||
Limit: 24,
|
||||
Limit: 100,
|
||||
Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount',
|
||||
Recursive: true,
|
||||
EnableTotalRecordCount: false,
|
||||
|
|
|
@ -115,7 +115,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
|
|||
<MenuItem
|
||||
key='admin-dashboard-link'
|
||||
component={Link}
|
||||
to='/dashboard.html'
|
||||
to='/dashboard'
|
||||
onClick={onMenuClose}
|
||||
>
|
||||
|
||||
|
@ -127,7 +127,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
|
|||
<MenuItem
|
||||
key='admin-metadata-link'
|
||||
component={Link}
|
||||
to='/edititemmetadata.html'
|
||||
to='/metadata'
|
||||
onClick={onMenuClose}
|
||||
>
|
||||
<ListItemIcon>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="dashboardSections" style="padding-top:.5em;">
|
||||
<div class="dashboardColumn dashboardColumn-2-60 dashboardColumn-3-46">
|
||||
<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>
|
||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
|
||||
<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>
|
||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
@ -70,7 +70,7 @@
|
|||
</div>
|
||||
|
||||
<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>
|
||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
|
|
@ -73,7 +73,7 @@ function showDeviceMenu(view, btn, deviceId) {
|
|||
callback: function (id) {
|
||||
switch (id) {
|
||||
case 'open':
|
||||
Dashboard.navigate('device.html?id=' + deviceId);
|
||||
Dashboard.navigate('dashboard/devices/edit?id=' + deviceId);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
|
@ -94,7 +94,7 @@ function load(page, devices) {
|
|||
deviceHtml += '<div class="cardBox visualCardBox">';
|
||||
deviceHtml += '<div class="cardScalable">';
|
||||
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
|
||||
const iconUrl = imageHelper.getDeviceIcon(device);
|
||||
|
||||
|
|
|
@ -264,7 +264,7 @@
|
|||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -639,7 +639,7 @@ function saveProfile(page, profile) {
|
|||
data: JSON.stringify(profile),
|
||||
contentType: 'application/json'
|
||||
}).then(function () {
|
||||
Dashboard.navigate('dlnaprofiles.html');
|
||||
Dashboard.navigate('dashboard/dlna/profiles');
|
||||
}, Dashboard.processErrorResponse);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -40,7 +40,7 @@ function renderProfiles(page, element, profiles) {
|
|||
html += '<div class="listItem listItem-border">';
|
||||
html += '<span class="listItemIcon material-icons live_tv" aria-hidden="true"></span>';
|
||||
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 += '</a>';
|
||||
html += '</div>';
|
||||
|
@ -78,10 +78,10 @@ function deleteProfile(page, id) {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dlnasettings.html',
|
||||
href: '#/dashboard/dlna',
|
||||
name: globalize.translate('Settings')
|
||||
}, {
|
||||
href: '#/dlnaprofiles.html',
|
||||
href: '#/dashboard/dlna/profiles',
|
||||
name: globalize.translate('TabProfiles')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -37,10 +37,10 @@ function onSubmit() {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dlnasettings.html',
|
||||
href: '#/dashboard/dlna',
|
||||
name: globalize.translate('Settings')
|
||||
}, {
|
||||
href: '#/dlnaprofiles.html',
|
||||
href: '#/dashboard/dlna/profiles',
|
||||
name: globalize.translate('TabProfiles')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -167,13 +167,13 @@ function setDecodingCodecsVisible(context, value) {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/encodingsettings.html',
|
||||
href: '#/dashboard/playback/transcoding',
|
||||
name: globalize.translate('Transcoding')
|
||||
}, {
|
||||
href: '#/playbackconfiguration.html',
|
||||
href: '#/dashboard/playback/resume',
|
||||
name: globalize.translate('ButtonResume')
|
||||
}, {
|
||||
href: '#/streamingsettings.html',
|
||||
href: '#/dashboard/playback/streaming',
|
||||
name: globalize.translate('TabStreaming')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -360,16 +360,16 @@ function getVirtualFolderHtml(page, virtualFolder, index) {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/library.html',
|
||||
href: '#/dashboard/libraries',
|
||||
name: globalize.translate('HeaderLibraries')
|
||||
}, {
|
||||
href: '#/librarydisplay.html',
|
||||
href: '#/dashboard/libraries/display',
|
||||
name: globalize.translate('Display')
|
||||
}, {
|
||||
href: '#/metadataimages.html',
|
||||
href: '#/dashboard/libraries/metadata',
|
||||
name: globalize.translate('Metadata')
|
||||
}, {
|
||||
href: '#/metadatanfo.html',
|
||||
href: '#/dashboard/libraries/nfo',
|
||||
name: globalize.translate('TabNfoSettings')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -7,16 +7,16 @@ import Dashboard from '../../utils/dashboard';
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/library.html',
|
||||
href: '#/dashboard/libraries',
|
||||
name: globalize.translate('HeaderLibraries')
|
||||
}, {
|
||||
href: '#/librarydisplay.html',
|
||||
href: '#/dashboard/libraries/display',
|
||||
name: globalize.translate('Display')
|
||||
}, {
|
||||
href: '#/metadataimages.html',
|
||||
href: '#/dashboard/libraries/metadata',
|
||||
name: globalize.translate('Metadata')
|
||||
}, {
|
||||
href: '#/metadatanfo.html',
|
||||
href: '#/dashboard/libraries/nfo',
|
||||
name: globalize.translate('TabNfoSettings')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -88,16 +88,16 @@ function onSubmit() {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/library.html',
|
||||
href: '#/dashboard/libraries',
|
||||
name: globalize.translate('HeaderLibraries')
|
||||
}, {
|
||||
href: '#/librarydisplay.html',
|
||||
href: '#/dashboard/libraries/display',
|
||||
name: globalize.translate('Display')
|
||||
}, {
|
||||
href: '#/metadataimages.html',
|
||||
href: '#/dashboard/libraries/metadata',
|
||||
name: globalize.translate('Metadata')
|
||||
}, {
|
||||
href: '#/metadatanfo.html',
|
||||
href: '#/dashboard/libraries/nfo',
|
||||
name: globalize.translate('TabNfoSettings')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -46,16 +46,16 @@ function showConfirmMessage() {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/library.html',
|
||||
href: '#/dashboard/libraries',
|
||||
name: globalize.translate('HeaderLibraries')
|
||||
}, {
|
||||
href: '#/librarydisplay.html',
|
||||
href: '#/dashboard/libraries/display',
|
||||
name: globalize.translate('Display')
|
||||
}, {
|
||||
href: '#/metadataimages.html',
|
||||
href: '#/dashboard/libraries/metadata',
|
||||
name: globalize.translate('Metadata')
|
||||
}, {
|
||||
href: '#/metadatanfo.html',
|
||||
href: '#/dashboard/libraries/nfo',
|
||||
name: globalize.translate('TabNfoSettings')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -31,13 +31,13 @@ function onSubmit() {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/encodingsettings.html',
|
||||
href: '#/dashboard/playback/transcoding',
|
||||
name: globalize.translate('Transcoding')
|
||||
}, {
|
||||
href: '#/playbackconfiguration.html',
|
||||
href: '#/dashboard/playback/resume',
|
||||
name: globalize.translate('ButtonResume')
|
||||
}, {
|
||||
href: '#/streamingsettings.html',
|
||||
href: '#/dashboard/playback/streaming',
|
||||
name: globalize.translate('TabStreaming')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -120,7 +120,7 @@ function onSearchBarType(searchBar) {
|
|||
|
||||
function getPluginHtml(plugin, options, installedPlugins) {
|
||||
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) {
|
||||
href += '&context=' + options.context;
|
||||
|
@ -161,13 +161,13 @@ function getPluginHtml(plugin, options, installedPlugins) {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/installedplugins.html',
|
||||
href: '#/dashboard/plugins',
|
||||
name: globalize.translate('TabMyPlugins')
|
||||
}, {
|
||||
href: '#/availableplugins.html',
|
||||
href: '#/dashboard/plugins/catalog',
|
||||
name: globalize.translate('TabCatalog')
|
||||
}, {
|
||||
href: '#/repositories.html',
|
||||
href: '#/dashboard/plugins/repositories',
|
||||
name: globalize.translate('TabRepositories')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -130,7 +130,7 @@ function populateList(page, plugins, pluginConfigurationPages) {
|
|||
} else {
|
||||
html += '<div class="centerMessage">';
|
||||
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 += '</a></p>';
|
||||
html += '</div>';
|
||||
|
@ -221,13 +221,13 @@ function reloadList(page) {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/installedplugins.html',
|
||||
href: '#/dashboard/plugins',
|
||||
name: globalize.translate('TabMyPlugins')
|
||||
}, {
|
||||
href: '#/availableplugins.html',
|
||||
href: '#/dashboard/plugins/catalog',
|
||||
name: globalize.translate('TabCatalog')
|
||||
}, {
|
||||
href: '#/repositories.html',
|
||||
href: '#/dashboard/plugins/repositories',
|
||||
name: globalize.translate('TabRepositories')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -105,13 +105,13 @@ function getRepositoryElement(repository) {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/installedplugins.html',
|
||||
href: '#/dashboard/plugins',
|
||||
name: globalize.translate('TabMyPlugins')
|
||||
}, {
|
||||
href: '#/availableplugins.html',
|
||||
href: '#/dashboard/plugins/catalog',
|
||||
name: globalize.translate('TabCatalog')
|
||||
}, {
|
||||
href: '#/repositories.html',
|
||||
href: '#/dashboard/plugins/repositories',
|
||||
name: globalize.translate('TabRepositories')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -53,12 +53,12 @@ function populateList(page, tasks) {
|
|||
html += '<div class="paperList">';
|
||||
}
|
||||
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 += '</a>';
|
||||
html += '<div class="listItemBody two-line">';
|
||||
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 += "<div class='secondary listItemBodyText' id='taskProgress" + task.Id + "'>" + getTaskProgressHtml(task) + '</div>';
|
||||
html += '</a>';
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
@ -22,13 +22,13 @@ function onSubmit() {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/encodingsettings.html',
|
||||
href: '#/dashboard/playback/transcoding',
|
||||
name: globalize.translate('Transcoding')
|
||||
}, {
|
||||
href: '#/playbackconfiguration.html',
|
||||
href: '#/dashboard/playback/resume',
|
||||
name: globalize.translate('ButtonResume')
|
||||
}, {
|
||||
href: '#/streamingsettings.html',
|
||||
href: '#/dashboard/playback/streaming',
|
||||
name: globalize.translate('TabStreaming')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { getParameterByName } from '../utils/url.ts';
|
|||
import Events from '../utils/events.ts';
|
||||
|
||||
function onListingsSubmitted() {
|
||||
Dashboard.navigate('livetvstatus.html');
|
||||
Dashboard.navigate('dashboard/livetv');
|
||||
}
|
||||
|
||||
function init(page, type, providerId) {
|
||||
|
|
|
@ -220,9 +220,9 @@ function getProviderName(providerId) {
|
|||
function getProviderConfigurationUrl(providerId) {
|
||||
switch (providerId.toLowerCase()) {
|
||||
case 'xmltv':
|
||||
return '#/livetvguideprovider.html?type=xmltv';
|
||||
return '#/dashboard/livetv/guide?type=xmltv';
|
||||
case 'schedulesdirect':
|
||||
return '#/livetvguideprovider.html?type=schedulesdirect';
|
||||
return '#/dashboard/livetv/guide?type=schedulesdirect';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,7 +249,7 @@ function addProvider(button) {
|
|||
}
|
||||
|
||||
function addDevice() {
|
||||
Dashboard.navigate('livetvtuner.html');
|
||||
Dashboard.navigate('dashboard/livetv/tuner');
|
||||
}
|
||||
|
||||
function showDeviceMenu(button, tunerDeviceId) {
|
||||
|
@ -274,7 +274,7 @@ function showDeviceMenu(button, tunerDeviceId) {
|
|||
break;
|
||||
|
||||
case 'edit':
|
||||
Dashboard.navigate('livetvtuner.html?id=' + tunerDeviceId);
|
||||
Dashboard.navigate('dashboard/livetv/tuner?id=' + tunerDeviceId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -290,7 +290,7 @@ function onDevicesListClick(e) {
|
|||
if (btnCardOptions) {
|
||||
showDeviceMenu(btnCardOptions, id);
|
||||
} else {
|
||||
Dashboard.navigate('livetvtuner.html?id=' + id);
|
||||
Dashboard.navigate('dashboard/livetv/tuner?id=' + id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ function submitForm(page) {
|
|||
contentType: 'application/json'
|
||||
}).then(function () {
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
Dashboard.navigate('livetvstatus.html');
|
||||
Dashboard.navigate('dashboard/livetv');
|
||||
}, function () {
|
||||
loading.hide();
|
||||
Dashboard.alert({
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
</div>
|
||||
<div class="adminSection verticalSection verticalSection-extrabottompadding hide">
|
||||
<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">
|
||||
<span class="material-icons listItemIcon listItemIcon-transparent dashboard" aria-hidden="true"></span>
|
||||
<div class="listItemBody">
|
||||
|
@ -85,7 +85,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<span class="material-icons listItemIcon listItemIcon-transparent mode_edit" aria-hidden="true"></span>
|
||||
<div class="listItemBody">
|
||||
|
|
|
@ -327,8 +327,8 @@ function refreshLibraryInfoInDrawer(user) {
|
|||
html += '<h3 class="sidebarHeader">';
|
||||
html += globalize.translate('HeaderAdmin');
|
||||
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 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 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="#/metadata"><span class="material-icons navMenuOptionIcon mode_edit" aria-hidden="true"></span><span class="navMenuOptionText">${globalize.translate('Metadata')}</span></a>`;
|
||||
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() {
|
||||
const section = this.getElementsByClassName('sectionName')[0];
|
||||
const text = section ? section.innerHTML : this.innerHTML;
|
||||
|
@ -1026,15 +783,8 @@ pageClassOn('pageshow', 'page', function (e) {
|
|||
const isDashboardPage = page.classList.contains('type-interior');
|
||||
const isHomePage = page.classList.contains('homePage');
|
||||
const isLibraryPage = !isDashboardPage && page.classList.contains('libraryPage');
|
||||
const apiClient = getCurrentApiClient();
|
||||
|
||||
if (isDashboardPage) {
|
||||
if (mainDrawerButton) {
|
||||
mainDrawerButton.classList.remove('hide');
|
||||
}
|
||||
|
||||
refreshDashboardInfoInDrawer(page, apiClient);
|
||||
} else {
|
||||
if (!isDashboardPage) {
|
||||
if (mainDrawerButton) {
|
||||
if (enableLibraryNavDrawer || (isHomePage && enableLibraryNavDrawerHome)) {
|
||||
mainDrawerButton.classList.remove('hide');
|
||||
|
|
|
@ -301,7 +301,7 @@ export class UserSettings {
|
|||
|
||||
/**
|
||||
* Get or set 'Use Episode Images in Next Up and Continue Watching' state.
|
||||
* @param {string|boolean|undefined} val - Flag to enable 'Use Episode Images in Next Up and Continue Watching' or undefined.
|
||||
* @param {string|boolean|undefined} [val] - Flag to enable 'Use Episode Images in Next Up and Continue Watching' or undefined.
|
||||
* @return {boolean} 'Use Episode Images in Next Up' state.
|
||||
*/
|
||||
useEpisodeImagesInNextUpAndResume(val) {
|
||||
|
@ -463,7 +463,7 @@ export class UserSettings {
|
|||
|
||||
/**
|
||||
* Get or set max days for next up list.
|
||||
* @param {number|undefined} val - Max days for next up.
|
||||
* @param {number|undefined} [val] - Max days for next up.
|
||||
* @return {number} Max days for a show to stay in next up without being watched.
|
||||
*/
|
||||
maxDaysForNextUp(val) {
|
||||
|
@ -482,7 +482,7 @@ export class UserSettings {
|
|||
|
||||
/**
|
||||
* Get or set rewatching in next up.
|
||||
* @param {boolean|undefined} val - If rewatching items should be included in next up.
|
||||
* @param {boolean|undefined} [val] - If rewatching items should be included in next up.
|
||||
* @returns {boolean} Rewatching in next up state.
|
||||
*/
|
||||
enableRewatchingInNextUp(val) {
|
||||
|
|
|
@ -1336,7 +1336,7 @@
|
|||
"EnableFasterAnimations": "Hurtigere animationer",
|
||||
"DisablePlugin": "Deaktiver",
|
||||
"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",
|
||||
"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.",
|
||||
|
@ -1702,5 +1702,19 @@
|
|||
"SubtitleCyan": "Cyan",
|
||||
"SubtitleMagenta": "Magenta",
|
||||
"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"
|
||||
}
|
||||
|
|
|
@ -1733,7 +1733,7 @@
|
|||
"PasswordRequiredForAdmin": "Für Admin Konten wird ein Passwort benötigt.",
|
||||
"LabelEnableLUFSScan": "LUFS-Scan aktivieren",
|
||||
"LabelSyncPlayNoGroups": "Keine Gruppen verfügbar",
|
||||
"LabelEnableLUFSScanHelp": "Clients können die Audio Wiedergabe normalisieren um die selbe Lautstärke für mehrere Stücke zu bekommen.\nDie verlängert den Bibiliotheks Scan und benötigt mehr 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",
|
||||
"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).",
|
||||
|
@ -1758,8 +1758,8 @@
|
|||
"HeaderEpisodesStatus": "Episodenstatus",
|
||||
"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.",
|
||||
"LabelThrottleDelaySeconds": "Limitieren 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.",
|
||||
"LabelThrottleDelaySeconds": "Drosseln nach",
|
||||
"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",
|
||||
"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",
|
||||
|
@ -1776,5 +1776,5 @@
|
|||
"ForeignPartsOnly": "Erzwungen/Nur ausländische Teile",
|
||||
"HearingImpairedShort": "BaFa/SDH",
|
||||
"HeaderGuestCast": "Gast Stars",
|
||||
"LabelBackdropScreensaverIntervalHelp": "Die Zeit in Sekunden zwischen dem Wechsel verschiedener Hintergrundbilder im Bildschirmschoner"
|
||||
"LabelBackdropScreensaverIntervalHelp": "Die Zeit in Sekunden zwischen dem Wechsel verschiedener Hintergrundbilder im Bildschirmschoner."
|
||||
}
|
||||
|
|
|
@ -1264,7 +1264,7 @@
|
|||
"AskAdminToCreateLibrary": "Pide a un administrador crear una biblioteca.",
|
||||
"Artist": "Artista",
|
||||
"AllowFfmpegThrottlingHelp": "Cuando una transcodificación o remuxeado se adelanta lo suficiente de la posición de reproducción actual, se pausa el proceso para que consuma menos recursos. Esto es más útil cuando se mira sin buscar con frecuencia. Apaga esto si experimentas problemas de reproducción.",
|
||||
"AllowFfmpegThrottling": "Regular transcodificaciones",
|
||||
"AllowFfmpegThrottling": "Limitar transcodificaciones",
|
||||
"AlbumArtist": "Artista del álbum",
|
||||
"Album": "Album",
|
||||
"Yadif": "YADIF",
|
||||
|
@ -1756,10 +1756,10 @@
|
|||
"AllowSegmentDeletion": "Borrar segmentos",
|
||||
"HeaderEpisodesStatus": "Estatus de los Episodios",
|
||||
"AllowSegmentDeletionHelp": "Borrar los viejos segmentos después de que hayan sido enviados al cliente. Esto previene que se tenga almacenado la totalidad de la transcodificación en el disco. Esto funciona unicamente cuando se tenga habilitado el throttling. Apagar esta opción cuando se tengan problemas de reproducción.",
|
||||
"LabelThrottleDelaySeconds": "Acelerar después",
|
||||
"LabelThrottleDelaySeconds": "Limitar después de",
|
||||
"LabelThrottleDelaySecondsHelp": "Tiempo en segundos después de que la transcodificación entre en aceleración. Deben ser los suficientes para que el buffer del cliente siga operando. Unicamente funciona si la aceleración está habilitada.",
|
||||
"LabelSegmentKeepSeconds": "Tiempo para guardar segmentos",
|
||||
"LabelSegmentKeepSecondsHelp": "Tiempo en segundos en los que los segmentos deben permanecer antes de que sean sobrescritos. Estos deben de ser mayores a los indicados en \"Acelerar despues de\". Esto funciona unicamente si esta habilitada la opción de eliminar el segmento.",
|
||||
"LabelSegmentKeepSecondsHelp": "Tiempo en segundos en los que los segmentos deben permanecer antes de que sean sobrescritos. Estos deben de ser mayores a los indicados en \"Limitar después de\". Esto funciona unicamente si esta habilitada la opción de eliminar el segmento.",
|
||||
"AllowAv1Encoding": "Permitir encodificación en formato AV1",
|
||||
"GoHome": "Ir a Inicio",
|
||||
"UnknownError": "Un error desconocido ocurrió.",
|
||||
|
|
|
@ -1246,7 +1246,7 @@
|
|||
"LabelDroppedFrames": "Frames perdidos",
|
||||
"LabelCorruptedFrames": "Frames corruptos",
|
||||
"AskAdminToCreateLibrary": "Pídele a un administrador que cree una biblioteca.",
|
||||
"AllowFfmpegThrottling": "Acelerar las conversiones",
|
||||
"AllowFfmpegThrottling": "Limitar transcodificaciones",
|
||||
"ClientSettings": "Ajustes de cliente",
|
||||
"PreferEmbeddedEpisodeInfosOverFileNames": "Priorizar la información embebida sobre los nombres de archivos",
|
||||
"PreferEmbeddedEpisodeInfosOverFileNamesHelp": "Usar la información de episodio de los metadatos embebidos si está disponible.",
|
||||
|
|
|
@ -1307,7 +1307,7 @@
|
|||
"AllowRemoteAccessHelp": "Si no se marca, se bloquearán todas las conexiones remotas.",
|
||||
"AllowRemoteAccess": "Permitir conexiones remotas a este servidor",
|
||||
"AllowFfmpegThrottlingHelp": "Cuando una transcodificación o remuxeado se adelanta lo suficiente de la posición de reproducción actual, se pausa el proceso para que consuma menos recursos. Esto es más útil cuando se mira sin buscar con frecuencia. Apaga esto si experimentas problemas de reproducción.",
|
||||
"AllowFfmpegThrottling": "Regular transcodificaciones",
|
||||
"AllowFfmpegThrottling": "Limitar transcodificaciones",
|
||||
"AllowOnTheFlySubtitleExtractionHelp": "Los subtítulos incrustados pueden extraerse de los videos y entregarse a los clientes en texto plano para ayudar a evitar la transcodificación de video. En algunos sistemas, esto puede tardar mucho tiempo y provocar que la reproducción de video se detenga durante el proceso de extracción. Deshabilite esta opción para que los subtítulos incrustados se graben con transcodificación de video cuando no estén soportados de forma nativa por el dispositivo cliente.",
|
||||
"AllowOnTheFlySubtitleExtraction": "Permitir la extracción de subtítulos sobre la marcha",
|
||||
"AllowMediaConversionHelp": "Permitir o denegar acceso a la función de convertir medios.",
|
||||
|
@ -1578,5 +1578,62 @@
|
|||
"LabelMaxDaysForNextUp": "Días máximos en «A continuación»",
|
||||
"LabelEnableAudioVbrHelp": "La tasa de bits variable ofrece mejor calidad a la tasa media de bits promedio, pero en algunos raros casos puede causar almacenamiento en búfer y problemas de compatibilidad.",
|
||||
"LabelEnableAudioVbr": "Habilitar codificación VBR de audio",
|
||||
"HeaderPerformance": "Rendimiento"
|
||||
"HeaderPerformance": "Rendimiento",
|
||||
"HeaderEpisodesStatus": "Status de los Episodios",
|
||||
"LabelBackdropScreensaverInterval": "Intervalo del Protector de Pantalla de Fondo",
|
||||
"LabelBackdropScreensaverIntervalHelp": "Intervalo en segundos entre los diferentes fondos cuando se usa el protector de pantalla de fondo.",
|
||||
"LabelEnableLUFSScanHelp": "Permite a los clientes normalizar la reproducción de audio para tener el mismo volumen en todas las pistas. Esto hará que los escaneos de biblioteca tomen más tiempo y utilicen más recursos.",
|
||||
"LabelSyncPlaySettingsSpeedToSyncHelp": "Método de corrección de sincronización que consiste en acelerar la reproducción. La corrección de sincronización debe estar activada.",
|
||||
"LabelSyncPlaySettingsSkipToSyncHelp": "Método de corrección de sincronización que consiste en saltar a la posición de reproducción estimada. La Corrección de Sincronización debe estar activada.",
|
||||
"ListView": "Vista en Lista",
|
||||
"MessageNoItemsAvailable": "No hay elementos disponibles actualmente.",
|
||||
"Bold": "Negrita",
|
||||
"Larger": "Más grande",
|
||||
"LabelSyncPlaySettingsMinDelaySkipToSyncHelp": "Retraso mínimo (en milisegundos) después del cual SkipToSync intentará corregir la posición de reproducción.",
|
||||
"Localization": "Localización",
|
||||
"GoHome": "Ir a Inicio",
|
||||
"LabelSyncPlaySettingsSpeedToSync": "Activar SpeedToSync",
|
||||
"LabelDeveloper": "Desarrollador",
|
||||
"LabelLevel": "Nivel",
|
||||
"LabelDate": "Fecha",
|
||||
"EnableCardLayout": "Mostrar CardBox visual",
|
||||
"LabelMediaDetails": "Detalles de multimedia",
|
||||
"LabelSyncPlaySettingsSkipToSync": "Activar SkipToSync",
|
||||
"LabelSystem": "Sistema",
|
||||
"LogLevel.Debug": "Depurar",
|
||||
"LogLevel.Trace": "Rastreo",
|
||||
"LogLevel.Warning": "Advertencia",
|
||||
"LogLevel.Error": "Error",
|
||||
"LogLevel.Critical": "Crítico",
|
||||
"GridView": "Vista en Cuadrícula",
|
||||
"GetThePlugin": "Obtener el Plugin",
|
||||
"LabelSyncPlayNoGroups": "No hay grupos disponibles",
|
||||
"LabelTextWeight": "Grosor del texto",
|
||||
"LogLevel.None": "Ningún",
|
||||
"LabelSyncPlaySettingsSpeedToSyncDuration": "Duración de SpeedToSync",
|
||||
"LabelSyncPlaySettingsMinDelaySkipToSync": "Retraso mínimo de SkipToSync",
|
||||
"EnableAudioNormalizationHelp": "La normalización de audio añadirá una ganancia constante para mantener la media en el nivel deseado (-18dB).",
|
||||
"LabelEnableLUFSScan": "Activar escaneo LUFS",
|
||||
"MenuOpen": "Abrir Menú",
|
||||
"MenuClose": "Cerrar Menú",
|
||||
"BackdropScreensaver": "Protector de Pantalla de Fondo",
|
||||
"LabelSyncPlaySettingsMaxDelaySpeedToSyncHelp": "Retraso máximo de reproducción (en milisegundos) después del cual SkipToSync se usará en lugar de SpeedToSync.",
|
||||
"LogoScreensaver": "Protector de Pantalla de Logo",
|
||||
"MessageNoFavoritesAvailable": "No hay favoritos disponibles actualmente.",
|
||||
"LabelSyncPlaySettingsSpeedToSyncDurationHelp": "Cantidad de milisegundos que SpeedToSync utilizará para corregir la posición de reproducción.",
|
||||
"HeaderConfirmRepositoryInstallation": "Confirma la instalación del repositorio de plugins",
|
||||
"EnableAudioNormalization": "Normalización de audio",
|
||||
"LogLevel.Information": "Información",
|
||||
"AllowCollectionManagement": "Permitir a este usuario gestionar colecciones",
|
||||
"Lyricist": "Letrista",
|
||||
"AllowSegmentDeletion": "Borrar segmentos",
|
||||
"AllowSegmentDeletionHelp": "Borrar segmentos viejos que ya hayan sido enviados al cliente. Esto evita tener que guardar la totalidad del archivo transcodificado en el disco. Funciona solamente si la opción \"regular transcodificaciones\" también está activada. Si se experimentan problemas en la reproducción, desactivar esta opción.",
|
||||
"LabelThrottleDelaySeconds": "Limitar después de",
|
||||
"LabelThrottleDelaySecondsHelp": "Duración en segundos después de la cual la transcodificación será limitada. Debe ser suficiente para permitirle al cliente mantener un buffer adecuado. Solo funciona si la limitación de la transcodificación está activada.",
|
||||
"LabelSegmentKeepSeconds": "Tiempo de retención de segmentos",
|
||||
"LabelSegmentKeepSecondsHelp": "Duración en segundos que se retendrán los segmentos antes de ser sobreescritos. Debe ser mayor que \"Limitar después de\". Solo funciona si la opción \"Borrar segmentos\" está activada.",
|
||||
"LabelParallelImageEncodingLimit": "Límite de codificaciones paralelas de imágenes",
|
||||
"LabelParallelImageEncodingLimitHelp": "Cantidad máxima de codificaciones de imágenes que pueden ejecutarse en paralelo. Al establecer 0 se elegirá un límite basado en las especificaciones de su sistema.",
|
||||
"MediaInfoTitle": "Título",
|
||||
"HeaderGuestCast": "Estrellas Invitadas"
|
||||
}
|
||||
|
|
216
src/strings/fo.json
Normal file
216
src/strings/fo.json
Normal file
|
@ -0,0 +1,216 @@
|
|||
{
|
||||
"AccessRestrictedTryAgainLater": "Atgongd er avmarkað. Vinaliga royn aftur seinni.",
|
||||
"Actor": "Sjónleikari",
|
||||
"Add": "Legg afturat",
|
||||
"AddedOnValue": "{0} lagt afturat",
|
||||
"AddToCollection": "Koyr í samling",
|
||||
"AddToFavorites": "Legg til yndislistan",
|
||||
"AddToPlaylist": "Legg til spælilistan",
|
||||
"Alerts": "Ávaringar",
|
||||
"All": "Øll",
|
||||
"AllEpisodes": "Allir partar",
|
||||
"AllLanguages": "Øll tungumál",
|
||||
"LabelTonemappingMode": "Tónaavmyndingarháttur",
|
||||
"HearingImpairedShort": "Hoyriveik/SDH",
|
||||
"MachineTranslated": "Maskin týðing",
|
||||
"AiTranslated": "Vitlíkistýðing",
|
||||
"AllowAv1Encoding": "Loyva koding í AV1 bygnaði",
|
||||
"LabelIsHearingImpaired": "Til hoyriveik (SDH)",
|
||||
"Unknown": "Ókend",
|
||||
"TonemappingModeHelp": "Vel tónaavmyndingarháttin. Um tú verður fyri útblástum hálýsingum, royn so heldur RGB-støðuna.",
|
||||
"Unreleased": "Ikki latið út enn",
|
||||
"AlbumArtist": "Album Listafólk",
|
||||
"AllChannels": "Allar rásir",
|
||||
"AllComplexFormats": "Allar Kompleksu Formatir (ASS, SSA, VobSub, PGS, SUB, IDX, ...)",
|
||||
"Directors": "Leikstjórar",
|
||||
"AgeValue": "({0} ára gamalt)",
|
||||
"AllLibraries": "Øll søvn",
|
||||
"Artist": "Listafólk",
|
||||
"Artists": "Listafólk",
|
||||
"Books": "Bøkur",
|
||||
"Composer": "Tónaskald",
|
||||
"DailyAt": "Dagligani kl. {0}",
|
||||
"DashboardVersionNumber": "Útgáva: {0}",
|
||||
"DeathDateValue": "Deyð(ur): {0}",
|
||||
"Digital": "Talgilt",
|
||||
"Director": "Leikstjóri",
|
||||
"Friday": "Fríggjadag",
|
||||
"HeaderAdmin": "Umsiting",
|
||||
"HeaderDevices": "Eindir",
|
||||
"HeaderError": "Feilur",
|
||||
"HeaderForKids": "Fyri Børn",
|
||||
"AllowSegmentDeletion": "Strika partar",
|
||||
"LabelThrottleDelaySeconds": "Kyrkja",
|
||||
"AllowMediaConversion": "miðla",
|
||||
"AllowOnTheFlySubtitleExtraction": "undirteksta",
|
||||
"AllowCollectionManagement": "brúkara",
|
||||
"AllowFfmpegThrottling": "Kyrkja",
|
||||
"AllowFfmpegThrottlingHelp": "umkoding avspæling avspælingar",
|
||||
"AllowSegmentDeletionHelp": "avspælingar",
|
||||
"LabelSegmentKeepSecondsHelp": "Kyrkja parta.",
|
||||
"AllowHWTranscodingHelp": "avkoda ambætarinum",
|
||||
"AllowMediaConversionHelp": "miðla",
|
||||
"AllowOnTheFlySubtitleExtractionHelp": "Íkervnir undirtekstir avspæling íkervnar undirtekstir",
|
||||
"AllowRemoteAccess": "ambætaran",
|
||||
"HeaderPassword": "Loyniorð",
|
||||
"HeaderLibraries": "Søvn",
|
||||
"HeaderParentalRatings": "Aldursmark",
|
||||
"HeaderSecondsValue": "{0} sekund",
|
||||
"HeaderSendMessage": "Send boð",
|
||||
"Kids": "Børn",
|
||||
"LabelArtists": "Listafólk",
|
||||
"LabelCountry": "Land",
|
||||
"LabelDeveloper": "Mennari",
|
||||
"Absolute": "Absolut",
|
||||
"AddToPlayQueue": "Legg til spæl bíðirøð",
|
||||
"AirDate": "Útgávu ár",
|
||||
"Aired": "Útgivið",
|
||||
"Album": "Album",
|
||||
"Albums": "Album",
|
||||
"LabelSegmentKeepSeconds": "Tíð at halda petti",
|
||||
"AroundTime": "Umleið {0}",
|
||||
"Ascending": "Hækkandi",
|
||||
"SearchForMissingMetadata": "Leita eftir manglandi metadata",
|
||||
"SearchForSubtitles": "Leita eftir undirtekstið",
|
||||
"SearchResults": "Leitiúrslit",
|
||||
"SelectServer": "Vel ambætara",
|
||||
"SendMessage": "Send boð",
|
||||
"Season": "Sesong",
|
||||
"SeriesDisplayOrderHelp": "Raða partar eftir dato, DVD ordan ella absolut nummerering.",
|
||||
"ServerNameIsRestarting": "Ambætarin á {0} endurbyrjar.",
|
||||
"ServerNameIsShuttingDown": "Ambætarin á {0} sløknar.",
|
||||
"ServerUpdateNeeded": "Hesin ambætarin má dagførast. Fyri at heinta nýggjastu útgávuna, vinarliga vitja {0}.",
|
||||
"Share": "Býta",
|
||||
"ShowIndicatorsFor": "Vís indikator fyri",
|
||||
"ShowLess": "Vís minni",
|
||||
"ShowMore": "Vís meiri",
|
||||
"ShowParentImages": "Vís røð myndir",
|
||||
"Shuffle": "Blanda",
|
||||
"Shows": "Røðir",
|
||||
"ShowTitle": "Vís heitið",
|
||||
"Small": "Lítið",
|
||||
"SmallCaps": "Lítlir stavir",
|
||||
"Smaller": "Minni",
|
||||
"Songs": "Sangir",
|
||||
"Sort": "Skipa",
|
||||
"SpecialFeatures": "Serstøk eyðkenni",
|
||||
"StopRecording": "Steðga upptøku",
|
||||
"Studio": "Filmsfelag",
|
||||
"Studios": "Filmsfeløg",
|
||||
"Subtitle": "Undirtekstur",
|
||||
"SubtitleCyan": "Blágrønur",
|
||||
"SubtitleGreen": "Grønt",
|
||||
"SubtitleOffset": "Undirtekstur offset",
|
||||
"SubtitleRed": "Reytt",
|
||||
"Subtitles": "Undirtekstur",
|
||||
"Suggestions": "Uppskot",
|
||||
"Sync": "Synkronisera",
|
||||
"SyncPlayGroupDefaultTitle": "{0}'sa bólkur",
|
||||
"TabAdvanced": "Framkomin",
|
||||
"TabCatalog": "Skrá",
|
||||
"TabDashboard": "Kunningarbretti",
|
||||
"TabDirectPlay": "Beinleiðis avspæling",
|
||||
"TabLatest": "Lagt afturat fyri stuttum",
|
||||
"TabLogs": "Gerðalistar",
|
||||
"TabNetworking": "Net",
|
||||
"TabNfoSettings": "NFO stillingar",
|
||||
"TabPlugins": "Ískoytisforrit",
|
||||
"TabProfiles": "Vangamyndir",
|
||||
"TabRepositories": "Goymslur",
|
||||
"TabServer": "Ambætari",
|
||||
"TabStreaming": "Stroyming",
|
||||
"TabUpcoming": "Komandi",
|
||||
"Tags": "Frámerkir",
|
||||
"TellUsAboutYourself": "Fortel um teg sjálvan",
|
||||
"ThemeSongs": "Tema sangir",
|
||||
"ThemeVideos": "Tema sjónbond",
|
||||
"ThumbCard": "Tummlakort",
|
||||
"TitleHardwareAcceleration": "Tólbúnað ferðøking",
|
||||
"TitleHostingSettings": "Hýsingastillingar",
|
||||
"TrackCount": "{0} spor",
|
||||
"Trailers": "Forfilmar",
|
||||
"Tuesday": "Týsdag",
|
||||
"TV": "Sjónvarp",
|
||||
"TypeOptionPluralAudio": "Ljóð",
|
||||
"TypeOptionPluralBoxSet": "Boks sett",
|
||||
"TypeOptionPluralMusicAlbum": "Tónleika album",
|
||||
"TypeOptionPluralMusicVideo": "Tónleikasjónbond",
|
||||
"TypeOptionPluralSeason": "Sesongir",
|
||||
"TypeOptionPluralVideo": "Sjónbond",
|
||||
"Typewriter": "Skrivimaskina",
|
||||
"Uniform": "Einsháttað",
|
||||
"Unmute": "Skrúva ljóðið upp",
|
||||
"Unrated": "Eingin meting",
|
||||
"Up": "Upp",
|
||||
"ValueAudioCodec": "Ljóð codec: {0}",
|
||||
"ValueCodec": "Codec: {0}",
|
||||
"ValueContainer": "Bingja: {0}",
|
||||
"ValueEpisodeCount": "{0} partar",
|
||||
"ValueMinutes": "{0} min",
|
||||
"ValueMovieCount": "{0} filmar",
|
||||
"ValueOneEpisode": "1 partur",
|
||||
"ValueOneMovie": "1 filmur",
|
||||
"ValueOneMusicVideo": "1 tónleika sjónband",
|
||||
"ValueOneSeries": "1 røð",
|
||||
"ValueSeconds": "{0} sekund",
|
||||
"ValueSeriesCount": "{0} røðir",
|
||||
"Sports": "Ítróttur",
|
||||
"Smart": "Smart",
|
||||
"ShowYear": "Vís árið",
|
||||
"SimultaneousConnectionLimitHelp": "Mest loyvda antalið av samstundis stroymingum. Skriva 0 fyri einki hámark.",
|
||||
"Settings": "Stillingar",
|
||||
"SettingsSaved": "Stillingar eru goymdar.",
|
||||
"SelectAdminUsername": "Vinarliga vel eitt brúkaranavn til fyrisitara kontu.",
|
||||
"Series": "Røð",
|
||||
"SeriesCancelled": "Røðin er steðga.",
|
||||
"Search": "Leita",
|
||||
"SearchForCollectionInternetMetadata": "Leita eftir list og metadata á alnótini",
|
||||
"SeriesSettings": "Røð stillingar",
|
||||
"SeriesYearToPresent": "{0} - Núverandi",
|
||||
"ServerRestartNeededAfterPluginInstall": "Jellyfin má endurbyrjast, aftaná at eitt ískoytisforrit er lagt inn.",
|
||||
"ShowAdvancedSettings": "Vís framkomnar stillingar",
|
||||
"StopPlayback": "Steðga avspæling",
|
||||
"SubtitleGray": "Grátt",
|
||||
"SubtitleLightGray": "Ljósagráður",
|
||||
"SubtitleMagenta": "Viólreyður",
|
||||
"AllowedRemoteAddressesHelp": "Komma býttur listið av IP addressum ella IP/netmask inngangur fyri netverk, ið verða loyvd at fjarbinda. Um hesin teigur er blankur, so eru allar fjar addressur loyvdar.",
|
||||
"SubtitleWhite": "Hvítt",
|
||||
"Arranger": "Fyrireikari",
|
||||
"SubtitleYellow": "Gult",
|
||||
"AskAdminToCreateLibrary": "Bið ein fyrisitari upprætta eitt savn.",
|
||||
"TabAccess": "Atgongd",
|
||||
"TypeOptionPluralEpisode": "Partar",
|
||||
"TabContainers": "Kassar",
|
||||
"TitlePlayback": "Avspæling",
|
||||
"TabMusic": "Tónleikur",
|
||||
"TabOther": "Annað",
|
||||
"TagsValue": "Frámerkir: {0}",
|
||||
"TabMyPlugins": "Míni ískoytisforrit",
|
||||
"TabParentalControl": "Foreldra ræði",
|
||||
"Sunday": "Sunnudag",
|
||||
"SubtitleBlack": "Svart",
|
||||
"TabResponses": "Svar",
|
||||
"SubtitleBlue": "Blátt",
|
||||
"TextSent": "Tekst sent.",
|
||||
"TheseSettingsAffectSubtitlesOnThisDevice": "Hesir stillingar ávirka undirteksir á hesari eindini",
|
||||
"Thumb": "Tummil",
|
||||
"Thursday": "Hósdagur",
|
||||
"Track": "Spor",
|
||||
"Transcoding": "Umkodning",
|
||||
"TypeOptionPluralBook": "Bøkur",
|
||||
"TypeOptionPluralMovie": "Filmar",
|
||||
"TypeOptionPluralMusicArtist": "Tónleikarar",
|
||||
"TypeOptionPluralSeries": "Sjónvarpsrøðir",
|
||||
"UnknownError": "Ein ókendur feilur hendi.",
|
||||
"Unplayed": "Ikki spældur",
|
||||
"Upload": "Send upp",
|
||||
"UseEpisodeImagesInNextUp": "Brúka partamyndir í 'Komandi' og 'Hyggj víðari' teigum",
|
||||
"UserMenu": "Brúkara valmynd",
|
||||
"ValueAlbumCount": "{0} album",
|
||||
"ValueConditions": "Treytir: {0}",
|
||||
"ValueDiscNumber": "Fløga {0}",
|
||||
"ValueMusicVideoCount": "{0} tónleika sjónbond",
|
||||
"ValueOneAlbum": "1 album",
|
||||
"ValueOneSong": "1 sangur",
|
||||
"ValueSongCount": "{0} sangir"
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue