1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Merge branch 'fix-3916' of https://github.com/Kaysera/jellyfin-web into fix-3916

This commit is contained in:
Guillermo Fernandez 2023-12-18 22:40:22 +01:00
commit 273db3ac79
134 changed files with 5097 additions and 2182 deletions

View file

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

View file

@ -66,6 +66,7 @@ module.exports = {
'no-unused-expressions': ['off'],
'@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'],

65
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,65 @@
name: Build
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master, release* ]
pull_request:
branches: [ master, release* ]
workflow_dispatch:
jobs:
run-build-prod:
name: Run production build
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup node environment
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
with:
node-version: 20
check-latest: true
cache: npm
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run a production build
run: npm run build:production
- name: Upload artifact
uses: actions/upload-artifact@v3.1.3
with:
name: jellyfin-web__prod
path: |
dist
pr_context:
name: Save PR context as artifact
if: ${{ always() && !cancelled() && github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
needs:
- run-build-prod
steps:
- name: Save PR context
env:
PR_NUMBER: ${{ github.event.number }}
PR_SHA: ${{ github.sha }}
run: |
echo $PR_NUMBER > PR_number
echo $PR_SHA > PR_sha
- name: Upload PR number as artifact
uses: actions/upload-artifact@v3.1.3
with:
name: PR_context
path: |
PR_number
PR_sha

View file

@ -1,31 +1,34 @@
name: "CodeQL"
name: CodeQL
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
branches: [ master, release* ]
pull_request:
branches: [ master ]
branches: [ master, release* ]
schedule:
- cron: '30 7 * * 6'
jobs:
analyze:
name: Analyze
codeql:
name: Run CodeQL
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Initialize CodeQL
uses: github/codeql-action/init@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3
with:
languages: ${{ matrix.language }}
languages: javascript
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3

65
.github/workflows/job-messages.yml vendored Normal file
View file

@ -0,0 +1,65 @@
name: Job messages
on:
workflow_call:
inputs:
branch:
required: false
type: string
commit:
required: true
type: string
preview_url:
required: false
type: string
build_workflow_run_id:
required: false
type: number
commenting_workflow_run_id:
required: true
type: string
in_progress:
required: true
type: boolean
outputs:
msg:
description: The composed message
value: ${{ jobs.msg.outputs.msg }}
marker:
description: Hidden marker to detect PR comments composed by the bot
value: "CFPages-deployment"
jobs:
msg:
name: Deployment status
runs-on: ubuntu-latest
outputs:
msg: ${{ env.msg }}
steps:
- name: Compose message
if: ${{ always() }}
id: compose
env:
COMMIT: ${{ inputs.commit }}
PREVIEW_URL: ${{ inputs.preview_url != '' && (inputs.branch != 'master' && inputs.preview_url || format('https://jellyfin-web.pages.dev ({0})', inputs.preview_url)) || 'Not available' }}
DEPLOY_STATUS: ${{ inputs.in_progress && '🔄 Deploying...' || (inputs.preview_url != '' && '✅ Deployed!' || '❌ Failure. Check workflow logs for details') }}
DEPLOYMENT_TYPE: ${{ inputs.branch != 'master' && '🔀 Preview' || '⚙️ Production' }}
BUILD_WORKFLOW_RUN: ${{ !inputs.in_progress && format('**[View build logs](https://github.com/{0}/actions/runs/{1})**', 'jellyfin/jellyfin-web', inputs.build_workflow_run_id) || '' }}
COMMENTING_WORKFLOW_RUN: ${{ format('**[View bot logs](https://github.com/{0}/actions/runs/{1})**', 'jellyfin/jellyfin-web', inputs.commenting_workflow_run_id) }}
# EOF is needed for multiline environment variables in a GitHub Actions context
run: |
echo "## Cloudflare Pages deployment" > $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| **Latest commit** | <code>${COMMIT::7}</code> |" >> $GITHUB_STEP_SUMMARY
echo "|------------------------- |:----------------------------: |" >> $GITHUB_STEP_SUMMARY
echo "| **Status** | $DEPLOY_STATUS |" >> $GITHUB_STEP_SUMMARY
echo "| **Preview URL** | $PREVIEW_URL |" >> $GITHUB_STEP_SUMMARY
echo "| **Type** | $DEPLOYMENT_TYPE |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "$BUILD_WORKFLOW_RUN" >> $GITHUB_STEP_SUMMARY
echo "$COMMENTING_WORKFLOW_RUN" >> $GITHUB_STEP_SUMMARY
COMPOSED_MSG=$(cat $GITHUB_STEP_SUMMARY)
echo "msg<<EOF" >> $GITHUB_ENV
echo "$COMPOSED_MSG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV

97
.github/workflows/publish.yml vendored Normal file
View file

@ -0,0 +1,97 @@
name: Publish
on:
workflow_run:
workflows:
- Build
types:
- completed
jobs:
publish:
permissions:
contents: read
deployments: write
name: Deploy to Cloudflare Pages
runs-on: ubuntu-latest
outputs:
url: ${{ steps.cf.outputs.url }}
steps:
- name: Download workflow artifact
uses: dawidd6/action-download-artifact@v2.27.0
with:
run_id: ${{ github.event.workflow_run.id }}
name: jellyfin-web__prod
path: dist
- name: Publish
id: cf
uses: cloudflare/pages-action@1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: jellyfin-web
branch: ${{ github.event.workflow_run.head_branch }}
directory: dist
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
pr-context:
name: PR context
if: ${{ always() && github.event.workflow_run.event == 'pull_request' }}
runs-on: ubuntu-latest
outputs:
commit: ${{ env.pr_sha }}
pr_number: ${{ env.pr_number }}
steps:
- name: Get PR context
uses: dawidd6/action-download-artifact@v2.27.0
id: pr_context
with:
run_id: ${{ github.event.workflow_run.id }}
name: PR_context
- name: Set PR context environment variables
if: ${{ steps.pr_context.conclusion == 'success' }}
run: |
echo "pr_number=$(cat PR_number)" >> $GITHUB_ENV
echo "pr_sha=$(cat PR_sha)" >> $GITHUB_ENV
compose-comment:
name: Compose comment
if: ${{ always() }}
uses: ./.github/workflows/job-messages.yml
needs:
- publish
- pr-context
with:
branch: ${{ github.event.workflow_run.head_branch }}
commit: ${{ needs.pr-context.outputs.commit != '' && needs.pr-context.outputs.commit || github.event.workflow_run.head_sha }}
preview_url: ${{ needs.publish.outputs.url }}
build_workflow_run_id: ${{ github.event.workflow_run.id }}
commenting_workflow_run_id: ${{ github.run_id }}
in_progress: false
comment-status:
name: Create comment status
if: |
always() &&
github.event.workflow_run.event == 'pull_request' &&
needs.pr-context.outputs.pr_number != ''
runs-on: ubuntu-latest
needs:
- compose-comment
- pr-context
steps:
- name: Update job summary in PR comment
uses: thollander/actions-comment-pull-request@v2.4.2
with:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
message: ${{ needs.compose-comment.outputs.msg }}
pr_number: ${{ needs.pr-context.outputs.pr_number }}
comment_tag: ${{ needs.compose-comment.outputs.marker }}
mode: recreate

View file

@ -1,4 +1,8 @@
name: Lint
name: Quality checks
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
on:
push:
@ -99,3 +103,45 @@ jobs:
- name: Run stylelint
run: npm run stylelint:scss
run-tsc:
name: Run TypeScript build check
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup node environment
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
with:
node-version: 20
check-latest: true
cache: npm
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run tsc
run: npm run build:check
run-test:
name: Run tests
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup node environment
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
with:
node-version: 20
check-latest: true
cache: npm
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run test suite
run: npm run test

View file

@ -1,29 +0,0 @@
name: TypeScript Build Check
on:
push:
branches: [ master, release* ]
pull_request:
branches: [ master, release* ]
jobs:
tsc:
name: Run TypeScript build check
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup node environment
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
with:
node-version: 20
check-latest: true
cache: npm
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run tsc
run: npm run build:check

View file

@ -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

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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\""

View file

@ -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 />
</>
);
};

View file

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

View file

@ -0,0 +1,108 @@
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import { useTheme } from '@mui/material/styles';
import React, { FC, useCallback, useEffect, useState } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import AppBody from 'components/AppBody';
import AppToolbar from 'components/toolbar/AppToolbar';
import ElevationScroll from 'components/ElevationScroll';
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
import { useApi } from 'hooks/useApi';
import { useLocalStorage } from 'hooks/useLocalStorage';
import AppDrawer from './components/drawer/AppDrawer';
import './AppOverrides.scss';
interface AppLayoutProps {
drawerlessPaths: string[]
}
interface DashboardAppSettings {
isDrawerPinned: boolean
}
const DEFAULT_APP_SETTINGS: DashboardAppSettings = {
isDrawerPinned: false
};
const AppLayout: FC<AppLayoutProps> = ({
drawerlessPaths
}) => {
const [ appSettings, setAppSettings ] = useLocalStorage<DashboardAppSettings>('DashboardAppSettings', DEFAULT_APP_SETTINGS);
const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned);
const location = useLocation();
const theme = useTheme();
const { user } = useApi();
const isDrawerAvailable = !drawerlessPaths.some(path => location.pathname.startsWith(`/${path}`));
const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user);
useEffect(() => {
if (isDrawerActive !== appSettings.isDrawerPinned) {
setAppSettings({
...appSettings,
isDrawerPinned: isDrawerActive
});
}
}, [ appSettings, isDrawerActive, setAppSettings ]);
const onToggleDrawer = useCallback(() => {
setIsDrawerActive(!isDrawerActive);
}, [ isDrawerActive, setIsDrawerActive ]);
return (
<Box sx={{ display: 'flex' }}>
<ElevationScroll elevate={isDrawerOpen}>
<AppBar
position='fixed'
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.drawer + 1 }}
>
<AppToolbar
isDrawerAvailable={isDrawerAvailable}
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onToggleDrawer}
/>
</AppBar>
</ElevationScroll>
<AppDrawer
open={isDrawerOpen}
onClose={onToggleDrawer}
onOpen={onToggleDrawer}
/>
<Box
component='main'
sx={{
width: '100%',
flexGrow: 1,
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
marginLeft: 0,
...(isDrawerAvailable && {
marginLeft: {
sm: `-${DRAWER_WIDTH}px`
}
}),
...(isDrawerActive && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen
}),
marginLeft: 0
})
}}
>
<AppBody>
<Outlet />
</AppBody>
</Box>
</Box>
);
};
export default AppLayout;

View file

@ -0,0 +1,22 @@
// Default MUI breakpoints
// https://mui.com/material-ui/customization/breakpoints/#default-breakpoints
$mui-bp-sm: 600px;
$mui-bp-md: 900px;
$mui-bp-lg: 1200px;
$mui-bp-xl: 1536px;
// Fix dashboard pages layout to work with drawer
.dashboardDocument {
.mainAnimatedPage {
position: relative;
}
.skinBody {
position: unset !important;
}
// Fix the padding of dashboard pages
.content-primary.content-primary {
padding-top: 3.25rem !important;
}
}

View file

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

View file

@ -19,10 +19,10 @@ import ListItemLink from 'components/ListItemLink';
import globalize from 'scripts/globalize';
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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

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

View file

@ -1,170 +1,164 @@
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
import type { LegacyRoute } from 'components/router/LegacyRoute';
export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
{
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'

View file

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

View file

@ -19,9 +19,9 @@ import { parseISO8601Date, toLocaleDateString, toLocaleTimeString } from 'script
import globalize from 'scripts/globalize';
import { 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>

View file

@ -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>`

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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>
);
};

View file

@ -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;
}

View file

@ -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;

View file

@ -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>

View file

@ -0,0 +1,34 @@
import React, { FC, useCallback } from 'react';
import { IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import globalize from 'scripts/globalize';
const NewCollectionButton: FC = () => {
const showCollectionEditor = useCallback(() => {
import('components/collectionEditor/collectionEditor').then(
({ default: CollectionEditor }) => {
const serverId = window.ApiClient.serverId();
const collectionEditor = new CollectionEditor();
collectionEditor.show({
items: [],
serverId: serverId
}).catch(() => {
// closed collection editor
});
}).catch(err => {
console.error('[NewCollection] failed to load collection editor', err);
});
}, []);
return (
<IconButton
title={globalize.translate('Add')}
className='paper-icon-button-light btnNewCollection autoSize'
onClick={showCollectionEditor}
>
<AddIcon />
</IconButton>
);
};
export default NewCollectionButton;

View file

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

View file

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

View file

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

View file

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

View file

@ -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 />

View file

@ -1,12 +0,0 @@
import { AsyncRoute, AsyncRouteType } from 'components/router/AsyncRoute';
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'dashboard/activity', page: 'dashboard/activity', type: AsyncRouteType.Experimental },
{ path: 'notificationsettings.html', page: 'dashboard/notifications' },
{ path: 'usernew.html', page: 'user/usernew' },
{ path: 'userprofiles.html', page: 'user/userprofiles' },
{ path: 'useredit.html', page: 'user/useredit' },
{ path: 'userlibraryaccess.html', page: 'user/userlibraryaccess' },
{ path: 'userparentalcontrol.html', page: 'user/userparentalcontrol' },
{ path: 'userpassword.html', page: 'user/userpassword' }
];

View file

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

View file

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

View file

@ -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>
);

View file

@ -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' }
];

View file

@ -1,11 +0,0 @@
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'notificationsettings.html', page: 'dashboard/notifications' },
{ path: 'usernew.html', page: 'user/usernew' },
{ path: 'userprofiles.html', page: 'user/userprofiles' },
{ path: 'useredit.html', page: 'user/useredit' },
{ path: 'userlibraryaccess.html', page: 'user/userlibraryaccess' },
{ path: 'userparentalcontrol.html', page: 'user/userparentalcontrol' },
{ path: 'userpassword.html', page: 'user/userpassword' }
];

View file

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

View file

@ -1,179 +0,0 @@
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
{
path: 'dashboard.html',
pageProps: {
controller: 'dashboard/dashboard',
view: 'dashboard/dashboard.html'
}
}, {
path: 'dashboardgeneral.html',
pageProps: {
controller: 'dashboard/general',
view: 'dashboard/general.html'
}
}, {
path: 'networking.html',
pageProps: {
controller: 'dashboard/networking',
view: 'dashboard/networking.html'
}
}, {
path: 'devices.html',
pageProps: {
controller: 'dashboard/devices/devices',
view: 'dashboard/devices/devices.html'
}
}, {
path: 'device.html',
pageProps: {
controller: 'dashboard/devices/device',
view: 'dashboard/devices/device.html'
}
}, {
path: 'dlnaprofile.html',
pageProps: {
controller: 'dashboard/dlna/profile',
view: 'dashboard/dlna/profile.html'
}
}, {
path: 'dlnaprofiles.html',
pageProps: {
controller: 'dashboard/dlna/profiles',
view: 'dashboard/dlna/profiles.html'
}
}, {
path: 'dlnasettings.html',
pageProps: {
controller: 'dashboard/dlna/settings',
view: 'dashboard/dlna/settings.html'
}
}, {
path: 'addplugin.html',
pageProps: {
controller: 'dashboard/plugins/add/index',
view: 'dashboard/plugins/add/index.html'
}
}, {
path: 'library.html',
pageProps: {
controller: 'dashboard/library',
view: 'dashboard/library.html'
}
}, {
path: 'librarydisplay.html',
pageProps: {
controller: 'dashboard/librarydisplay',
view: 'dashboard/librarydisplay.html'
}
}, {
path: 'edititemmetadata.html',
pageProps: {
controller: 'edititemmetadata',
view: 'edititemmetadata.html'
}
}, {
path: 'encodingsettings.html',
pageProps: {
controller: 'dashboard/encodingsettings',
view: 'dashboard/encodingsettings.html'
}
}, {
path: 'log.html',
pageProps: {
controller: 'dashboard/logs',
view: 'dashboard/logs.html'
}
}, {
path: 'metadataimages.html',
pageProps: {
controller: 'dashboard/metadataImages',
view: 'dashboard/metadataimages.html'
}
}, {
path: 'metadatanfo.html',
pageProps: {
controller: 'dashboard/metadatanfo',
view: 'dashboard/metadatanfo.html'
}
}, {
path: 'playbackconfiguration.html',
pageProps: {
controller: 'dashboard/playback',
view: 'dashboard/playback.html'
}
}, {
path: 'availableplugins.html',
pageProps: {
controller: 'dashboard/plugins/available/index',
view: 'dashboard/plugins/available/index.html'
}
}, {
path: 'repositories.html',
pageProps: {
controller: 'dashboard/plugins/repositories/index',
view: 'dashboard/plugins/repositories/index.html'
}
}, {
path: 'livetvguideprovider.html',
pageProps: {
controller: 'livetvguideprovider',
view: 'livetvguideprovider.html'
}
}, {
path: 'livetvsettings.html',
pageProps: {
controller: 'livetvsettings',
view: 'livetvsettings.html'
}
}, {
path: 'livetvstatus.html',
pageProps: {
controller: 'livetvstatus',
view: 'livetvstatus.html'
}
}, {
path: 'livetvtuner.html',
pageProps: {
controller: 'livetvtuner',
view: 'livetvtuner.html'
}
}, {
path: 'installedplugins.html',
pageProps: {
controller: 'dashboard/plugins/installed/index',
view: 'dashboard/plugins/installed/index.html'
}
}, {
path: 'scheduledtask.html',
pageProps: {
controller: 'dashboard/scheduledtasks/scheduledtask',
view: 'dashboard/scheduledtasks/scheduledtask.html'
}
}, {
path: 'scheduledtasks.html',
pageProps: {
controller: 'dashboard/scheduledtasks/scheduledtasks',
view: 'dashboard/scheduledtasks/scheduledtasks.html'
}
}, {
path: 'dashboard/activity',
pageProps: {
controller: 'dashboard/serveractivity',
view: 'dashboard/serveractivity.html'
}
}, {
path: 'apikeys.html',
pageProps: {
controller: 'dashboard/apikeys',
view: 'dashboard/apikeys.html'
}
}, {
path: 'streamingsettings.html',
pageProps: {
view: 'dashboard/streaming.html',
controller: 'dashboard/streaming'
}
}
];

View file

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

View file

@ -5,33 +5,39 @@
*/
import escapeHtml from 'escape-html';
import datetime from '../../scripts/datetime';
import imageLoader from '../images/imageLoader';
import itemHelper from '../itemHelper';
import cardBuilderUtils from './cardBuilderUtils';
import browser from 'scripts/browser';
import datetime from 'scripts/datetime';
import dom from 'scripts/dom';
import globalize from 'scripts/globalize';
import imageHelper from 'scripts/imagehelper';
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
import { randomInt } from 'utils/number';
import focusManager from '../focusManager';
import imageLoader from '../images/imageLoader';
import indicators from '../indicators/indicators';
import globalize from '../../scripts/globalize';
import itemHelper from '../itemHelper';
import layoutManager from '../layoutManager';
import dom from '../../scripts/dom';
import browser from '../../scripts/browser';
import { playbackManager } from '../playback/playbackmanager';
import itemShortcuts from '../shortcuts';
import imageHelper from '../../scripts/imagehelper';
import { randomInt } from '../../utils/number.ts';
import './card.scss';
import '../../elements/emby-button/paper-icon-button-light';
import '../guide/programs.scss';
import ServerConnections from '../ServerConnections';
import { appRouter } from '../router/appRouter';
import ServerConnections from '../ServerConnections';
import itemShortcuts from '../shortcuts';
import 'elements/emby-button/paper-icon-button-light';
import './card.scss';
import '../guide/programs.scss';
const enableFocusTransform = !browser.slow && !browser.edge;
/**
* 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];
@ -42,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) {
@ -271,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';
@ -301,16 +96,16 @@ function setCardData(items, options) {
options.shape = 'banner';
options.coverImage = true;
} else if (primaryImageAspectRatio >= 1.33) {
options.shape = requestedShape === 'autooverflow' ? 'overflowBackdrop' : 'backdrop';
options.shape = getBackdropShape(requestedShape === 'autooverflow');
} else if (primaryImageAspectRatio > 0.71) {
options.shape = requestedShape === 'autooverflow' ? 'overflowSquare' : 'square';
options.shape = getSquareShape(requestedShape === 'autooverflow');
} else {
options.shape = requestedShape === 'autooverflow' ? 'overflowPortrait' : 'portrait';
options.shape = getPortraitShape(requestedShape === 'autooverflow');
}
}
if (!options.shape) {
options.shape = options.defaultShape || (requestedShape === 'autooverflow' ? 'overflowSquare' : 'square');
options.shape = options.defaultShape || getSquareShape(requestedShape === 'autooverflow');
}
}
@ -318,7 +113,7 @@ function setCardData(items, options) {
options.preferThumb = options.shape === 'backdrop' || options.shape === 'overflowBackdrop';
}
options.uiAspect = getDesiredAspect(options.shape);
options.uiAspect = cardBuilderUtils.getDesiredAspect(options.shape);
options.primaryImageAspectRatio = primaryImageAspectRatio;
if (!options.width && options.widths) {
@ -343,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;
@ -461,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;
@ -509,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;
@ -641,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;
@ -664,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 = '';
@ -717,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 = '';
@ -758,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 = '';
@ -1000,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);
@ -1026,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;
@ -1108,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;
@ -1118,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';
@ -1440,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 = '';
@ -1482,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>';
@ -1530,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)) {
@ -1574,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;
@ -1597,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';
@ -1676,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 + '"]');
@ -1689,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 + '"]');
@ -1709,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 + '"]');
@ -1726,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 + '"]');

View file

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

View file

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

View file

@ -10,28 +10,28 @@ const createLinkElement = (activeTab: string) => ({
is="emby-linkbutton"
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>`
});

View file

@ -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>`

View file

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

View file

@ -1,39 +1,25 @@
import escapeHtml from 'escape-html';
import cardBuilder from '../cardbuilder/cardBuilder';
import layoutManager from '../layoutManager';
import imageLoader from '../images/imageLoader';
import globalize from '../../scripts/globalize';
import { appRouter } from '../router/appRouter';
import imageHelper from '../../scripts/imagehelper';
import '../../elements/emby-button/paper-icon-button-light';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
import '../../elements/emby-scroller/emby-scroller';
import '../../elements/emby-button/emby-button';
import globalize from 'scripts/globalize';
import { DEFAULT_SECTIONS, HomeSectionType } from 'types/homeSectionType';
import Dashboard from 'utils/dashboard';
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';
import 'elements/emby-scroller/emby-scroller';
import 'elements/emby-button/emby-button';
import './homesections.scss';
import Dashboard from '../../utils/dashboard';
import ServerConnections from '../ServerConnections';
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) {
@ -94,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');
});
}
}
@ -133,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();
}
@ -169,583 +162,11 @@ function enableScrollX() {
return true;
}
function getSquareShape() {
return enableScrollX() ? 'overflowSquare' : 'square';
}
function getThumbShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
function getPortraitShape() {
return enableScrollX() ? 'overflowPortrait' : 'portrait';
}
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();
} else if (viewType === 'music' || viewType === 'homevideos') {
shape = getSquareShape();
} else {
shape = getThumbShape();
}
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: getThumbShape(),
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() : getThumbShape(),
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: getThumbShape(),
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: getThumbShape(),
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
};

View 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;
}

View 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);
}

View 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);
}

View 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);
}
});
}

View 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;
}

View 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);
});
}

View 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;
}

View 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;

View file

@ -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);
});

View file

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

View file

@ -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,

View file

@ -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>

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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);
}

View file

@ -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>

View file

@ -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')
}];
}

View file

@ -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')
}];
}

View file

@ -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')
}];
}

View file

@ -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')
}];
}

View file

@ -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')
}];
}

View file

@ -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')
}];
}

View file

@ -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')
}];
}

View file

@ -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')
}];
}

View file

@ -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')
}];
}

View file

@ -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')
}];
}

View file

@ -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')
}];
}

View file

@ -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>';

View file

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

View file

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

View file

@ -22,13 +22,13 @@ function onSubmit() {
function getTabs() {
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')
}];
}

View file

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

View file

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

View file

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

View file

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

View file

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

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