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:
commit
273db3ac79
134 changed files with 5097 additions and 2182 deletions
23
.devcontainer/devcontainer.json
Normal file
23
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
|
||||
{
|
||||
"name": "Node.js",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-20-bullseye",
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
//https://github.com/microsoft/vscode-dev-containers/issues/559
|
||||
"postCreateCommand": "source $NVM_DIR/nvm.sh && nvm install 20"
|
||||
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
|
@ -66,6 +66,7 @@ module.exports = {
|
|||
'no-unused-expressions': ['off'],
|
||||
'@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
|
||||
'no-unused-private-class-members': ['error'],
|
||||
'no-useless-rename': ['error'],
|
||||
'no-useless-constructor': ['off'],
|
||||
'@typescript-eslint/no-useless-constructor': ['error'],
|
||||
'no-var': ['error'],
|
||||
|
|
65
.github/workflows/build.yml
vendored
Normal file
65
.github/workflows/build.yml
vendored
Normal 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
|
|
@ -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
65
.github/workflows/job-messages.yml
vendored
Normal 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
97
.github/workflows/publish.yml
vendored
Normal 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
|
|
@ -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
|
29
.github/workflows/tsc.yml
vendored
29
.github/workflows/tsc.yml
vendored
|
@ -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
|
|
@ -66,6 +66,7 @@
|
|||
- [Fishbigger](https://github.com/fishbigger)
|
||||
- [sleepycatcoding](https://github.com/sleepycatcoding)
|
||||
- [TheMelmacian](https://github.com/TheMelmacian)
|
||||
- [tehciolo](https://github.com/tehciolo)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
|
|
@ -77,8 +77,9 @@ Jellyfin Web is the frontend used for most of the clients available for end user
|
|||
.
|
||||
└── src
|
||||
├── apps
|
||||
│ ├── experimental # New experimental app layout
|
||||
│ └── stable # Classic (stable) app layout
|
||||
│ ├── dashboard # Admin dashboard app layout and routes
|
||||
│ ├── experimental # New experimental app layout and routes
|
||||
│ └── stable # Classic (stable) app layout and routes
|
||||
├── assets # Static assets
|
||||
├── components # Higher order visual components and React components
|
||||
├── controllers # Legacy page views and controllers 🧹
|
||||
|
@ -87,7 +88,6 @@ Jellyfin Web is the frontend used for most of the clients available for end user
|
|||
├── legacy # Polyfills for legacy browsers
|
||||
├── libraries # Third party libraries 🧹
|
||||
├── plugins # Client plugins
|
||||
├── routes # React routes/pages
|
||||
├── scripts # Random assortment of visual components and utilities 🐉
|
||||
├── strings # Translation files
|
||||
├── styles # Common app Sass stylesheets
|
||||
|
|
1674
package-lock.json
generated
1674
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -58,6 +58,7 @@
|
|||
"stylelint-scss": "5.0.0",
|
||||
"ts-loader": "9.4.4",
|
||||
"typescript": "5.0.4",
|
||||
"vitest": "0.34.6",
|
||||
"webpack": "5.88.1",
|
||||
"webpack-bundle-analyzer": "4.9.1",
|
||||
"webpack-cli": "5.1.4",
|
||||
|
@ -145,6 +146,8 @@
|
|||
"build:check": "tsc --noEmit",
|
||||
"escheck": "es-check",
|
||||
"lint": "eslint \"./\"",
|
||||
"test": "vitest --watch=false",
|
||||
"test:watch": "vitest",
|
||||
"stylelint": "npm run stylelint:css && npm run stylelint:scss",
|
||||
"stylelint:css": "stylelint \"src/**/*.css\"",
|
||||
"stylelint:scss": "stylelint --config=\".stylelintrc.scss.json\" \"src/**/*.scss\""
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import loadable from '@loadable/component';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { History } from '@remix-run/router';
|
||||
import React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
||||
import AppHeader from 'components/AppHeader';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
import { HistoryRouter } from 'components/router/HistoryRouter';
|
||||
|
@ -12,6 +14,7 @@ import { ApiProvider } from 'hooks/useApi';
|
|||
import { WebConfigProvider } from 'hooks/useWebConfig';
|
||||
import theme from 'themes/theme';
|
||||
|
||||
const DashboardApp = loadable(() => import('./apps/dashboard/App'));
|
||||
const ExperimentalApp = loadable(() => import('./apps/experimental/App'));
|
||||
const StableApp = loadable(() => import('./apps/stable/App'));
|
||||
|
||||
|
@ -21,16 +24,22 @@ const RootAppLayout = () => {
|
|||
const layoutMode = localStorage.getItem('layout');
|
||||
const isExperimentalLayout = layoutMode === 'experimental';
|
||||
|
||||
const location = useLocation();
|
||||
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
|
||||
.some(path => location.pathname.startsWith(`/${path}`));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Backdrop />
|
||||
<AppHeader isHidden={isExperimentalLayout} />
|
||||
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
|
||||
|
||||
{
|
||||
isExperimentalLayout ?
|
||||
<ExperimentalApp /> :
|
||||
<StableApp />
|
||||
}
|
||||
|
||||
<DashboardApp />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
66
src/apps/dashboard/App.tsx
Normal file
66
src/apps/dashboard/App.tsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
import loadable from '@loadable/component';
|
||||
import React from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import ConnectionRequired from 'components/ConnectionRequired';
|
||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||
import { AsyncPageProps, AsyncRoute, toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||
import { toRedirectRoute } from 'components/router/Redirect';
|
||||
import ServerContentPage from 'components/ServerContentPage';
|
||||
|
||||
import AppLayout from './AppLayout';
|
||||
import { REDIRECTS } from './routes/_redirects';
|
||||
import { ASYNC_ADMIN_ROUTES } from './routes/_asyncRoutes';
|
||||
import { LEGACY_ADMIN_ROUTES } from './routes/_legacyRoutes';
|
||||
|
||||
const DashboardAsyncPage = loadable(
|
||||
(props: { page: string }) => import(/* webpackChunkName: "[request]" */ `./routes/${props.page}`),
|
||||
{ cacheKey: (props: AsyncPageProps) => props.page }
|
||||
);
|
||||
|
||||
const toDashboardAsyncPageRoute = (route: AsyncRoute) => (
|
||||
toAsyncPageRoute({
|
||||
...route,
|
||||
element: DashboardAsyncPage
|
||||
})
|
||||
);
|
||||
|
||||
export const DASHBOARD_APP_PATHS = {
|
||||
Dashboard: 'dashboard',
|
||||
MetadataManager: 'metadata',
|
||||
PluginConfig: 'configurationpage'
|
||||
};
|
||||
|
||||
const DashboardApp = () => (
|
||||
<Routes>
|
||||
<Route element={<ConnectionRequired isAdminRequired />}>
|
||||
<Route element={<AppLayout drawerlessPaths={[ DASHBOARD_APP_PATHS.MetadataManager ]} />}>
|
||||
<Route path={DASHBOARD_APP_PATHS.Dashboard}>
|
||||
{ASYNC_ADMIN_ROUTES.map(toDashboardAsyncPageRoute)}
|
||||
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
|
||||
</Route>
|
||||
|
||||
{/* NOTE: The metadata editor might deserve a dedicated app in the future */}
|
||||
{toViewManagerPageRoute({
|
||||
path: DASHBOARD_APP_PATHS.MetadataManager,
|
||||
pageProps: {
|
||||
controller: 'edititemmetadata',
|
||||
view: 'edititemmetadata.html'
|
||||
}
|
||||
})}
|
||||
|
||||
<Route path={DASHBOARD_APP_PATHS.PluginConfig} element={
|
||||
<ServerContentPage view='/web/configurationpage' />
|
||||
} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
{/* Suppress warnings for unhandled routes */}
|
||||
<Route path='*' element={null} />
|
||||
|
||||
{/* Redirects for old paths */}
|
||||
{REDIRECTS.map(toRedirectRoute)}
|
||||
</Routes>
|
||||
);
|
||||
|
||||
export default DashboardApp;
|
108
src/apps/dashboard/AppLayout.tsx
Normal file
108
src/apps/dashboard/AppLayout.tsx
Normal file
|
@ -0,0 +1,108 @@
|
|||
import AppBar from '@mui/material/AppBar';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
|
||||
import AppBody from 'components/AppBody';
|
||||
import AppToolbar from 'components/toolbar/AppToolbar';
|
||||
import ElevationScroll from 'components/ElevationScroll';
|
||||
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||
|
||||
import AppDrawer from './components/drawer/AppDrawer';
|
||||
|
||||
import './AppOverrides.scss';
|
||||
|
||||
interface AppLayoutProps {
|
||||
drawerlessPaths: string[]
|
||||
}
|
||||
|
||||
interface DashboardAppSettings {
|
||||
isDrawerPinned: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_APP_SETTINGS: DashboardAppSettings = {
|
||||
isDrawerPinned: false
|
||||
};
|
||||
|
||||
const AppLayout: FC<AppLayoutProps> = ({
|
||||
drawerlessPaths
|
||||
}) => {
|
||||
const [ appSettings, setAppSettings ] = useLocalStorage<DashboardAppSettings>('DashboardAppSettings', DEFAULT_APP_SETTINGS);
|
||||
const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned);
|
||||
const location = useLocation();
|
||||
const theme = useTheme();
|
||||
const { user } = useApi();
|
||||
|
||||
const isDrawerAvailable = !drawerlessPaths.some(path => location.pathname.startsWith(`/${path}`));
|
||||
const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDrawerActive !== appSettings.isDrawerPinned) {
|
||||
setAppSettings({
|
||||
...appSettings,
|
||||
isDrawerPinned: isDrawerActive
|
||||
});
|
||||
}
|
||||
}, [ appSettings, isDrawerActive, setAppSettings ]);
|
||||
|
||||
const onToggleDrawer = useCallback(() => {
|
||||
setIsDrawerActive(!isDrawerActive);
|
||||
}, [ isDrawerActive, setIsDrawerActive ]);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<ElevationScroll elevate={isDrawerOpen}>
|
||||
<AppBar
|
||||
position='fixed'
|
||||
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.drawer + 1 }}
|
||||
>
|
||||
<AppToolbar
|
||||
isDrawerAvailable={isDrawerAvailable}
|
||||
isDrawerOpen={isDrawerOpen}
|
||||
onDrawerButtonClick={onToggleDrawer}
|
||||
/>
|
||||
</AppBar>
|
||||
</ElevationScroll>
|
||||
|
||||
<AppDrawer
|
||||
open={isDrawerOpen}
|
||||
onClose={onToggleDrawer}
|
||||
onOpen={onToggleDrawer}
|
||||
/>
|
||||
|
||||
<Box
|
||||
component='main'
|
||||
sx={{
|
||||
width: '100%',
|
||||
flexGrow: 1,
|
||||
transition: theme.transitions.create('margin', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen
|
||||
}),
|
||||
marginLeft: 0,
|
||||
...(isDrawerAvailable && {
|
||||
marginLeft: {
|
||||
sm: `-${DRAWER_WIDTH}px`
|
||||
}
|
||||
}),
|
||||
...(isDrawerActive && {
|
||||
transition: theme.transitions.create('margin', {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen
|
||||
}),
|
||||
marginLeft: 0
|
||||
})
|
||||
}}
|
||||
>
|
||||
<AppBody>
|
||||
<Outlet />
|
||||
</AppBody>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
22
src/apps/dashboard/AppOverrides.scss
Normal file
22
src/apps/dashboard/AppOverrides.scss
Normal file
|
@ -0,0 +1,22 @@
|
|||
// Default MUI breakpoints
|
||||
// https://mui.com/material-ui/customization/breakpoints/#default-breakpoints
|
||||
$mui-bp-sm: 600px;
|
||||
$mui-bp-md: 900px;
|
||||
$mui-bp-lg: 1200px;
|
||||
$mui-bp-xl: 1536px;
|
||||
|
||||
// Fix dashboard pages layout to work with drawer
|
||||
.dashboardDocument {
|
||||
.mainAnimatedPage {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.skinBody {
|
||||
position: unset !important;
|
||||
}
|
||||
|
||||
// Fix the padding of dashboard pages
|
||||
.content-primary.content-primary {
|
||||
padding-top: 3.25rem !important;
|
||||
}
|
||||
}
|
29
src/apps/dashboard/components/drawer/AppDrawer.tsx
Normal file
29
src/apps/dashboard/components/drawer/AppDrawer.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';
|
||||
|
||||
import ServerDrawerSection from './sections/ServerDrawerSection';
|
||||
import DevicesDrawerSection from './sections/DevicesDrawerSection';
|
||||
import LiveTvDrawerSection from './sections/LiveTvDrawerSection';
|
||||
import AdvancedDrawerSection from './sections/AdvancedDrawerSection';
|
||||
import PluginDrawerSection from './sections/PluginDrawerSection';
|
||||
|
||||
const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
||||
open = false,
|
||||
onClose,
|
||||
onOpen
|
||||
}) => (
|
||||
<ResponsiveDrawer
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
<ServerDrawerSection />
|
||||
<DevicesDrawerSection />
|
||||
<LiveTvDrawerSection />
|
||||
<AdvancedDrawerSection />
|
||||
<PluginDrawerSection />
|
||||
</ResponsiveDrawer>
|
||||
);
|
||||
|
||||
export default AppDrawer;
|
|
@ -19,10 +19,10 @@ import ListItemLink from 'components/ListItemLink';
|
|||
import globalize from 'scripts/globalize';
|
||||
|
||||
const PLUGIN_PATHS = [
|
||||
'/installedplugins.html',
|
||||
'/availableplugins.html',
|
||||
'/repositories.html',
|
||||
'/addplugin.html',
|
||||
'/dashboard/plugins',
|
||||
'/dashboard/plugins/catalog',
|
||||
'/dashboard/plugins/repositories',
|
||||
'/dashboard/plugins/add',
|
||||
'/configurationpage'
|
||||
];
|
||||
|
||||
|
@ -41,7 +41,7 @@ const AdvancedDrawerSection = () => {
|
|||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/networking.html'>
|
||||
<ListItemLink to='/dashboard/networking'>
|
||||
<ListItemIcon>
|
||||
<Lan />
|
||||
</ListItemIcon>
|
||||
|
@ -49,7 +49,7 @@ const AdvancedDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/apikeys.html'>
|
||||
<ListItemLink to='/dashboard/keys'>
|
||||
<ListItemIcon>
|
||||
<VpnKey />
|
||||
</ListItemIcon>
|
||||
|
@ -57,7 +57,7 @@ const AdvancedDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/log.html'>
|
||||
<ListItemLink to='/dashboard/logs'>
|
||||
<ListItemIcon>
|
||||
<Article />
|
||||
</ListItemIcon>
|
||||
|
@ -65,7 +65,7 @@ const AdvancedDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/notificationsettings.html'>
|
||||
<ListItemLink to='/dashboard/notifications'>
|
||||
<ListItemIcon>
|
||||
<EditNotifications />
|
||||
</ListItemIcon>
|
||||
|
@ -73,7 +73,7 @@ const AdvancedDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/installedplugins.html' selected={false}>
|
||||
<ListItemLink to='/dashboard/plugins' selected={false}>
|
||||
<ListItemIcon>
|
||||
<Extension />
|
||||
</ListItemIcon>
|
||||
|
@ -83,19 +83,19 @@ const AdvancedDrawerSection = () => {
|
|||
</ListItem>
|
||||
<Collapse in={isPluginSectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/installedplugins.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/plugins' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabMyPlugins')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/availableplugins.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/plugins/catalog' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabCatalog')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/repositories.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/plugins/repositories' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabRepositories')} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
</Collapse>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/scheduledtasks.html'>
|
||||
<ListItemLink to='/dashboard/tasks'>
|
||||
<ListItemIcon>
|
||||
<Schedule />
|
||||
</ListItemIcon>
|
|
@ -12,8 +12,8 @@ import ListItemLink from 'components/ListItemLink';
|
|||
import globalize from 'scripts/globalize';
|
||||
|
||||
const DLNA_PATHS = [
|
||||
'/dlnasettings.html',
|
||||
'/dlnaprofiles.html'
|
||||
'/dashboard/dlna',
|
||||
'/dashboard/dlna/profiles'
|
||||
];
|
||||
|
||||
const DevicesDrawerSection = () => {
|
||||
|
@ -31,7 +31,7 @@ const DevicesDrawerSection = () => {
|
|||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/devices.html'>
|
||||
<ListItemLink to='/dashboard/devices'>
|
||||
<ListItemIcon>
|
||||
<Devices />
|
||||
</ListItemIcon>
|
||||
|
@ -47,7 +47,7 @@ const DevicesDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dlnasettings.html' selected={false}>
|
||||
<ListItemLink to='/dashboard/dlna' selected={false}>
|
||||
<ListItemIcon>
|
||||
<Input />
|
||||
</ListItemIcon>
|
||||
|
@ -57,10 +57,10 @@ const DevicesDrawerSection = () => {
|
|||
</ListItem>
|
||||
<Collapse in={isDlnaSectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/dlnasettings.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/dlna' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Settings')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dlnaprofiles.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/dlna/profiles' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabProfiles')} />
|
||||
</ListItemLink>
|
||||
</List>
|
|
@ -20,7 +20,7 @@ const LiveTvDrawerSection = () => {
|
|||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/livetvstatus.html'>
|
||||
<ListItemLink to='/dashboard/livetv'>
|
||||
<ListItemIcon>
|
||||
<LiveTv />
|
||||
</ListItemIcon>
|
||||
|
@ -28,7 +28,7 @@ const LiveTvDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/livetvsettings.html'>
|
||||
<ListItemLink to='/dashboard/recordings'>
|
||||
<ListItemIcon>
|
||||
<Dvr />
|
||||
</ListItemIcon>
|
|
@ -12,16 +12,16 @@ import ListItemLink from 'components/ListItemLink';
|
|||
import globalize from 'scripts/globalize';
|
||||
|
||||
const LIBRARY_PATHS = [
|
||||
'/library.html',
|
||||
'/librarydisplay.html',
|
||||
'/metadataimages.html',
|
||||
'/metadatanfo.html'
|
||||
'/dashboard/libraries',
|
||||
'/dashboard/libraries/display',
|
||||
'/dashboard/libraries/metadata',
|
||||
'/dashboard/libraries/nfo'
|
||||
];
|
||||
|
||||
const PLAYBACK_PATHS = [
|
||||
'/encodingsettings.html',
|
||||
'/playbackconfiguration.html',
|
||||
'/streamingsettings.html'
|
||||
'/dashboard/playback/transcoding',
|
||||
'/dashboard/playback/resume',
|
||||
'/dashboard/playback/streaming'
|
||||
];
|
||||
|
||||
const ServerDrawerSection = () => {
|
||||
|
@ -40,7 +40,7 @@ const ServerDrawerSection = () => {
|
|||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard.html'>
|
||||
<ListItemLink to='/dashboard'>
|
||||
<ListItemIcon>
|
||||
<Dashboard />
|
||||
</ListItemIcon>
|
||||
|
@ -48,7 +48,7 @@ const ServerDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboardgeneral.html'>
|
||||
<ListItemLink to='/dashboard/settings'>
|
||||
<ListItemIcon>
|
||||
<Settings />
|
||||
</ListItemIcon>
|
||||
|
@ -56,7 +56,7 @@ const ServerDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/userprofiles.html'>
|
||||
<ListItemLink to='/dashboard/users'>
|
||||
<ListItemIcon>
|
||||
<People />
|
||||
</ListItemIcon>
|
||||
|
@ -64,7 +64,7 @@ const ServerDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/library.html' selected={false}>
|
||||
<ListItemLink to='/dashboard/libraries' selected={false}>
|
||||
<ListItemIcon>
|
||||
<LibraryAdd />
|
||||
</ListItemIcon>
|
||||
|
@ -74,22 +74,22 @@ const ServerDrawerSection = () => {
|
|||
</ListItem>
|
||||
<Collapse in={isLibrarySectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/library.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/libraries' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('HeaderLibraries')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/librarydisplay.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/libraries/display' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Display')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/metadataimages.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/libraries/metadata' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Metadata')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/metadatanfo.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/libraries/nfo' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabNfoSettings')} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
</Collapse>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/encodingsettings.html' selected={false}>
|
||||
<ListItemLink to='/dashboard/playback/transcoding' selected={false}>
|
||||
<ListItemIcon>
|
||||
<PlayCircle />
|
||||
</ListItemIcon>
|
||||
|
@ -99,13 +99,13 @@ const ServerDrawerSection = () => {
|
|||
</ListItem>
|
||||
<Collapse in={isPlaybackSectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/encodingsettings.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/playback/transcoding' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Transcoding')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/playbackconfiguration.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/playback/resume' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('ButtonResume')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/streamingsettings.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/playback/streaming' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabStreaming')} />
|
||||
</ListItemLink>
|
||||
</List>
|
12
src/apps/dashboard/routes/_asyncRoutes.ts
Normal file
12
src/apps/dashboard/routes/_asyncRoutes.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import type { AsyncRoute } from 'components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'activity' },
|
||||
{ path: 'notifications' },
|
||||
{ path: 'users' },
|
||||
{ path: 'users/access' },
|
||||
{ path: 'users/add' },
|
||||
{ path: 'users/parentalcontrol' },
|
||||
{ path: 'users/password' },
|
||||
{ path: 'users/profile' }
|
||||
];
|
|
@ -1,170 +1,164 @@
|
|||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
import type { LegacyRoute } from 'components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||
{
|
||||
path: 'dashboard.html',
|
||||
path: '/dashboard',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dashboard',
|
||||
view: 'dashboard/dashboard.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dashboardgeneral.html',
|
||||
path: 'settings',
|
||||
pageProps: {
|
||||
controller: 'dashboard/general',
|
||||
view: 'dashboard/general.html'
|
||||
}
|
||||
}, {
|
||||
path: 'networking.html',
|
||||
path: 'networking',
|
||||
pageProps: {
|
||||
controller: 'dashboard/networking',
|
||||
view: 'dashboard/networking.html'
|
||||
}
|
||||
}, {
|
||||
path: 'devices.html',
|
||||
path: 'devices',
|
||||
pageProps: {
|
||||
controller: 'dashboard/devices/devices',
|
||||
view: 'dashboard/devices/devices.html'
|
||||
}
|
||||
}, {
|
||||
path: 'device.html',
|
||||
path: 'devices/edit',
|
||||
pageProps: {
|
||||
controller: 'dashboard/devices/device',
|
||||
view: 'dashboard/devices/device.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dlnaprofile.html',
|
||||
path: 'dlna/profiles/edit',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dlna/profile',
|
||||
view: 'dashboard/dlna/profile.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dlnaprofiles.html',
|
||||
path: 'dlna/profiles',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dlna/profiles',
|
||||
view: 'dashboard/dlna/profiles.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dlnasettings.html',
|
||||
path: 'dlna',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dlna/settings',
|
||||
view: 'dashboard/dlna/settings.html'
|
||||
}
|
||||
}, {
|
||||
path: 'addplugin.html',
|
||||
path: 'plugins/add',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/add/index',
|
||||
view: 'dashboard/plugins/add/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'library.html',
|
||||
path: 'libraries',
|
||||
pageProps: {
|
||||
controller: 'dashboard/library',
|
||||
view: 'dashboard/library.html'
|
||||
}
|
||||
}, {
|
||||
path: 'librarydisplay.html',
|
||||
path: 'libraries/display',
|
||||
pageProps: {
|
||||
controller: 'dashboard/librarydisplay',
|
||||
view: 'dashboard/librarydisplay.html'
|
||||
}
|
||||
}, {
|
||||
path: 'edititemmetadata.html',
|
||||
pageProps: {
|
||||
controller: 'edititemmetadata',
|
||||
view: 'edititemmetadata.html'
|
||||
}
|
||||
}, {
|
||||
path: 'encodingsettings.html',
|
||||
path: 'playback/transcoding',
|
||||
pageProps: {
|
||||
controller: 'dashboard/encodingsettings',
|
||||
view: 'dashboard/encodingsettings.html'
|
||||
}
|
||||
}, {
|
||||
path: 'log.html',
|
||||
path: 'logs',
|
||||
pageProps: {
|
||||
controller: 'dashboard/logs',
|
||||
view: 'dashboard/logs.html'
|
||||
}
|
||||
}, {
|
||||
path: 'metadataimages.html',
|
||||
path: 'libraries/metadata',
|
||||
pageProps: {
|
||||
controller: 'dashboard/metadataImages',
|
||||
view: 'dashboard/metadataimages.html'
|
||||
}
|
||||
}, {
|
||||
path: 'metadatanfo.html',
|
||||
path: 'libraries/nfo',
|
||||
pageProps: {
|
||||
controller: 'dashboard/metadatanfo',
|
||||
view: 'dashboard/metadatanfo.html'
|
||||
}
|
||||
}, {
|
||||
path: 'playbackconfiguration.html',
|
||||
path: 'playback/resume',
|
||||
pageProps: {
|
||||
controller: 'dashboard/playback',
|
||||
view: 'dashboard/playback.html'
|
||||
}
|
||||
}, {
|
||||
path: 'availableplugins.html',
|
||||
path: 'plugins/catalog',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/available/index',
|
||||
view: 'dashboard/plugins/available/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'repositories.html',
|
||||
path: 'plugins/repositories',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/repositories/index',
|
||||
view: 'dashboard/plugins/repositories/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvguideprovider.html',
|
||||
path: 'livetv/guide',
|
||||
pageProps: {
|
||||
controller: 'livetvguideprovider',
|
||||
view: 'livetvguideprovider.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvsettings.html',
|
||||
path: 'recordings',
|
||||
pageProps: {
|
||||
controller: 'livetvsettings',
|
||||
view: 'livetvsettings.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvstatus.html',
|
||||
path: 'livetv',
|
||||
pageProps: {
|
||||
controller: 'livetvstatus',
|
||||
view: 'livetvstatus.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvtuner.html',
|
||||
path: 'livetv/tuner',
|
||||
pageProps: {
|
||||
controller: 'livetvtuner',
|
||||
view: 'livetvtuner.html'
|
||||
}
|
||||
}, {
|
||||
path: 'installedplugins.html',
|
||||
path: 'plugins',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/installed/index',
|
||||
view: 'dashboard/plugins/installed/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'scheduledtask.html',
|
||||
path: 'tasks/edit',
|
||||
pageProps: {
|
||||
controller: 'dashboard/scheduledtasks/scheduledtask',
|
||||
view: 'dashboard/scheduledtasks/scheduledtask.html'
|
||||
}
|
||||
}, {
|
||||
path: 'scheduledtasks.html',
|
||||
path: 'tasks',
|
||||
pageProps: {
|
||||
controller: 'dashboard/scheduledtasks/scheduledtasks',
|
||||
view: 'dashboard/scheduledtasks/scheduledtasks.html'
|
||||
}
|
||||
}, {
|
||||
path: 'apikeys.html',
|
||||
path: 'keys',
|
||||
pageProps: {
|
||||
controller: 'dashboard/apikeys',
|
||||
view: 'dashboard/apikeys.html'
|
||||
}
|
||||
}, {
|
||||
path: 'streamingsettings.html',
|
||||
path: 'playback/streaming',
|
||||
pageProps: {
|
||||
view: 'dashboard/streaming.html',
|
||||
controller: 'dashboard/streaming'
|
40
src/apps/dashboard/routes/_redirects.ts
Normal file
40
src/apps/dashboard/routes/_redirects.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import type { Redirect } from 'components/router/Redirect';
|
||||
|
||||
export const REDIRECTS: Redirect[] = [
|
||||
{ from: 'addplugin.html', to: '/dashboard/plugins/add' },
|
||||
{ from: 'apikeys.html', to: '/dashboard/keys' },
|
||||
{ from: 'availableplugins.html', to: '/dashboard/plugins/catalog' },
|
||||
{ from: 'dashboard.html', to: '/dashboard' },
|
||||
{ from: 'dashboardgeneral.html', to: '/dashboard/settings' },
|
||||
{ from: 'device.html', to: '/dashboard/devices/edit' },
|
||||
{ from: 'devices.html', to: '/dashboard/devices' },
|
||||
{ from: 'dlnaprofile.html', to: '/dashboard/dlna/profiles/edit' },
|
||||
{ from: 'dlnaprofiles.html', to: '/dashboard/dlna/profiles' },
|
||||
{ from: 'dlnasettings.html', to: '/dashboard/dlna' },
|
||||
{ from: 'edititemmetadata.html', to: '/metadata' },
|
||||
{ from: 'encodingsettings.html', to: '/dashboard/playback/transcoding' },
|
||||
{ from: 'installedplugins.html', to: '/dashboard/plugins' },
|
||||
{ from: 'library.html', to: '/dashboard/libraries' },
|
||||
{ from: 'librarydisplay.html', to: '/dashboard/libraries/display' },
|
||||
{ from: 'livetvguideprovider.html', to: '/dashboard/livetv/guide' },
|
||||
{ from: 'livetvsettings.html', to: '/dashboard/recordings' },
|
||||
{ from: 'livetvstatus.html', to: '/dashboard/livetv' },
|
||||
{ from: 'livetvtuner.html', to: '/dashboard/livetv/tuner' },
|
||||
{ from: 'log.html', to: '/dashboard/logs' },
|
||||
{ from: 'metadataimages.html', to: '/dashboard/libraries/metadata' },
|
||||
{ from: 'metadatanfo.html', to: '/dashboard/libraries/nfo' },
|
||||
{ from: 'networking.html', to: '/dashboard/networking' },
|
||||
{ from: 'notificationsettings.html', to: '/dashboard/notifications' },
|
||||
{ from: 'playbackconfiguration.html', to: '/dashboard/playback/resume' },
|
||||
{ from: 'repositories.html', to: '/dashboard/plugins/repositories' },
|
||||
{ from: 'scheduledtask.html', to: '/dashboard/tasks/edit' },
|
||||
{ from: 'scheduledtasks.html', to: '/dashboard/tasks' },
|
||||
{ from: 'serveractivity.html', to: '/dashboard/activity' },
|
||||
{ from: 'streamingsettings.html', to: '/dashboard/playback/streaming' },
|
||||
{ from: 'useredit.html', to: '/dashboard/users/profile' },
|
||||
{ from: 'userlibraryaccess.html', to: '/dashboard/users/access' },
|
||||
{ from: 'usernew.html', to: '/dashboard/users/add' },
|
||||
{ from: 'userparentalcontrol.html', to: '/dashboard/users/parentalcontrol' },
|
||||
{ from: 'userpassword.html', to: '/dashboard/users/password' },
|
||||
{ from: 'userprofiles.html', to: '/dashboard/users' }
|
||||
];
|
|
@ -19,9 +19,9 @@ import { parseISO8601Date, toLocaleDateString, toLocaleTimeString } from 'script
|
|||
import globalize from 'scripts/globalize';
|
||||
import { toBoolean } from 'utils/string';
|
||||
|
||||
import LogLevelChip from '../../components/activityTable/LogLevelChip';
|
||||
import OverviewCell from '../../components/activityTable/OverviewCell';
|
||||
import GridActionsCellLink from '../../components/GridActionsCellLink';
|
||||
import LogLevelChip from '../components/activityTable/LogLevelChip';
|
||||
import OverviewCell from '../components/activityTable/OverviewCell';
|
||||
import GridActionsCellLink from '../components/dataGrid/GridActionsCellLink';
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 25;
|
||||
const VIEW_PARAM = 'useractivity';
|
||||
|
@ -68,7 +68,7 @@ const Activity = () => {
|
|||
sx={{ padding: 0 }}
|
||||
title={users[row.UserId]?.Name ?? undefined}
|
||||
component={Link}
|
||||
to={`/useredit.html?userId=${row.UserId}`}
|
||||
to={`/dashboard/users/profile?userId=${row.UserId}`}
|
||||
>
|
||||
<UserAvatar user={users[row.UserId]} />
|
||||
</IconButton>
|
|
@ -9,7 +9,7 @@ const PluginLink = () => (
|
|||
__html: `<a
|
||||
is='emby-linkbutton'
|
||||
class='button-link'
|
||||
href='#/addplugin.html?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173'
|
||||
href='#/dashboard/plugins/add?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173'
|
||||
>
|
||||
${globalize.translate('GetThePlugin')}
|
||||
</a>`
|
|
@ -140,7 +140,7 @@ const UserNew: FunctionComponent = () => {
|
|||
}
|
||||
|
||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
Dashboard.navigate('useredit.html?userId=' + user.Id)
|
||||
Dashboard.navigate('/dashboard/users/profile?userId=' + user.Id)
|
||||
.catch(err => {
|
||||
console.error('[usernew] failed to navigate to edit user page', err);
|
||||
});
|
|
@ -85,21 +85,21 @@ const UserProfiles: FunctionComponent = () => {
|
|||
callback: function (id: string) {
|
||||
switch (id) {
|
||||
case 'open':
|
||||
Dashboard.navigate('useredit.html?userId=' + userId)
|
||||
Dashboard.navigate('/dashboard/users/profile?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to user edit page', err);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'access':
|
||||
Dashboard.navigate('userlibraryaccess.html?userId=' + userId)
|
||||
Dashboard.navigate('/dashboard/users/access?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to user library page', err);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'parentalcontrol':
|
||||
Dashboard.navigate('userparentalcontrol.html?userId=' + userId)
|
||||
Dashboard.navigate('/dashboard/users/parentalcontrol?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to parental control page', err);
|
||||
});
|
||||
|
@ -146,7 +146,7 @@ const UserProfiles: FunctionComponent = () => {
|
|||
});
|
||||
|
||||
(page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
|
||||
Dashboard.navigate('usernew.html')
|
||||
Dashboard.navigate('/dashboard/users/add')
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to new user page', err);
|
||||
});
|
|
@ -32,7 +32,7 @@ const getCheckedElementDataIds = (elements: NodeListOf<Element>) => (
|
|||
);
|
||||
|
||||
function onSaveComplete() {
|
||||
Dashboard.navigate('userprofiles.html')
|
||||
Dashboard.navigate('/dashboard/users')
|
||||
.catch(err => {
|
||||
console.error('[useredit] failed to navigate to user profile', err);
|
||||
});
|
|
@ -1,16 +1,16 @@
|
|||
import React from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
||||
import { REDIRECTS } from 'apps/stable/routes/_redirects';
|
||||
import ConnectionRequired from 'components/ConnectionRequired';
|
||||
import ServerContentPage from 'components/ServerContentPage';
|
||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||
import { toRedirectRoute } from 'components/router/Redirect';
|
||||
|
||||
import AppLayout from './AppLayout';
|
||||
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
||||
import { ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
||||
|
||||
const ExperimentalApp = () => {
|
||||
return (
|
||||
|
@ -22,16 +22,6 @@ const ExperimentalApp = () => {
|
|||
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
|
||||
</Route>
|
||||
|
||||
{/* Admin routes */}
|
||||
<Route element={<ConnectionRequired isAdminRequired />}>
|
||||
{ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)}
|
||||
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
|
||||
|
||||
<Route path='configurationpage' element={
|
||||
<ServerContentPage view='/web/configurationpage' />
|
||||
} />
|
||||
</Route>
|
||||
|
||||
{/* Public routes */}
|
||||
<Route element={<ConnectionRequired isUserRequired={false} />}>
|
||||
<Route index element={<Navigate replace to='/home.html' />} />
|
||||
|
@ -42,6 +32,15 @@ const ExperimentalApp = () => {
|
|||
|
||||
{/* Redirects for old paths */}
|
||||
{REDIRECTS.map(toRedirectRoute)}
|
||||
|
||||
{/* Ignore dashboard routes */}
|
||||
{Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => (
|
||||
<Route
|
||||
key={key}
|
||||
path={`/${path}/*`}
|
||||
element={null}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,11 +10,6 @@ $mui-bp-xl: 1536px;
|
|||
position: relative;
|
||||
}
|
||||
|
||||
// Fix dashboard pages layout to work with drawer
|
||||
.dashboardDocument .skinBody {
|
||||
position: unset;
|
||||
}
|
||||
|
||||
// Hide some items from the user "settings" page that are in the drawer
|
||||
#myPreferencesMenuPage {
|
||||
.lnkQuickConnectPreferences,
|
||||
|
@ -26,8 +21,7 @@ $mui-bp-xl: 1536px;
|
|||
|
||||
// Fix the padding of some pages
|
||||
.homePage.libraryPage, // Home page
|
||||
.libraryPage:not(.withTabs), // Tabless library pages
|
||||
.content-primary.content-primary { // Dashboard pages
|
||||
.libraryPage:not(.withTabs) { // Tabless library pages
|
||||
padding-top: 3.25rem !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,15 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';
|
||||
|
||||
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from '../../routes/asyncRoutes';
|
||||
import { LEGACY_ADMIN_ROUTES, LEGACY_USER_ROUTES } from '../../routes/legacyRoutes';
|
||||
|
||||
import AdvancedDrawerSection from './dashboard/AdvancedDrawerSection';
|
||||
import DevicesDrawerSection from './dashboard/DevicesDrawerSection';
|
||||
import LiveTvDrawerSection from './dashboard/LiveTvDrawerSection';
|
||||
import PluginDrawerSection from './dashboard/PluginDrawerSection';
|
||||
import ServerDrawerSection from './dashboard/ServerDrawerSection';
|
||||
import MainDrawerContent from './MainDrawerContent';
|
||||
import { ASYNC_USER_ROUTES } from '../../routes/asyncRoutes';
|
||||
import { LEGACY_USER_ROUTES } from '../../routes/legacyRoutes';
|
||||
import { isTabPath } from '../tabs/tabRoutes';
|
||||
|
||||
export const DRAWER_WIDTH = 240;
|
||||
import MainDrawerContent from './MainDrawerContent';
|
||||
|
||||
const DRAWERLESS_ROUTES = [
|
||||
'edititemmetadata.html', // metadata manager
|
||||
'video' // video player
|
||||
];
|
||||
|
||||
|
@ -26,77 +18,29 @@ const MAIN_DRAWER_ROUTES = [
|
|||
...LEGACY_USER_ROUTES
|
||||
].filter(route => !DRAWERLESS_ROUTES.includes(route.path));
|
||||
|
||||
const ADMIN_DRAWER_ROUTES = [
|
||||
...ASYNC_ADMIN_ROUTES,
|
||||
...LEGACY_ADMIN_ROUTES,
|
||||
{ path: '/configurationpage' } // Plugin configuration page
|
||||
].filter(route => !DRAWERLESS_ROUTES.includes(route.path));
|
||||
|
||||
/** Utility function to check if a path has a drawer. */
|
||||
export const isDrawerPath = (path: string) => (
|
||||
MAIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path)
|
||||
|| ADMIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path)
|
||||
);
|
||||
|
||||
const Drawer: FC<ResponsiveDrawerProps> = ({ children, ...props }) => {
|
||||
const location = useLocation();
|
||||
const hasSecondaryToolBar = isTabPath(location.pathname);
|
||||
|
||||
return (
|
||||
<ResponsiveDrawer
|
||||
{...props}
|
||||
hasSecondaryToolBar={hasSecondaryToolBar}
|
||||
>
|
||||
{children}
|
||||
</ResponsiveDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
||||
open = false,
|
||||
onClose,
|
||||
onOpen
|
||||
}) => (
|
||||
<Routes>
|
||||
{
|
||||
MAIN_DRAWER_ROUTES.map(route => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={
|
||||
<Drawer
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
<MainDrawerContent />
|
||||
</Drawer>
|
||||
}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{
|
||||
ADMIN_DRAWER_ROUTES.map(route => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={
|
||||
<Drawer
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
<ServerDrawerSection />
|
||||
<DevicesDrawerSection />
|
||||
<LiveTvDrawerSection />
|
||||
<AdvancedDrawerSection />
|
||||
<PluginDrawerSection />
|
||||
</Drawer>
|
||||
}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Routes>
|
||||
);
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const hasSecondaryToolBar = isTabPath(location.pathname);
|
||||
|
||||
return (
|
||||
<ResponsiveDrawer
|
||||
hasSecondaryToolBar={hasSecondaryToolBar}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
<MainDrawerContent />
|
||||
</ResponsiveDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppDrawer;
|
||||
|
|
|
@ -150,7 +150,7 @@ const MainDrawerContent = () => {
|
|||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard.html'>
|
||||
<ListItemLink to='/dashboard'>
|
||||
<ListItemIcon>
|
||||
<Dashboard />
|
||||
</ListItemIcon>
|
||||
|
@ -158,7 +158,7 @@ const MainDrawerContent = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/edititemmetadata.html'>
|
||||
<ListItemLink to='/metadata'>
|
||||
<ListItemIcon>
|
||||
<Edit />
|
||||
</ListItemIcon>
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
import { IconButton } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
const NewCollectionButton: FC = () => {
|
||||
const showCollectionEditor = useCallback(() => {
|
||||
import('components/collectionEditor/collectionEditor').then(
|
||||
({ default: CollectionEditor }) => {
|
||||
const serverId = window.ApiClient.serverId();
|
||||
const collectionEditor = new CollectionEditor();
|
||||
collectionEditor.show({
|
||||
items: [],
|
||||
serverId: serverId
|
||||
}).catch(() => {
|
||||
// closed collection editor
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[NewCollection] failed to load collection editor', err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
title={globalize.translate('Add')}
|
||||
className='paper-icon-button-light btnNewCollection autoSize'
|
||||
onClick={showCollectionEditor}
|
||||
>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewCollectionButton;
|
57
src/apps/experimental/components/library/PlayAllButton.tsx
Normal file
57
src/apps/experimental/components/library/PlayAllButton.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { IconButton } from '@mui/material';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { getFiltersQuery } from 'utils/items';
|
||||
import { LibraryViewSettings } from 'types/library';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
|
||||
interface PlayAllButtonProps {
|
||||
item: BaseItemDto | undefined;
|
||||
items: BaseItemDto[];
|
||||
viewType: LibraryTab;
|
||||
hasFilters: boolean;
|
||||
libraryViewSettings: LibraryViewSettings
|
||||
}
|
||||
|
||||
const PlayAllButton: FC<PlayAllButtonProps> = ({ item, items, viewType, hasFilters, libraryViewSettings }) => {
|
||||
const play = useCallback(() => {
|
||||
if (item && !hasFilters) {
|
||||
playbackManager.play({
|
||||
items: [item],
|
||||
autoplay: true,
|
||||
queryOptions: {
|
||||
SortBy: [libraryViewSettings.SortBy],
|
||||
SortOrder: [libraryViewSettings.SortOrder]
|
||||
}
|
||||
});
|
||||
} else {
|
||||
playbackManager.play({
|
||||
items: items,
|
||||
autoplay: true,
|
||||
queryOptions: {
|
||||
ParentId: item?.Id ?? undefined,
|
||||
...getFiltersQuery(viewType, libraryViewSettings),
|
||||
SortBy: [libraryViewSettings.SortBy],
|
||||
SortOrder: [libraryViewSettings.SortOrder]
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}, [hasFilters, item, items, libraryViewSettings, viewType]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
title={globalize.translate('HeaderPlayAll')}
|
||||
className='paper-icon-button-light btnPlay autoSize'
|
||||
onClick={play}
|
||||
>
|
||||
<PlayArrowIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlayAllButton;
|
39
src/apps/experimental/components/library/QueueButton.tsx
Normal file
39
src/apps/experimental/components/library/QueueButton.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { IconButton } from '@mui/material';
|
||||
import QueueIcon from '@mui/icons-material/Queue';
|
||||
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
interface QueueButtonProps {
|
||||
item: BaseItemDto | undefined
|
||||
items: BaseItemDto[];
|
||||
hasFilters: boolean;
|
||||
}
|
||||
|
||||
const QueueButton: FC<QueueButtonProps> = ({ item, items, hasFilters }) => {
|
||||
const queue = useCallback(() => {
|
||||
if (item && !hasFilters) {
|
||||
playbackManager.queue({
|
||||
items: [item]
|
||||
});
|
||||
} else {
|
||||
playbackManager.queue({
|
||||
items: items
|
||||
});
|
||||
}
|
||||
}, [hasFilters, item, items]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
title={globalize.translate('AddToPlayQueue')}
|
||||
className='paper-icon-button-light btnQueue autoSize'
|
||||
onClick={queue}
|
||||
>
|
||||
<QueueIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueueButton;
|
49
src/apps/experimental/components/library/ShuffleButton.tsx
Normal file
49
src/apps/experimental/components/library/ShuffleButton.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { IconButton } from '@mui/material';
|
||||
import ShuffleIcon from '@mui/icons-material/Shuffle';
|
||||
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { getFiltersQuery } from 'utils/items';
|
||||
import { LibraryViewSettings } from 'types/library';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
|
||||
interface ShuffleButtonProps {
|
||||
item: BaseItemDto | undefined;
|
||||
items: BaseItemDto[];
|
||||
viewType: LibraryTab
|
||||
hasFilters: boolean;
|
||||
libraryViewSettings: LibraryViewSettings
|
||||
}
|
||||
|
||||
const ShuffleButton: FC<ShuffleButtonProps> = ({ item, items, viewType, hasFilters, libraryViewSettings }) => {
|
||||
const shuffle = useCallback(() => {
|
||||
if (item && !hasFilters) {
|
||||
playbackManager.shuffle(item);
|
||||
} else {
|
||||
playbackManager.play({
|
||||
items: items,
|
||||
autoplay: true,
|
||||
queryOptions: {
|
||||
ParentId: item?.Id ?? undefined,
|
||||
...getFiltersQuery(viewType, libraryViewSettings),
|
||||
SortBy: [ItemSortBy.Random]
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [hasFilters, item, items, libraryViewSettings, viewType]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
title={globalize.translate('Shuffle')}
|
||||
className='paper-icon-button-light btnShuffle autoSize'
|
||||
onClick={shuffle}
|
||||
>
|
||||
<ShuffleIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShuffleButton;
|
|
@ -98,7 +98,7 @@ const SortButton: FC<SortButtonProps> = ({
|
|||
title={globalize.translate('Sort')}
|
||||
sx={{ ml: 2 }}
|
||||
aria-describedby={id}
|
||||
className='paper-icon-button-light btnShuffle autoSize'
|
||||
className='paper-icon-button-light btnSort autoSize'
|
||||
onClick={handleClick}
|
||||
>
|
||||
<SortByAlphaIcon />
|
||||
|
|
|
@ -100,7 +100,7 @@ const ViewSettingsButton: FC<ViewSettingsButtonProps> = ({
|
|||
title={globalize.translate('ButtonSelectView')}
|
||||
sx={{ ml: 2 }}
|
||||
aria-describedby={id}
|
||||
className='paper-icon-button-light btnShuffle autoSize'
|
||||
className='paper-icon-button-light btnSelectView autoSize'
|
||||
onClick={handleClick}
|
||||
>
|
||||
<ViewComfyIcon />
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import { AsyncRoute, AsyncRouteType } from 'components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'dashboard/activity', page: 'dashboard/activity', type: AsyncRouteType.Experimental },
|
||||
{ path: 'notificationsettings.html', page: 'dashboard/notifications' },
|
||||
{ path: 'usernew.html', page: 'user/usernew' },
|
||||
{ path: 'userprofiles.html', page: 'user/userprofiles' },
|
||||
{ path: 'useredit.html', page: 'user/useredit' },
|
||||
{ path: 'userlibraryaccess.html', page: 'user/userlibraryaccess' },
|
||||
{ path: 'userparentalcontrol.html', page: 'user/userparentalcontrol' },
|
||||
{ path: 'userpassword.html', page: 'user/userpassword' }
|
||||
];
|
|
@ -1,2 +1 @@
|
|||
export * from './admin';
|
||||
export * from './user';
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
export * from './admin';
|
||||
export * from './public';
|
||||
export * from './user';
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import React from 'react';
|
||||
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
||||
import AppBody from 'components/AppBody';
|
||||
import ServerContentPage from 'components/ServerContentPage';
|
||||
import ConnectionRequired from 'components/ConnectionRequired';
|
||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||
|
||||
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
||||
import { REDIRECTS } from './routes/_redirects';
|
||||
import { toRedirectRoute } from 'components/router/Redirect';
|
||||
|
||||
import { ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
||||
import { REDIRECTS } from './routes/_redirects';
|
||||
|
||||
const Layout = () => (
|
||||
<AppBody>
|
||||
<Outlet />
|
||||
|
@ -27,16 +27,6 @@ const StableApp = () => (
|
|||
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
|
||||
</Route>
|
||||
|
||||
{/* Admin routes */}
|
||||
<Route path='/' element={<ConnectionRequired isAdminRequired />}>
|
||||
{ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)}
|
||||
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
|
||||
|
||||
<Route path='configurationpage' element={
|
||||
<ServerContentPage view='/web/configurationpage' />
|
||||
} />
|
||||
</Route>
|
||||
|
||||
{/* Public routes */}
|
||||
<Route path='/' element={<ConnectionRequired isUserRequired={false} />}>
|
||||
<Route index element={<Navigate replace to='/home.html' />} />
|
||||
|
@ -50,6 +40,15 @@ const StableApp = () => (
|
|||
|
||||
{/* Redirects for old paths */}
|
||||
{REDIRECTS.map(toRedirectRoute)}
|
||||
|
||||
{/* Ignore dashboard routes */}
|
||||
{Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => (
|
||||
<Route
|
||||
key={key}
|
||||
path={`/${path}/*`}
|
||||
element={null}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import type { Redirect } from 'components/router/Redirect';
|
||||
|
||||
export const REDIRECTS: Redirect[] = [
|
||||
{ from: 'mypreferencesquickconnect.html', to: '/quickconnect' },
|
||||
{ from: 'serveractivity.html', to: '/dashboard/activity' }
|
||||
{ from: 'mypreferencesquickconnect.html', to: '/quickconnect' }
|
||||
];
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'notificationsettings.html', page: 'dashboard/notifications' },
|
||||
{ path: 'usernew.html', page: 'user/usernew' },
|
||||
{ path: 'userprofiles.html', page: 'user/userprofiles' },
|
||||
{ path: 'useredit.html', page: 'user/useredit' },
|
||||
{ path: 'userlibraryaccess.html', page: 'user/userlibraryaccess' },
|
||||
{ path: 'userparentalcontrol.html', page: 'user/userparentalcontrol' },
|
||||
{ path: 'userpassword.html', page: 'user/userpassword' }
|
||||
];
|
|
@ -1,2 +1 @@
|
|||
export * from './admin';
|
||||
export * from './user';
|
||||
|
|
|
@ -1,179 +0,0 @@
|
|||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||
{
|
||||
path: 'dashboard.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dashboard',
|
||||
view: 'dashboard/dashboard.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dashboardgeneral.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/general',
|
||||
view: 'dashboard/general.html'
|
||||
}
|
||||
}, {
|
||||
path: 'networking.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/networking',
|
||||
view: 'dashboard/networking.html'
|
||||
}
|
||||
}, {
|
||||
path: 'devices.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/devices/devices',
|
||||
view: 'dashboard/devices/devices.html'
|
||||
}
|
||||
}, {
|
||||
path: 'device.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/devices/device',
|
||||
view: 'dashboard/devices/device.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dlnaprofile.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dlna/profile',
|
||||
view: 'dashboard/dlna/profile.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dlnaprofiles.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dlna/profiles',
|
||||
view: 'dashboard/dlna/profiles.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dlnasettings.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dlna/settings',
|
||||
view: 'dashboard/dlna/settings.html'
|
||||
}
|
||||
}, {
|
||||
path: 'addplugin.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/add/index',
|
||||
view: 'dashboard/plugins/add/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'library.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/library',
|
||||
view: 'dashboard/library.html'
|
||||
}
|
||||
}, {
|
||||
path: 'librarydisplay.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/librarydisplay',
|
||||
view: 'dashboard/librarydisplay.html'
|
||||
}
|
||||
}, {
|
||||
path: 'edititemmetadata.html',
|
||||
pageProps: {
|
||||
controller: 'edititemmetadata',
|
||||
view: 'edititemmetadata.html'
|
||||
}
|
||||
}, {
|
||||
path: 'encodingsettings.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/encodingsettings',
|
||||
view: 'dashboard/encodingsettings.html'
|
||||
}
|
||||
}, {
|
||||
path: 'log.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/logs',
|
||||
view: 'dashboard/logs.html'
|
||||
}
|
||||
}, {
|
||||
path: 'metadataimages.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/metadataImages',
|
||||
view: 'dashboard/metadataimages.html'
|
||||
}
|
||||
}, {
|
||||
path: 'metadatanfo.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/metadatanfo',
|
||||
view: 'dashboard/metadatanfo.html'
|
||||
}
|
||||
}, {
|
||||
path: 'playbackconfiguration.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/playback',
|
||||
view: 'dashboard/playback.html'
|
||||
}
|
||||
}, {
|
||||
path: 'availableplugins.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/available/index',
|
||||
view: 'dashboard/plugins/available/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'repositories.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/repositories/index',
|
||||
view: 'dashboard/plugins/repositories/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvguideprovider.html',
|
||||
pageProps: {
|
||||
controller: 'livetvguideprovider',
|
||||
view: 'livetvguideprovider.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvsettings.html',
|
||||
pageProps: {
|
||||
controller: 'livetvsettings',
|
||||
view: 'livetvsettings.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvstatus.html',
|
||||
pageProps: {
|
||||
controller: 'livetvstatus',
|
||||
view: 'livetvstatus.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvtuner.html',
|
||||
pageProps: {
|
||||
controller: 'livetvtuner',
|
||||
view: 'livetvtuner.html'
|
||||
}
|
||||
}, {
|
||||
path: 'installedplugins.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/installed/index',
|
||||
view: 'dashboard/plugins/installed/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'scheduledtask.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/scheduledtasks/scheduledtask',
|
||||
view: 'dashboard/scheduledtasks/scheduledtask.html'
|
||||
}
|
||||
}, {
|
||||
path: 'scheduledtasks.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/scheduledtasks/scheduledtasks',
|
||||
view: 'dashboard/scheduledtasks/scheduledtasks.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dashboard/activity',
|
||||
pageProps: {
|
||||
controller: 'dashboard/serveractivity',
|
||||
view: 'dashboard/serveractivity.html'
|
||||
}
|
||||
}, {
|
||||
path: 'apikeys.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/apikeys',
|
||||
view: 'dashboard/apikeys.html'
|
||||
}
|
||||
}, {
|
||||
path: 'streamingsettings.html',
|
||||
pageProps: {
|
||||
view: 'dashboard/streaming.html',
|
||||
controller: 'dashboard/streaming'
|
||||
}
|
||||
}
|
||||
];
|
|
@ -1,3 +1,2 @@
|
|||
export * from './admin';
|
||||
export * from './public';
|
||||
export * from './user';
|
||||
|
|
|
@ -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 + '"]');
|
||||
|
||||
|
|
173
src/components/cardbuilder/cardBuilderUtils.js
Normal file
173
src/components/cardbuilder/cardBuilderUtils.js
Normal file
|
@ -0,0 +1,173 @@
|
|||
const ASPECT_RATIOS = {
|
||||
portrait: (2 / 3),
|
||||
backdrop: (16 / 9),
|
||||
square: 1,
|
||||
banner: (1000 / 185)
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the aspect ratio for a card given its shape.
|
||||
* @param {string} shape - Shape for which to get the aspect ratio.
|
||||
* @returns {null|number} Ratio of the shape.
|
||||
*/
|
||||
function getDesiredAspect(shape) {
|
||||
if (!shape) {
|
||||
return null;
|
||||
}
|
||||
|
||||
shape = shape.toLowerCase();
|
||||
if (shape.indexOf('portrait') !== -1) {
|
||||
return ASPECT_RATIOS.portrait;
|
||||
}
|
||||
if (shape.indexOf('backdrop') !== -1) {
|
||||
return ASPECT_RATIOS.backdrop;
|
||||
}
|
||||
if (shape.indexOf('square') !== -1) {
|
||||
return ASPECT_RATIOS.square;
|
||||
}
|
||||
if (shape.indexOf('banner') !== -1) {
|
||||
return ASPECT_RATIOS.banner;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the number of posters per row.
|
||||
* @param {string} shape - Shape of the cards.
|
||||
* @param {number} screenWidth - Width of the screen.
|
||||
* @param {boolean} isOrientationLandscape - Flag for the orientation of the screen.
|
||||
* @param {boolean} isTV - Flag to denote if posters are rendered on a television screen.
|
||||
* @returns {number} Number of cards per row for an itemsContainer.
|
||||
*/
|
||||
function getPostersPerRow(shape, screenWidth, isOrientationLandscape, isTV) {
|
||||
switch (shape) {
|
||||
case 'portrait': return postersPerRowPortrait(screenWidth, isTV);
|
||||
case 'square': return postersPerRowSquare(screenWidth, isTV);
|
||||
case 'banner': return postersPerRowBanner(screenWidth);
|
||||
case 'backdrop': return postersPerRowBackdrop(screenWidth, isTV);
|
||||
case 'smallBackdrop': return postersPerRowSmallBackdrop(screenWidth);
|
||||
case 'overflowSmallBackdrop': return postersPerRowOverflowSmallBackdrop(screenWidth, isOrientationLandscape, isTV);
|
||||
case 'overflowPortrait': return postersPerRowOverflowPortrait(screenWidth, isOrientationLandscape, isTV);
|
||||
case 'overflowSquare': return postersPerRowOverflowSquare(screenWidth, isOrientationLandscape, isTV);
|
||||
case 'overflowBackdrop': return postersPerRowOverflowBackdrop(screenWidth, isOrientationLandscape, isTV);
|
||||
default: return 4;
|
||||
}
|
||||
}
|
||||
|
||||
const postersPerRowPortrait = (screenWidth, isTV) => {
|
||||
switch (true) {
|
||||
case isTV: return 100 / 16.66666667;
|
||||
case screenWidth >= 2200: return 10;
|
||||
case screenWidth >= 1920: return 100 / 11.1111111111;
|
||||
case screenWidth >= 1600: return 8;
|
||||
case screenWidth >= 1400: return 100 / 14.28571428571;
|
||||
case screenWidth >= 1200: return 100 / 16.66666667;
|
||||
case screenWidth >= 800: return 5;
|
||||
case screenWidth >= 700: return 4;
|
||||
case screenWidth >= 500: return 100 / 33.33333333;
|
||||
default: return 100 / 33.33333333;
|
||||
}
|
||||
};
|
||||
|
||||
const postersPerRowSquare = (screenWidth, isTV) => {
|
||||
switch (true) {
|
||||
case isTV: return 100 / 16.66666667;
|
||||
case screenWidth >= 2200: return 10;
|
||||
case screenWidth >= 1920: return 100 / 11.1111111111;
|
||||
case screenWidth >= 1600: return 8;
|
||||
case screenWidth >= 1400: return 100 / 14.28571428571;
|
||||
case screenWidth >= 1200: return 100 / 16.66666667;
|
||||
case screenWidth >= 800: return 5;
|
||||
case screenWidth >= 700: return 4;
|
||||
case screenWidth >= 500: return 100 / 33.33333333;
|
||||
default: return 2;
|
||||
}
|
||||
};
|
||||
|
||||
const postersPerRowBanner = (screenWidth) => {
|
||||
switch (true) {
|
||||
case screenWidth >= 2200: return 4;
|
||||
case screenWidth >= 1200: return 100 / 33.33333333;
|
||||
case screenWidth >= 800: return 2;
|
||||
default: return 1;
|
||||
}
|
||||
};
|
||||
|
||||
const postersPerRowBackdrop = (screenWidth, isTV) => {
|
||||
switch (true) {
|
||||
case isTV: return 4;
|
||||
case screenWidth >= 2500: return 6;
|
||||
case screenWidth >= 1600: return 5;
|
||||
case screenWidth >= 1200: return 4;
|
||||
case screenWidth >= 770: return 3;
|
||||
case screenWidth >= 420: return 2;
|
||||
default: return 1;
|
||||
}
|
||||
};
|
||||
|
||||
function postersPerRowSmallBackdrop(screenWidth) {
|
||||
switch (true) {
|
||||
case screenWidth >= 1600: return 8;
|
||||
case screenWidth >= 1400: return 100 / 14.2857142857;
|
||||
case screenWidth >= 1200: return 100 / 16.66666667;
|
||||
case screenWidth >= 1000: return 5;
|
||||
case screenWidth >= 800: return 4;
|
||||
case screenWidth >= 500: return 100 / 33.33333333;
|
||||
default: return 2;
|
||||
}
|
||||
}
|
||||
|
||||
const postersPerRowOverflowSmallBackdrop = (screenWidth, isLandscape, isTV) => {
|
||||
switch (true) {
|
||||
case isTV: return 100 / 18.9;
|
||||
case isLandscape && screenWidth >= 800: return 100 / 15.5;
|
||||
case isLandscape: return 100 / 23.3;
|
||||
case screenWidth >= 540: return 100 / 30;
|
||||
default: return 100 / 72;
|
||||
}
|
||||
};
|
||||
|
||||
const postersPerRowOverflowPortrait = (screenWidth, isLandscape, isTV) => {
|
||||
switch (true) {
|
||||
case isTV: return 100 / 15.5;
|
||||
case isLandscape && screenWidth >= 1700: return 100 / 11.6;
|
||||
case isLandscape: return 100 / 15.5;
|
||||
case screenWidth >= 1400: return 100 / 15;
|
||||
case screenWidth >= 1200: return 100 / 18;
|
||||
case screenWidth >= 760: return 100 / 23;
|
||||
case screenWidth >= 400: return 100 / 31.5;
|
||||
default: return 100 / 42;
|
||||
}
|
||||
};
|
||||
|
||||
const postersPerRowOverflowSquare = (screenWidth, isLandscape, isTV) => {
|
||||
switch (true) {
|
||||
case isTV: return 100 / 15.5;
|
||||
case isLandscape && screenWidth >= 1700: return 100 / 11.6;
|
||||
case isLandscape: return 100 / 15.5;
|
||||
case screenWidth >= 1400: return 100 / 15;
|
||||
case screenWidth >= 1200: return 100 / 18;
|
||||
case screenWidth >= 760: return 100 / 23;
|
||||
case screenWidth >= 540: return 100 / 31.5;
|
||||
default: return 100 / 42;
|
||||
}
|
||||
};
|
||||
|
||||
const postersPerRowOverflowBackdrop = (screenWidth, isLandscape, isTV) => {
|
||||
switch (true) {
|
||||
case isTV: return 100 / 23.3;
|
||||
case isLandscape && screenWidth >= 1700: return 100 / 18.5;
|
||||
case isLandscape: return 100 / 23.3;
|
||||
case screenWidth >= 1800: return 100 / 23.5;
|
||||
case screenWidth >= 1400: return 100 / 30;
|
||||
case screenWidth >= 760: return 100 / 40;
|
||||
case screenWidth >= 640: return 100 / 56;
|
||||
default: return 100 / 72;
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
getDesiredAspect,
|
||||
getPostersPerRow
|
||||
};
|
417
src/components/cardbuilder/cardBuilderUtils.test.js
Normal file
417
src/components/cardbuilder/cardBuilderUtils.test.js
Normal file
|
@ -0,0 +1,417 @@
|
|||
import { describe, expect, test } from 'vitest';
|
||||
import cardBuilderUtils from './cardBuilderUtils';
|
||||
|
||||
describe('getDesiredAspect', () => {
|
||||
test('"portrait" (case insensitive)', () => {
|
||||
expect(cardBuilderUtils.getDesiredAspect('portrait')).toEqual((2 / 3));
|
||||
expect(cardBuilderUtils.getDesiredAspect('PorTRaIt')).toEqual((2 / 3));
|
||||
});
|
||||
|
||||
test('"backdrop" (case insensitive)', () => {
|
||||
expect(cardBuilderUtils.getDesiredAspect('backdrop')).toEqual((16 / 9));
|
||||
expect(cardBuilderUtils.getDesiredAspect('BaCkDroP')).toEqual((16 / 9));
|
||||
});
|
||||
|
||||
test('"square" (case insensitive)', () => {
|
||||
expect(cardBuilderUtils.getDesiredAspect('square')).toEqual(1);
|
||||
expect(cardBuilderUtils.getDesiredAspect('sQuArE')).toEqual(1);
|
||||
});
|
||||
|
||||
test('"banner" (case insensitive)', () => {
|
||||
expect(cardBuilderUtils.getDesiredAspect('banner')).toEqual((1000 / 185));
|
||||
expect(cardBuilderUtils.getDesiredAspect('BaNnEr')).toEqual((1000 / 185));
|
||||
});
|
||||
|
||||
test('invalid shape', () => {
|
||||
expect(cardBuilderUtils.getDesiredAspect('invalid')).toBeNull();
|
||||
});
|
||||
|
||||
test('shape is not provided', () => {
|
||||
expect(cardBuilderUtils.getDesiredAspect('')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPostersPerRow', () => {
|
||||
test('resolves to default of 4 posters per row if shape is not provided', () => {
|
||||
expect(cardBuilderUtils.getPostersPerRow('', 0, false, false)).toEqual(4);
|
||||
});
|
||||
|
||||
describe('portrait', () => {
|
||||
const postersPerRowForPortrait = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('portrait', screenWidth, false, isTV));
|
||||
|
||||
test('television', () => {
|
||||
expect(postersPerRowForPortrait(0, true)).toEqual(100 / 16.66666667);
|
||||
});
|
||||
|
||||
test('screen width less than 500px', () => {
|
||||
expect(postersPerRowForPortrait(100, false)).toEqual(100 / 33.33333333);
|
||||
expect(postersPerRowForPortrait(499, false)).toEqual(100 / 33.33333333);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 500px', () => {
|
||||
expect(postersPerRowForPortrait(500, false)).toEqual(100 / 33.33333333);
|
||||
expect(postersPerRowForPortrait(501, false)).toEqual(100 / 33.33333333);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 700px', () => {
|
||||
expect(postersPerRowForPortrait(700, false)).toEqual(4);
|
||||
expect(postersPerRowForPortrait(701, false)).toEqual(4);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 800px', () => {
|
||||
expect(postersPerRowForPortrait(800, false)).toEqual(5);
|
||||
expect(postersPerRowForPortrait(801, false)).toEqual(5);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1200px', () => {
|
||||
expect(postersPerRowForPortrait(1200, false)).toEqual(100 / 16.66666667);
|
||||
expect(postersPerRowForPortrait(1201, false)).toEqual(100 / 16.66666667);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1400px', () => {
|
||||
expect(postersPerRowForPortrait(1400, false)).toEqual( 100 / 14.28571428571);
|
||||
expect(postersPerRowForPortrait(1401, false)).toEqual( 100 / 14.28571428571);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1600px', () => {
|
||||
expect(postersPerRowForPortrait(1600, false)).toEqual( 8);
|
||||
expect(postersPerRowForPortrait(1601, false)).toEqual( 8);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1920px', () => {
|
||||
expect(postersPerRowForPortrait(1920, false)).toEqual( 100 / 11.1111111111);
|
||||
expect(postersPerRowForPortrait(1921, false)).toEqual( 100 / 11.1111111111);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 2200px', () => {
|
||||
expect(postersPerRowForPortrait(2200, false)).toEqual( 10);
|
||||
expect(postersPerRowForPortrait(2201, false)).toEqual( 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('square', () => {
|
||||
const postersPerRowForSquare = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('square', screenWidth, false, isTV));
|
||||
|
||||
test('television', () => {
|
||||
expect(postersPerRowForSquare(0, true)).toEqual(100 / 16.66666667);
|
||||
});
|
||||
|
||||
test('screen width less than 500px', () => {
|
||||
expect(postersPerRowForSquare(100, false)).toEqual(2);
|
||||
expect(postersPerRowForSquare(499, false)).toEqual(2);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 500px', () => {
|
||||
expect(postersPerRowForSquare(500, false)).toEqual(100 / 33.33333333);
|
||||
expect(postersPerRowForSquare(501, false)).toEqual(100 / 33.33333333);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 700px', () => {
|
||||
expect(postersPerRowForSquare(700, false)).toEqual(4);
|
||||
expect(postersPerRowForSquare(701, false)).toEqual(4);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 800px', () => {
|
||||
expect(postersPerRowForSquare(800, false)).toEqual(5);
|
||||
expect(postersPerRowForSquare(801, false)).toEqual(5);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1200px', () => {
|
||||
expect(postersPerRowForSquare(1200, false)).toEqual(100 / 16.66666667);
|
||||
expect(postersPerRowForSquare(1201, false)).toEqual(100 / 16.66666667);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1400px', () => {
|
||||
expect(postersPerRowForSquare(1400, false)).toEqual( 100 / 14.28571428571);
|
||||
expect(postersPerRowForSquare(1401, false)).toEqual( 100 / 14.28571428571);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1600px', () => {
|
||||
expect(postersPerRowForSquare(1600, false)).toEqual(8);
|
||||
expect(postersPerRowForSquare(1601, false)).toEqual(8);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1920px', () => {
|
||||
expect(postersPerRowForSquare(1920, false)).toEqual(100 / 11.1111111111);
|
||||
expect(postersPerRowForSquare(1921, false)).toEqual(100 / 11.1111111111);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 2200px', () => {
|
||||
expect(postersPerRowForSquare(2200, false)).toEqual( 10);
|
||||
expect(postersPerRowForSquare(2201, false)).toEqual( 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('banner', () => {
|
||||
const postersPerRowForBanner = (screenWidth) => (cardBuilderUtils.getPostersPerRow('banner', screenWidth, false, false));
|
||||
|
||||
test('screen width less than 800px', () => {
|
||||
expect(postersPerRowForBanner(799)).toEqual(1);
|
||||
});
|
||||
|
||||
test('screen width greater than or equal to 800px', () => {
|
||||
expect(postersPerRowForBanner(800)).toEqual(2);
|
||||
expect(postersPerRowForBanner(801)).toEqual(2);
|
||||
});
|
||||
|
||||
test('screen width greater than or equal to 1200px', () => {
|
||||
expect(postersPerRowForBanner(1200)).toEqual(100 / 33.33333333);
|
||||
expect(postersPerRowForBanner(1201)).toEqual(100 / 33.33333333);
|
||||
});
|
||||
|
||||
test('screen width greater than or equal to 2200px', () => {
|
||||
expect(postersPerRowForBanner(2200)).toEqual(4);
|
||||
expect(postersPerRowForBanner(2201)).toEqual(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('backdrop', () => {
|
||||
const postersPerRowForBackdrop = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('backdrop', screenWidth, false, isTV));
|
||||
|
||||
test('television', () => {
|
||||
expect(postersPerRowForBackdrop(0, true)).toEqual(4);
|
||||
});
|
||||
|
||||
test('screen width less than 420px', () => {
|
||||
expect(postersPerRowForBackdrop(100, false)).toEqual(1);
|
||||
expect(postersPerRowForBackdrop(419, false)).toEqual(1);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 420px', () => {
|
||||
expect(postersPerRowForBackdrop(420, false)).toEqual(2);
|
||||
expect(postersPerRowForBackdrop(421, false)).toEqual(2);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 770px', () => {
|
||||
expect(postersPerRowForBackdrop(770, false)).toEqual(3);
|
||||
expect(postersPerRowForBackdrop(771, false)).toEqual(3);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1200px', () => {
|
||||
expect(postersPerRowForBackdrop(1200, false)).toEqual(4);
|
||||
expect(postersPerRowForBackdrop(1201, false)).toEqual(4);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1600px', () => {
|
||||
expect(postersPerRowForBackdrop(1600, false)).toEqual(5);
|
||||
expect(postersPerRowForBackdrop(1601, false)).toEqual(5);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 2500px', () => {
|
||||
expect(postersPerRowForBackdrop(2500, false)).toEqual(6);
|
||||
expect(postersPerRowForBackdrop(2501, false)).toEqual(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('small backdrop', () => {
|
||||
const postersPerRowForSmallBackdrop = (screenWidth) => (cardBuilderUtils.getPostersPerRow('smallBackdrop', screenWidth, false, false));
|
||||
|
||||
test('screen width less than 500px', () => {
|
||||
expect(postersPerRowForSmallBackdrop(100)).toEqual(2);
|
||||
expect(postersPerRowForSmallBackdrop(499)).toEqual(2);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 500px', () => {
|
||||
expect(postersPerRowForSmallBackdrop(500)).toEqual(100 / 33.33333333);
|
||||
expect(postersPerRowForSmallBackdrop(501)).toEqual(100 / 33.33333333);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 800px', () => {
|
||||
expect(postersPerRowForSmallBackdrop(800)).toEqual(4);
|
||||
expect(postersPerRowForSmallBackdrop(801)).toEqual(4);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1000px', () => {
|
||||
expect(postersPerRowForSmallBackdrop(1000)).toEqual(5);
|
||||
expect(postersPerRowForSmallBackdrop(1001)).toEqual(5);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1200px', () => {
|
||||
expect(postersPerRowForSmallBackdrop(1200)).toEqual(100 / 16.66666667);
|
||||
expect(postersPerRowForSmallBackdrop(1201)).toEqual(100 / 16.66666667);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1400px', () => {
|
||||
expect(postersPerRowForSmallBackdrop(1400)).toEqual(100 / 14.2857142857);
|
||||
expect(postersPerRowForSmallBackdrop(1401)).toEqual(100 / 14.2857142857);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1600px', () => {
|
||||
expect(postersPerRowForSmallBackdrop(1600)).toEqual(8);
|
||||
expect(postersPerRowForSmallBackdrop(1601)).toEqual(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe('overflow small backdrop', () => {
|
||||
const postersPerRowForOverflowSmallBackdrop = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowSmallBackdrop', screenWidth, isLandscape, isTV));
|
||||
|
||||
test('television', () => {
|
||||
expect(postersPerRowForOverflowSmallBackdrop(0, false, true)).toEqual( 100 / 18.9);
|
||||
});
|
||||
|
||||
describe('non-landscape', () => {
|
||||
test('screen width greater or equal to 540px', () => {
|
||||
expect(postersPerRowForOverflowSmallBackdrop(540, false)).toEqual(100 / 30);
|
||||
expect(postersPerRowForOverflowSmallBackdrop(541, false)).toEqual(100 / 30);
|
||||
});
|
||||
|
||||
test('screen width is less than 540px', () => {
|
||||
expect(postersPerRowForOverflowSmallBackdrop(539, false)).toEqual(100 / 72);
|
||||
expect(postersPerRowForOverflowSmallBackdrop(100, false)).toEqual(100 / 72);
|
||||
});
|
||||
});
|
||||
|
||||
describe('landscape', () => {
|
||||
test('screen width greater or equal to 800px', () => {
|
||||
expect(postersPerRowForOverflowSmallBackdrop(800, true)).toEqual(100 / 15.5);
|
||||
expect(postersPerRowForOverflowSmallBackdrop(801, true)).toEqual(100 / 15.5);
|
||||
});
|
||||
|
||||
test('screen width is less than 800px', () => {
|
||||
expect(postersPerRowForOverflowSmallBackdrop(799, true)).toEqual(100 / 23.3);
|
||||
expect(postersPerRowForOverflowSmallBackdrop(100, true)).toEqual(100 / 23.3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('overflow portrait', () => {
|
||||
const postersPerRowForOverflowPortrait = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowPortrait', screenWidth, isLandscape, isTV));
|
||||
|
||||
test('television', () => {
|
||||
expect(postersPerRowForOverflowPortrait(0, false, true)).toEqual( 100 / 15.5);
|
||||
});
|
||||
|
||||
describe('non-landscape', () => {
|
||||
test('screen width greater or equal to 1400px', () => {
|
||||
expect(postersPerRowForOverflowPortrait(1400, false)).toEqual(100 / 15);
|
||||
expect(postersPerRowForOverflowPortrait(1401, false)).toEqual(100 / 15);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1200px', () => {
|
||||
expect(postersPerRowForOverflowPortrait(1200, false)).toEqual(100 / 18);
|
||||
expect(postersPerRowForOverflowPortrait(1201, false)).toEqual(100 / 18);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 760px', () => {
|
||||
expect(postersPerRowForOverflowPortrait(760, false)).toEqual(100 / 23);
|
||||
expect(postersPerRowForOverflowPortrait(761, false)).toEqual(100 / 23);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 400px', () => {
|
||||
expect(postersPerRowForOverflowPortrait(400, false)).toEqual(100 / 31.5);
|
||||
expect(postersPerRowForOverflowPortrait(401, false)).toEqual(100 / 31.5);
|
||||
});
|
||||
|
||||
test('screen width is less than 400px', () => {
|
||||
expect(postersPerRowForOverflowPortrait(399, false)).toEqual(100 / 42);
|
||||
expect(postersPerRowForOverflowPortrait(100, false)).toEqual(100 / 42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('landscape', () => {
|
||||
test('screen width greater or equal to 1700px', () => {
|
||||
expect(postersPerRowForOverflowPortrait(1700, true)).toEqual(100 / 11.6);
|
||||
expect(postersPerRowForOverflowPortrait(1701, true)).toEqual(100 / 11.6);
|
||||
});
|
||||
|
||||
test('screen width is less than 1700px', () => {
|
||||
expect(postersPerRowForOverflowPortrait(1699, true)).toEqual(100 / 15.5);
|
||||
expect(postersPerRowForOverflowPortrait(100, true)).toEqual(100 / 15.5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('overflow square', () => {
|
||||
const postersPerRowForOverflowSquare = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowSquare', screenWidth, isLandscape, isTV));
|
||||
|
||||
test('television', () => {
|
||||
expect(postersPerRowForOverflowSquare(0, false, true)).toEqual( 100 / 15.5);
|
||||
});
|
||||
|
||||
describe('non-landscape', () => {
|
||||
test('screen width greater or equal to 1400px', () => {
|
||||
expect(postersPerRowForOverflowSquare(1400, false)).toEqual(100 / 15);
|
||||
expect(postersPerRowForOverflowSquare(1401, false)).toEqual(100 / 15);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1200px', () => {
|
||||
expect(postersPerRowForOverflowSquare(1200, false)).toEqual(100 / 18);
|
||||
expect(postersPerRowForOverflowSquare(1201, false)).toEqual(100 / 18);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 760px', () => {
|
||||
expect(postersPerRowForOverflowSquare(760, false)).toEqual(100 / 23);
|
||||
expect(postersPerRowForOverflowSquare(761, false)).toEqual(100 / 23);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 540px', () => {
|
||||
expect(postersPerRowForOverflowSquare(540, false)).toEqual(100 / 31.5);
|
||||
expect(postersPerRowForOverflowSquare(541, false)).toEqual(100 / 31.5);
|
||||
});
|
||||
|
||||
test('screen width is less than 540px', () => {
|
||||
expect(postersPerRowForOverflowSquare(539, false)).toEqual(100 / 42);
|
||||
expect(postersPerRowForOverflowSquare(100, false)).toEqual(100 / 42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('landscape', () => {
|
||||
test('screen width greater or equal to 1700px', () => {
|
||||
expect(postersPerRowForOverflowSquare(1700, true)).toEqual(100 / 11.6);
|
||||
expect(postersPerRowForOverflowSquare(1701, true)).toEqual(100 / 11.6);
|
||||
});
|
||||
|
||||
test('screen width is less than 1700px', () => {
|
||||
expect(postersPerRowForOverflowSquare(1699, true)).toEqual(100 / 15.5);
|
||||
expect(postersPerRowForOverflowSquare(100, true)).toEqual(100 / 15.5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('overflow backdrop', () => {
|
||||
const postersPerRowForOverflowBackdrop = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowBackdrop', screenWidth, isLandscape, isTV));
|
||||
|
||||
test('television', () => {
|
||||
expect(postersPerRowForOverflowBackdrop(0, false, true)).toEqual( 100 / 23.3);
|
||||
});
|
||||
|
||||
describe('non-landscape', () => {
|
||||
test('screen width greater or equal to 1800px', () => {
|
||||
expect(postersPerRowForOverflowBackdrop(1800, false)).toEqual(100 / 23.5);
|
||||
expect(postersPerRowForOverflowBackdrop(1801, false)).toEqual(100 / 23.5);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1400px', () => {
|
||||
expect(postersPerRowForOverflowBackdrop(1400, false)).toEqual(100 / 30);
|
||||
expect(postersPerRowForOverflowBackdrop(1401, false)).toEqual(100 / 30);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 760px', () => {
|
||||
expect(postersPerRowForOverflowBackdrop(760, false)).toEqual(100 / 40);
|
||||
expect(postersPerRowForOverflowBackdrop(761, false)).toEqual(100 / 40);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 640px', () => {
|
||||
expect(postersPerRowForOverflowBackdrop(640, false)).toEqual(100 / 56);
|
||||
expect(postersPerRowForOverflowBackdrop(641, false)).toEqual(100 / 56);
|
||||
});
|
||||
|
||||
test('screen width is less than 640px', () => {
|
||||
expect(postersPerRowForOverflowBackdrop(639, false)).toEqual(100 / 72);
|
||||
expect(postersPerRowForOverflowBackdrop(100, false)).toEqual(100 / 72);
|
||||
});
|
||||
});
|
||||
|
||||
describe('landscape', () => {
|
||||
test('screen width greater or equal to 1700px', () => {
|
||||
expect(postersPerRowForOverflowBackdrop(1700, true)).toEqual(100 / 18.5);
|
||||
expect(postersPerRowForOverflowBackdrop(1701, true)).toEqual(100 / 18.5);
|
||||
});
|
||||
|
||||
test('screen width is less than 1700px', () => {
|
||||
expect(postersPerRowForOverflowBackdrop(1699, true)).toEqual(100 / 23.3);
|
||||
expect(postersPerRowForOverflowBackdrop(100, true)).toEqual(100 / 23.3);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -10,28 +10,28 @@ const createLinkElement = (activeTab: string) => ({
|
|||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'useredit' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('useredit.html', true);">
|
||||
onclick="Dashboard.navigate('/dashboard/users/profile', true);">
|
||||
${globalize.translate('Profile')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userlibraryaccess' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('userlibraryaccess.html', true);">
|
||||
onclick="Dashboard.navigate('/dashboard/users/access', true);">
|
||||
${globalize.translate('TabAccess')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userparentalcontrol' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('userparentalcontrol.html', true);">
|
||||
onclick="Dashboard.navigate('/dashboard/users/parentalcontrol', true);">
|
||||
${globalize.translate('TabParentalControl')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userpassword' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('userpassword.html', true);">
|
||||
onclick="Dashboard.navigate('/dashboard/users/password', true);">
|
||||
${globalize.translate('HeaderPassword')}
|
||||
</a>`
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl
|
|||
__html: `<a
|
||||
is="emby-linkbutton"
|
||||
class="cardContent"
|
||||
href="#/useredit.html?userId=${user.Id}"
|
||||
href="#/dashboard/users/profile?userId=${user.Id}"
|
||||
>
|
||||
${renderImgUrl}
|
||||
</a>`
|
||||
|
|
|
@ -1,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,
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
92
src/components/homesections/sections/activeRecordings.ts
Normal file
92
src/components/homesections/sections/activeRecordings.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
import type { ApiClient } from 'jellyfin-apiclient';
|
||||
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import type { SectionContainerElement, SectionOptions } from './section';
|
||||
|
||||
function getLatestRecordingsFetchFn(
|
||||
serverId: string,
|
||||
activeRecordingsOnly: boolean,
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return function () {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
return apiClient.getLiveTvRecordings({
|
||||
userId: apiClient.getCurrentUserId(),
|
||||
Limit: enableOverflow ? 12 : 5,
|
||||
Fields: 'PrimaryImageAspectRatio,BasicSyncInfo',
|
||||
EnableTotalRecordCount: false,
|
||||
IsLibraryItem: activeRecordingsOnly ? null : false,
|
||||
IsInProgress: activeRecordingsOnly ? true : null
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getLatestRecordingItemsHtml(
|
||||
activeRecordingsOnly: boolean,
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return function (items: BaseItemDto[]) {
|
||||
return cardBuilder.getCardsHtml({
|
||||
items: items,
|
||||
shape: enableOverflow ? 'autooverflow' : 'auto',
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
coverImage: true,
|
||||
lazy: true,
|
||||
showDetailsMenu: true,
|
||||
centerText: true,
|
||||
overlayText: false,
|
||||
showYear: true,
|
||||
lines: 2,
|
||||
overlayPlayButton: !activeRecordingsOnly,
|
||||
allowBottomPadding: !enableOverflow,
|
||||
preferThumb: true,
|
||||
cardLayout: false,
|
||||
overlayMoreButton: activeRecordingsOnly,
|
||||
action: activeRecordingsOnly ? 'none' : null,
|
||||
centerPlayButton: activeRecordingsOnly
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function loadRecordings(
|
||||
elem: HTMLElement,
|
||||
activeRecordingsOnly: boolean,
|
||||
apiClient: ApiClient,
|
||||
options: SectionOptions
|
||||
) {
|
||||
const title = activeRecordingsOnly ?
|
||||
globalize.translate('HeaderActiveRecordings') :
|
||||
globalize.translate('HeaderLatestRecordings');
|
||||
|
||||
let html = '';
|
||||
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + title + '</h2>';
|
||||
html += '</div>';
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x">';
|
||||
}
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
elem.classList.add('hide');
|
||||
elem.innerHTML = html;
|
||||
|
||||
const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer');
|
||||
if (!itemsContainer) return;
|
||||
itemsContainer.fetchData = getLatestRecordingsFetchFn(apiClient.serverId(), activeRecordingsOnly, options);
|
||||
itemsContainer.getItemsHtml = getLatestRecordingItemsHtml(activeRecordingsOnly, options);
|
||||
itemsContainer.parentContainer = elem;
|
||||
}
|
36
src/components/homesections/sections/libraryButtons.ts
Normal file
36
src/components/homesections/sections/libraryButtons.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
import escapeHtml from 'escape-html';
|
||||
|
||||
import imageLoader from 'components/images/imageLoader';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import globalize from 'scripts/globalize';
|
||||
import imageHelper from 'scripts/imagehelper';
|
||||
|
||||
function getLibraryButtonsHtml(items: BaseItemDto[]) {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="verticalSection verticalSection-extrabottompadding">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + globalize.translate('HeaderMyMedia') + '</h2>';
|
||||
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x" data-multiselect="false">';
|
||||
|
||||
// library card background images
|
||||
for (let i = 0, length = items.length; i < length; i++) {
|
||||
const item = items[i];
|
||||
const icon = imageHelper.getLibraryIcon(item.CollectionType);
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(item) + '" class="raised homeLibraryButton"><span class="material-icons homeLibraryIcon ' + icon + '" aria-hidden="true"></span><span class="homeLibraryText">' + escapeHtml(item.Name) + '</span></a>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
export function loadLibraryButtons(elem: HTMLElement, userViews: BaseItemDto[]) {
|
||||
elem.classList.remove('verticalSection');
|
||||
const html = getLibraryButtonsHtml(userViews);
|
||||
|
||||
elem.innerHTML = html;
|
||||
imageLoader.lazyChildren(elem);
|
||||
}
|
46
src/components/homesections/sections/libraryTiles.ts
Normal file
46
src/components/homesections/sections/libraryTiles.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import imageLoader from 'components/images/imageLoader';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { getBackdropShape } from 'utils/card';
|
||||
|
||||
import type { SectionOptions } from './section';
|
||||
|
||||
export function loadLibraryTiles(
|
||||
elem: HTMLElement,
|
||||
userViews: BaseItemDto[],
|
||||
{
|
||||
enableOverflow
|
||||
}: SectionOptions
|
||||
) {
|
||||
let html = '';
|
||||
if (userViews.length) {
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + globalize.translate('HeaderMyMedia') + '</h2>';
|
||||
if (enableOverflow) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right focuscontainer-x vertical-wrap">';
|
||||
}
|
||||
|
||||
html += cardBuilder.getCardsHtml({
|
||||
items: userViews,
|
||||
shape: getBackdropShape(enableOverflow),
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
overlayText: false,
|
||||
lazy: true,
|
||||
transition: false,
|
||||
allowBottomPadding: !enableOverflow
|
||||
});
|
||||
|
||||
if (enableOverflow) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
elem.innerHTML = html;
|
||||
imageLoader.lazyChildren(elem);
|
||||
}
|
181
src/components/homesections/sections/liveTv.ts
Normal file
181
src/components/homesections/sections/liveTv.ts
Normal file
|
@ -0,0 +1,181 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
|
||||
import type { ApiClient } from 'jellyfin-apiclient';
|
||||
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { getBackdropShape } from 'utils/card';
|
||||
|
||||
import type { SectionContainerElement, SectionOptions } from './section';
|
||||
|
||||
function getOnNowFetchFn(
|
||||
serverId: string
|
||||
) {
|
||||
return function () {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
return apiClient.getLiveTvRecommendedPrograms({
|
||||
userId: apiClient.getCurrentUserId(),
|
||||
IsAiring: true,
|
||||
limit: 24,
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Thumb,Backdrop',
|
||||
EnableTotalRecordCount: false,
|
||||
Fields: 'ChannelInfo,PrimaryImageAspectRatio'
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getOnNowItemsHtmlFn(
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return (items: BaseItemDto[]) => (
|
||||
cardBuilder.getCardsHtml({
|
||||
items: items,
|
||||
preferThumb: 'auto',
|
||||
inheritThumb: false,
|
||||
shape: (enableOverflow ? 'autooverflow' : 'auto'),
|
||||
showParentTitleOrTitle: true,
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
coverImage: true,
|
||||
overlayText: false,
|
||||
allowBottomPadding: !enableOverflow,
|
||||
showAirTime: true,
|
||||
showChannelName: false,
|
||||
showAirDateTime: false,
|
||||
showAirEndTime: true,
|
||||
defaultShape: getBackdropShape(enableOverflow),
|
||||
lines: 3,
|
||||
overlayPlayButton: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function buildSection(
|
||||
elem: HTMLElement,
|
||||
serverId: string,
|
||||
options: SectionOptions
|
||||
) {
|
||||
let html = '';
|
||||
|
||||
elem.classList.remove('padded-left');
|
||||
elem.classList.remove('padded-right');
|
||||
elem.classList.remove('padded-bottom');
|
||||
elem.classList.remove('verticalSection');
|
||||
|
||||
html += '<div class="verticalSection">';
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('LiveTV') + '</h2>';
|
||||
html += '</div>';
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true" data-scrollbuttons="false">';
|
||||
html += '<div class="padded-top padded-bottom scrollSlider focuscontainer-x">';
|
||||
} else {
|
||||
html += '<div class="padded-top padded-bottom focuscontainer-x">';
|
||||
}
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId,
|
||||
section: 'programs'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Programs') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId,
|
||||
section: 'guide'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Guide') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId,
|
||||
section: 'channels'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Channels') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('recordedtv', {
|
||||
serverId
|
||||
}) + '" class="raised"><span>' + globalize.translate('Recordings') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId,
|
||||
section: 'dvrschedule'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Schedule') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId,
|
||||
section: 'seriesrecording'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Series') + '</span></a>';
|
||||
|
||||
html += '</div>';
|
||||
if (options.enableOverflow) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="verticalSection">';
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||
|
||||
if (!layoutManager.tv) {
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId,
|
||||
section: 'onnow'
|
||||
}) + '" class="more button-flat button-flat-mini sectionTitleTextButton">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||
html += globalize.translate('HeaderOnNow');
|
||||
html += '</h2>';
|
||||
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
} else {
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('HeaderOnNow') + '</h2>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x">';
|
||||
}
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
elem.innerHTML = html;
|
||||
|
||||
const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer');
|
||||
if (!itemsContainer) return;
|
||||
itemsContainer.parentContainer = elem;
|
||||
itemsContainer.fetchData = getOnNowFetchFn(serverId);
|
||||
itemsContainer.getItemsHtml = getOnNowItemsHtmlFn(options);
|
||||
}
|
||||
|
||||
export function loadLiveTV(
|
||||
elem: HTMLElement,
|
||||
apiClient: ApiClient,
|
||||
user: UserDto,
|
||||
options: SectionOptions
|
||||
) {
|
||||
if (!user.Policy?.EnableLiveTvAccess) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return apiClient.getLiveTvRecommendedPrograms({
|
||||
userId: apiClient.getCurrentUserId(),
|
||||
IsAiring: true,
|
||||
limit: 1,
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Thumb,Backdrop',
|
||||
EnableTotalRecordCount: false,
|
||||
Fields: 'ChannelInfo,PrimaryImageAspectRatio'
|
||||
}).then(function (result) {
|
||||
if (result.Items?.length) {
|
||||
buildSection(elem, apiClient.serverId(), options);
|
||||
}
|
||||
});
|
||||
}
|
106
src/components/homesections/sections/nextUp.ts
Normal file
106
src/components/homesections/sections/nextUp.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
import type { ApiClient } from 'jellyfin-apiclient';
|
||||
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import globalize from 'scripts/globalize';
|
||||
import type { UserSettings } from 'scripts/settings/userSettings';
|
||||
import { getBackdropShape } from 'utils/card';
|
||||
|
||||
import type { SectionContainerElement, SectionOptions } from './section';
|
||||
|
||||
function getNextUpFetchFn(
|
||||
serverId: string,
|
||||
userSettings: UserSettings,
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return function () {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
const oldestDateForNextUp = new Date();
|
||||
oldestDateForNextUp.setDate(oldestDateForNextUp.getDate() - userSettings.maxDaysForNextUp());
|
||||
return apiClient.getNextUpEpisodes({
|
||||
Limit: enableOverflow ? 24 : 15,
|
||||
Fields: 'PrimaryImageAspectRatio,DateCreated,BasicSyncInfo,Path,MediaSourceCount',
|
||||
UserId: apiClient.getCurrentUserId(),
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
||||
EnableTotalRecordCount: false,
|
||||
DisableFirstEpisode: false,
|
||||
NextUpDateCutoff: oldestDateForNextUp.toISOString(),
|
||||
EnableRewatching: userSettings.enableRewatchingInNextUp()
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getNextUpItemsHtmlFn(
|
||||
useEpisodeImages: boolean,
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return function (items: BaseItemDto[]) {
|
||||
const cardLayout = false;
|
||||
return cardBuilder.getCardsHtml({
|
||||
items: items,
|
||||
preferThumb: true,
|
||||
inheritThumb: !useEpisodeImages,
|
||||
shape: getBackdropShape(enableOverflow),
|
||||
overlayText: false,
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
lazy: true,
|
||||
overlayPlayButton: true,
|
||||
context: 'home',
|
||||
centerText: !cardLayout,
|
||||
allowBottomPadding: !enableOverflow,
|
||||
cardLayout: cardLayout
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function loadNextUp(
|
||||
elem: HTMLElement,
|
||||
apiClient: ApiClient,
|
||||
userSettings: UserSettings,
|
||||
options: SectionOptions
|
||||
) {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||
if (!layoutManager.tv) {
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('nextup', {
|
||||
serverId: apiClient.serverId()
|
||||
}) + '" class="button-flat button-flat-mini sectionTitleTextButton">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||
html += globalize.translate('NextUp');
|
||||
html += '</h2>';
|
||||
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
} else {
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||
html += globalize.translate('NextUp');
|
||||
html += '</h2>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x" data-monitor="videoplayback,markplayed">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x" data-monitor="videoplayback,markplayed">';
|
||||
}
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
elem.classList.add('hide');
|
||||
elem.innerHTML = html;
|
||||
|
||||
const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer');
|
||||
if (!itemsContainer) return;
|
||||
itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings, options);
|
||||
itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), options);
|
||||
itemsContainer.parentContainer = elem;
|
||||
}
|
158
src/components/homesections/sections/recentlyAdded.ts
Normal file
158
src/components/homesections/sections/recentlyAdded.ts
Normal file
|
@ -0,0 +1,158 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
|
||||
import escapeHtml from 'escape-html';
|
||||
import type { ApiClient } from 'jellyfin-apiclient';
|
||||
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import globalize from 'scripts/globalize';
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
|
||||
|
||||
import type { SectionContainerElement, SectionOptions } from './section';
|
||||
|
||||
function getFetchLatestItemsFn(
|
||||
serverId: string,
|
||||
parentId: string | undefined,
|
||||
collectionType: string | null | undefined,
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return function () {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
let limit = 16;
|
||||
|
||||
if (enableOverflow) {
|
||||
if (collectionType === 'music') {
|
||||
limit = 30;
|
||||
}
|
||||
} else if (collectionType === 'tvshows') {
|
||||
limit = 5;
|
||||
} else if (collectionType === 'music') {
|
||||
limit = 9;
|
||||
} else {
|
||||
limit = 8;
|
||||
}
|
||||
|
||||
const options = {
|
||||
Limit: limit,
|
||||
Fields: 'PrimaryImageAspectRatio,BasicSyncInfo,Path',
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Backdrop,Thumb',
|
||||
ParentId: parentId
|
||||
};
|
||||
|
||||
return apiClient.getLatestItems(options);
|
||||
};
|
||||
}
|
||||
|
||||
function getLatestItemsHtmlFn(
|
||||
itemType: BaseItemKind | undefined,
|
||||
viewType: string | null | undefined,
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return function (items: BaseItemDto[]) {
|
||||
const cardLayout = false;
|
||||
let shape;
|
||||
if (itemType === 'Channel' || viewType === 'movies' || viewType === 'books' || viewType === 'tvshows') {
|
||||
shape = getPortraitShape(enableOverflow);
|
||||
} else if (viewType === 'music' || viewType === 'homevideos') {
|
||||
shape = getSquareShape(enableOverflow);
|
||||
} else {
|
||||
shape = getBackdropShape(enableOverflow);
|
||||
}
|
||||
|
||||
return cardBuilder.getCardsHtml({
|
||||
items: items,
|
||||
shape: shape,
|
||||
preferThumb: viewType !== 'movies' && viewType !== 'tvshows' && itemType !== 'Channel' && viewType !== 'music' ? 'auto' : null,
|
||||
showUnplayedIndicator: false,
|
||||
showChildCountIndicator: true,
|
||||
context: 'home',
|
||||
overlayText: false,
|
||||
centerText: !cardLayout,
|
||||
overlayPlayButton: viewType !== 'photos',
|
||||
allowBottomPadding: !enableOverflow && !cardLayout,
|
||||
cardLayout: cardLayout,
|
||||
showTitle: viewType !== 'photos',
|
||||
showYear: viewType === 'movies' || viewType === 'tvshows' || !viewType,
|
||||
showParentTitle: viewType === 'music' || viewType === 'tvshows' || !viewType || (cardLayout && (viewType === 'tvshows')),
|
||||
lines: 2
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function renderLatestSection(
|
||||
elem: HTMLElement,
|
||||
apiClient: ApiClient,
|
||||
user: UserDto,
|
||||
parent: BaseItemDto,
|
||||
options: SectionOptions
|
||||
) {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||
if (!layoutManager.tv) {
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(parent, {
|
||||
section: 'latest'
|
||||
}) + '" class="more button-flat button-flat-mini sectionTitleTextButton">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||
html += globalize.translate('LatestFromLibrary', escapeHtml(parent.Name));
|
||||
html += '</h2>';
|
||||
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
} else {
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('LatestFromLibrary', escapeHtml(parent.Name)) + '</h2>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer focuscontainer-x padded-left padded-right vertical-wrap">';
|
||||
}
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
elem.innerHTML = html;
|
||||
|
||||
const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer');
|
||||
if (!itemsContainer) return;
|
||||
itemsContainer.fetchData = getFetchLatestItemsFn(apiClient.serverId(), parent.Id, parent.CollectionType, options);
|
||||
itemsContainer.getItemsHtml = getLatestItemsHtmlFn(parent.Type, parent.CollectionType, options);
|
||||
itemsContainer.parentContainer = elem;
|
||||
}
|
||||
|
||||
export function loadRecentlyAdded(
|
||||
elem: HTMLElement,
|
||||
apiClient: ApiClient,
|
||||
user: UserDto,
|
||||
userViews: BaseItemDto[],
|
||||
options: SectionOptions
|
||||
) {
|
||||
elem.classList.remove('verticalSection');
|
||||
const excludeViewTypes = ['playlists', 'livetv', 'boxsets', 'channels'];
|
||||
const userExcludeItems = user.Configuration?.LatestItemsExcludes ?? [];
|
||||
|
||||
userViews.forEach(item => {
|
||||
if (!item.Id || userExcludeItems.indexOf(item.Id) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.CollectionType || excludeViewTypes.indexOf(item.CollectionType) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frag = document.createElement('div');
|
||||
frag.classList.add('verticalSection');
|
||||
frag.classList.add('hide');
|
||||
elem.appendChild(frag);
|
||||
|
||||
renderLatestSection(frag, apiClient, user, item, options);
|
||||
});
|
||||
}
|
105
src/components/homesections/sections/resume.ts
Normal file
105
src/components/homesections/sections/resume.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import type { ApiClient } from 'jellyfin-apiclient';
|
||||
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import globalize from 'scripts/globalize';
|
||||
import type { UserSettings } from 'scripts/settings/userSettings';
|
||||
import { getBackdropShape, getPortraitShape } from 'utils/card';
|
||||
|
||||
import type { SectionContainerElement, SectionOptions } from './section';
|
||||
|
||||
const dataMonitorHints: Record<string, string> = {
|
||||
Audio: 'audioplayback,markplayed',
|
||||
Video: 'videoplayback,markplayed'
|
||||
};
|
||||
|
||||
function getItemsToResumeFn(
|
||||
mediaType: BaseItemKind,
|
||||
serverId: string,
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return function () {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
const limit = enableOverflow ? 12 : 5;
|
||||
|
||||
const options = {
|
||||
Limit: limit,
|
||||
Recursive: true,
|
||||
Fields: 'PrimaryImageAspectRatio,BasicSyncInfo',
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Backdrop,Thumb',
|
||||
EnableTotalRecordCount: false,
|
||||
MediaTypes: mediaType
|
||||
};
|
||||
|
||||
return apiClient.getResumableItems(apiClient.getCurrentUserId(), options);
|
||||
};
|
||||
}
|
||||
|
||||
function getItemsToResumeHtmlFn(
|
||||
useEpisodeImages: boolean,
|
||||
mediaType: BaseItemKind,
|
||||
{ enableOverflow }: SectionOptions
|
||||
) {
|
||||
return function (items: BaseItemDto[]) {
|
||||
const cardLayout = false;
|
||||
return cardBuilder.getCardsHtml({
|
||||
items: items,
|
||||
preferThumb: true,
|
||||
inheritThumb: !useEpisodeImages,
|
||||
shape: (mediaType === 'Book') ?
|
||||
getPortraitShape(enableOverflow) :
|
||||
getBackdropShape(enableOverflow),
|
||||
overlayText: false,
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
lazy: true,
|
||||
showDetailsMenu: true,
|
||||
overlayPlayButton: true,
|
||||
context: 'home',
|
||||
centerText: !cardLayout,
|
||||
allowBottomPadding: false,
|
||||
cardLayout: cardLayout,
|
||||
showYear: true,
|
||||
lines: 2
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function loadResume(
|
||||
elem: HTMLElement,
|
||||
apiClient: ApiClient,
|
||||
titleLabel: string,
|
||||
mediaType: BaseItemKind,
|
||||
userSettings: UserSettings,
|
||||
options: SectionOptions
|
||||
) {
|
||||
let html = '';
|
||||
|
||||
const dataMonitor = dataMonitorHints[mediaType] ?? 'markplayed';
|
||||
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + globalize.translate(titleLabel) + '</h2>';
|
||||
if (options.enableOverflow) {
|
||||
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">';
|
||||
html += `<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x" data-monitor="${dataMonitor}">`;
|
||||
} else {
|
||||
html += `<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right vertical-wrap focuscontainer-x" data-monitor="${dataMonitor}">`;
|
||||
}
|
||||
|
||||
if (options.enableOverflow) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
elem.classList.add('hide');
|
||||
elem.innerHTML = html;
|
||||
|
||||
const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer');
|
||||
if (!itemsContainer) return;
|
||||
itemsContainer.fetchData = getItemsToResumeFn(mediaType, apiClient.serverId(), options);
|
||||
itemsContainer.getItemsHtml = getItemsToResumeHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), mediaType, options);
|
||||
itemsContainer.parentContainer = elem;
|
||||
}
|
12
src/components/homesections/sections/section.d.ts
vendored
Normal file
12
src/components/homesections/sections/section.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto-query-result';
|
||||
|
||||
export interface SectionOptions {
|
||||
enableOverflow: boolean
|
||||
}
|
||||
|
||||
export type SectionContainerElement = {
|
||||
fetchData: () => Promise<BaseItemDtoQueryResult | BaseItemDto[]>
|
||||
getItemsHtml: (items: BaseItemDto[]) => void
|
||||
parentContainer: HTMLElement
|
||||
} & Element;
|
|
@ -444,7 +444,7 @@ function executeCommand(item, id, options) {
|
|||
});
|
||||
break;
|
||||
case 'multiSelect':
|
||||
import('./multiSelect/multiSelect').then(({ startMultiSelect: startMultiSelect }) => {
|
||||
import('./multiSelect/multiSelect').then(({ startMultiSelect }) => {
|
||||
const card = dom.parentWithClass(options.positionTo, 'card');
|
||||
startMultiSelect(card);
|
||||
});
|
||||
|
|
|
@ -527,7 +527,7 @@ class AppRouter {
|
|||
}
|
||||
|
||||
if (item === 'manageserver') {
|
||||
return '#/dashboard.html';
|
||||
return '#/dashboard';
|
||||
}
|
||||
|
||||
if (item === 'recordedtv') {
|
||||
|
|
|
@ -49,7 +49,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
|||
const getDefaultParameters = useCallback(() => ({
|
||||
ParentId: parentId,
|
||||
searchTerm: query,
|
||||
Limit: 24,
|
||||
Limit: 100,
|
||||
Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount',
|
||||
Recursive: true,
|
||||
EnableTotalRecordCount: false,
|
||||
|
|
|
@ -115,7 +115,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
|
|||
<MenuItem
|
||||
key='admin-dashboard-link'
|
||||
component={Link}
|
||||
to='/dashboard.html'
|
||||
to='/dashboard'
|
||||
onClick={onMenuClose}
|
||||
>
|
||||
|
||||
|
@ -127,7 +127,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
|
|||
<MenuItem
|
||||
key='admin-metadata-link'
|
||||
component={Link}
|
||||
to='/edititemmetadata.html'
|
||||
to='/metadata'
|
||||
onClick={onMenuClose}
|
||||
>
|
||||
<ListItemIcon>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="dashboardSections" style="padding-top:.5em;">
|
||||
<div class="dashboardColumn dashboardColumn-2-60 dashboardColumn-3-46">
|
||||
<div class="dashboardSection">
|
||||
<a is="emby-linkbutton" href="#/dashboardgeneral.html" class="button-flat sectionTitleTextButton">
|
||||
<a is="emby-linkbutton" href="#/dashboard/settings" class="button-flat sectionTitleTextButton">
|
||||
<h3>${TabServer}</h3>
|
||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
|
||||
<div class="dashboardSection">
|
||||
<a is="emby-linkbutton" href="#/devices.html" class="button-flat sectionTitleTextButton">
|
||||
<a is="emby-linkbutton" href="#/dashboard/devices" class="button-flat sectionTitleTextButton">
|
||||
<h3>${HeaderActiveDevices}</h3>
|
||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
@ -70,7 +70,7 @@
|
|||
</div>
|
||||
|
||||
<div class="dashboardSection">
|
||||
<a is="emby-linkbutton" href="#/dashboardgeneral.html" class="button-flat sectionTitleTextButton">
|
||||
<a is="emby-linkbutton" href="#/dashboard/settings" class="button-flat sectionTitleTextButton">
|
||||
<h3>${HeaderPaths}</h3>
|
||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
|
|
@ -73,7 +73,7 @@ function showDeviceMenu(view, btn, deviceId) {
|
|||
callback: function (id) {
|
||||
switch (id) {
|
||||
case 'open':
|
||||
Dashboard.navigate('device.html?id=' + deviceId);
|
||||
Dashboard.navigate('dashboard/devices/edit?id=' + deviceId);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
|
@ -94,7 +94,7 @@ function load(page, devices) {
|
|||
deviceHtml += '<div class="cardBox visualCardBox">';
|
||||
deviceHtml += '<div class="cardScalable">';
|
||||
deviceHtml += '<div class="cardPadder cardPadder-backdrop"></div>';
|
||||
deviceHtml += `<a is="emby-linkbutton" href="#!/device.html?id=${escapeHtml(device.Id)}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`;
|
||||
deviceHtml += `<a is="emby-linkbutton" href="#/dashboard/devices/edit?id=${escapeHtml(device.Id)}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`;
|
||||
// audit note: getDeviceIcon returns static text
|
||||
const iconUrl = imageHelper.getDeviceIcon(device);
|
||||
|
||||
|
|
|
@ -264,7 +264,7 @@
|
|||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
<button is="emby-button" type="button" class="button-cancel raised block" onclick="Dashboard.navigate('dlnaprofiles.html');">
|
||||
<button is="emby-button" type="button" class="button-cancel raised block" onclick="Dashboard.navigate('dashboard/dlna/profiles');">
|
||||
<span>${ButtonCancel}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -639,7 +639,7 @@ function saveProfile(page, profile) {
|
|||
data: JSON.stringify(profile),
|
||||
contentType: 'application/json'
|
||||
}).then(function () {
|
||||
Dashboard.navigate('dlnaprofiles.html');
|
||||
Dashboard.navigate('dashboard/dlna/profiles');
|
||||
}, Dashboard.processErrorResponse);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${HeaderCustomDlnaProfiles}</h2>
|
||||
<a is="emby-linkbutton" href="#/dlnaprofile.html" class="fab submit" style="margin:0 0 0 1em">
|
||||
<a is="emby-linkbutton" href="#/dashboard/dlna/profiles/edit" class="fab submit" style="margin:0 0 0 1em">
|
||||
<span class="material-icons add" aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -40,7 +40,7 @@ function renderProfiles(page, element, profiles) {
|
|||
html += '<div class="listItem listItem-border">';
|
||||
html += '<span class="listItemIcon material-icons live_tv" aria-hidden="true"></span>';
|
||||
html += '<div class="listItemBody two-line">';
|
||||
html += "<a is='emby-linkbutton' style='padding:0;margin:0;' data-ripple='false' class='clearLink' href='#/dlnaprofile.html?id=" + profile.Id + "'>";
|
||||
html += "<a is='emby-linkbutton' style='padding:0;margin:0;' data-ripple='false' class='clearLink' href='#/dashboard/dlna/profiles/edit?id=" + profile.Id + "'>";
|
||||
html += '<div>' + escapeHtml(profile.Name) + '</div>';
|
||||
html += '</a>';
|
||||
html += '</div>';
|
||||
|
@ -78,10 +78,10 @@ function deleteProfile(page, id) {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dlnasettings.html',
|
||||
href: '#/dashboard/dlna',
|
||||
name: globalize.translate('Settings')
|
||||
}, {
|
||||
href: '#/dlnaprofiles.html',
|
||||
href: '#/dashboard/dlna/profiles',
|
||||
name: globalize.translate('TabProfiles')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -37,10 +37,10 @@ function onSubmit() {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dlnasettings.html',
|
||||
href: '#/dashboard/dlna',
|
||||
name: globalize.translate('Settings')
|
||||
}, {
|
||||
href: '#/dlnaprofiles.html',
|
||||
href: '#/dashboard/dlna/profiles',
|
||||
name: globalize.translate('TabProfiles')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -167,13 +167,13 @@ function setDecodingCodecsVisible(context, value) {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/encodingsettings.html',
|
||||
href: '#/dashboard/playback/transcoding',
|
||||
name: globalize.translate('Transcoding')
|
||||
}, {
|
||||
href: '#/playbackconfiguration.html',
|
||||
href: '#/dashboard/playback/resume',
|
||||
name: globalize.translate('ButtonResume')
|
||||
}, {
|
||||
href: '#/streamingsettings.html',
|
||||
href: '#/dashboard/playback/streaming',
|
||||
name: globalize.translate('TabStreaming')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -360,16 +360,16 @@ function getVirtualFolderHtml(page, virtualFolder, index) {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/library.html',
|
||||
href: '#/dashboard/libraries',
|
||||
name: globalize.translate('HeaderLibraries')
|
||||
}, {
|
||||
href: '#/librarydisplay.html',
|
||||
href: '#/dashboard/libraries/display',
|
||||
name: globalize.translate('Display')
|
||||
}, {
|
||||
href: '#/metadataimages.html',
|
||||
href: '#/dashboard/libraries/metadata',
|
||||
name: globalize.translate('Metadata')
|
||||
}, {
|
||||
href: '#/metadatanfo.html',
|
||||
href: '#/dashboard/libraries/nfo',
|
||||
name: globalize.translate('TabNfoSettings')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -7,16 +7,16 @@ import Dashboard from '../../utils/dashboard';
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/library.html',
|
||||
href: '#/dashboard/libraries',
|
||||
name: globalize.translate('HeaderLibraries')
|
||||
}, {
|
||||
href: '#/librarydisplay.html',
|
||||
href: '#/dashboard/libraries/display',
|
||||
name: globalize.translate('Display')
|
||||
}, {
|
||||
href: '#/metadataimages.html',
|
||||
href: '#/dashboard/libraries/metadata',
|
||||
name: globalize.translate('Metadata')
|
||||
}, {
|
||||
href: '#/metadatanfo.html',
|
||||
href: '#/dashboard/libraries/nfo',
|
||||
name: globalize.translate('TabNfoSettings')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -88,16 +88,16 @@ function onSubmit() {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/library.html',
|
||||
href: '#/dashboard/libraries',
|
||||
name: globalize.translate('HeaderLibraries')
|
||||
}, {
|
||||
href: '#/librarydisplay.html',
|
||||
href: '#/dashboard/libraries/display',
|
||||
name: globalize.translate('Display')
|
||||
}, {
|
||||
href: '#/metadataimages.html',
|
||||
href: '#/dashboard/libraries/metadata',
|
||||
name: globalize.translate('Metadata')
|
||||
}, {
|
||||
href: '#/metadatanfo.html',
|
||||
href: '#/dashboard/libraries/nfo',
|
||||
name: globalize.translate('TabNfoSettings')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -46,16 +46,16 @@ function showConfirmMessage() {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/library.html',
|
||||
href: '#/dashboard/libraries',
|
||||
name: globalize.translate('HeaderLibraries')
|
||||
}, {
|
||||
href: '#/librarydisplay.html',
|
||||
href: '#/dashboard/libraries/display',
|
||||
name: globalize.translate('Display')
|
||||
}, {
|
||||
href: '#/metadataimages.html',
|
||||
href: '#/dashboard/libraries/metadata',
|
||||
name: globalize.translate('Metadata')
|
||||
}, {
|
||||
href: '#/metadatanfo.html',
|
||||
href: '#/dashboard/libraries/nfo',
|
||||
name: globalize.translate('TabNfoSettings')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -31,13 +31,13 @@ function onSubmit() {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/encodingsettings.html',
|
||||
href: '#/dashboard/playback/transcoding',
|
||||
name: globalize.translate('Transcoding')
|
||||
}, {
|
||||
href: '#/playbackconfiguration.html',
|
||||
href: '#/dashboard/playback/resume',
|
||||
name: globalize.translate('ButtonResume')
|
||||
}, {
|
||||
href: '#/streamingsettings.html',
|
||||
href: '#/dashboard/playback/streaming',
|
||||
name: globalize.translate('TabStreaming')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -120,7 +120,7 @@ function onSearchBarType(searchBar) {
|
|||
|
||||
function getPluginHtml(plugin, options, installedPlugins) {
|
||||
let html = '';
|
||||
let href = plugin.externalUrl ? plugin.externalUrl : '#/addplugin.html?name=' + encodeURIComponent(plugin.name) + '&guid=' + plugin.guid;
|
||||
let href = plugin.externalUrl ? plugin.externalUrl : '#/dashboard/plugins/add?name=' + encodeURIComponent(plugin.name) + '&guid=' + plugin.guid;
|
||||
|
||||
if (options.context) {
|
||||
href += '&context=' + options.context;
|
||||
|
@ -161,13 +161,13 @@ function getPluginHtml(plugin, options, installedPlugins) {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/installedplugins.html',
|
||||
href: '#/dashboard/plugins',
|
||||
name: globalize.translate('TabMyPlugins')
|
||||
}, {
|
||||
href: '#/availableplugins.html',
|
||||
href: '#/dashboard/plugins/catalog',
|
||||
name: globalize.translate('TabCatalog')
|
||||
}, {
|
||||
href: '#/repositories.html',
|
||||
href: '#/dashboard/plugins/repositories',
|
||||
name: globalize.translate('TabRepositories')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -130,7 +130,7 @@ function populateList(page, plugins, pluginConfigurationPages) {
|
|||
} else {
|
||||
html += '<div class="centerMessage">';
|
||||
html += '<h1>' + globalize.translate('MessageNoPluginsInstalled') + '</h1>';
|
||||
html += '<p><a is="emby-linkbutton" class="button-link" href="#/availableplugins.html">';
|
||||
html += '<p><a is="emby-linkbutton" class="button-link" href="#/dashboard/plugins/catalog">';
|
||||
html += globalize.translate('MessageBrowsePluginCatalog');
|
||||
html += '</a></p>';
|
||||
html += '</div>';
|
||||
|
@ -221,13 +221,13 @@ function reloadList(page) {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/installedplugins.html',
|
||||
href: '#/dashboard/plugins',
|
||||
name: globalize.translate('TabMyPlugins')
|
||||
}, {
|
||||
href: '#/availableplugins.html',
|
||||
href: '#/dashboard/plugins/catalog',
|
||||
name: globalize.translate('TabCatalog')
|
||||
}, {
|
||||
href: '#/repositories.html',
|
||||
href: '#/dashboard/plugins/repositories',
|
||||
name: globalize.translate('TabRepositories')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -105,13 +105,13 @@ function getRepositoryElement(repository) {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/installedplugins.html',
|
||||
href: '#/dashboard/plugins',
|
||||
name: globalize.translate('TabMyPlugins')
|
||||
}, {
|
||||
href: '#/availableplugins.html',
|
||||
href: '#/dashboard/plugins/catalog',
|
||||
name: globalize.translate('TabCatalog')
|
||||
}, {
|
||||
href: '#/repositories.html',
|
||||
href: '#/dashboard/plugins/repositories',
|
||||
name: globalize.translate('TabRepositories')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -53,12 +53,12 @@ function populateList(page, tasks) {
|
|||
html += '<div class="paperList">';
|
||||
}
|
||||
html += '<div class="listItem listItem-border scheduledTaskPaperIconItem" data-status="' + task.State + '">';
|
||||
html += "<a is='emby-linkbutton' style='margin:0;padding:0;' class='clearLink listItemIconContainer' href='scheduledtask.html?id=" + task.Id + "'>";
|
||||
html += "<a is='emby-linkbutton' style='margin:0;padding:0;' class='clearLink listItemIconContainer' href='/dashboard/tasks/edit?id=" + task.Id + "'>";
|
||||
html += '<span class="material-icons listItemIcon schedule" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
html += '<div class="listItemBody two-line">';
|
||||
const textAlignStyle = globalize.getIsRTL() ? 'right' : 'left';
|
||||
html += "<a class='clearLink' style='margin:0;padding:0;display:block;text-align:" + textAlignStyle + ";' is='emby-linkbutton' href='scheduledtask.html?id=" + task.Id + "'>";
|
||||
html += "<a class='clearLink' style='margin:0;padding:0;display:block;text-align:" + textAlignStyle + ";' is='emby-linkbutton' href='/dashboard/tasks/edit?id=" + task.Id + "'>";
|
||||
html += "<h3 class='listItemBodyText'>" + task.Name + '</h3>';
|
||||
html += "<div class='secondary listItemBodyText' id='taskProgress" + task.Id + "'>" + getTaskProgressHtml(task) + '</div>';
|
||||
html += '</a>';
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
<div id="serverActivityPage" data-role="page" class="page type-interior serverActivityPage noSecondaryNavPage" data-title="${HeaderActivity}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="verticalSection">
|
||||
<h2 class="sectionTitle"></h2>
|
||||
</div>
|
||||
<div class="readOnlyContent">
|
||||
<div class="paperList activityItems" data-activitylimit="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,32 +0,0 @@
|
|||
import ActivityLog from '../../components/activitylog';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import { toBoolean } from '../../utils/string.ts';
|
||||
|
||||
export default function (view, params) {
|
||||
let activityLog;
|
||||
|
||||
if (toBoolean(params.useractivity, true)) {
|
||||
view.querySelector('.activityItems').setAttribute('data-useractivity', 'true');
|
||||
view.querySelector('.sectionTitle').innerHTML = globalize.translate('HeaderActivity');
|
||||
} else {
|
||||
view.querySelector('.activityItems').setAttribute('data-useractivity', 'false');
|
||||
view.querySelector('.sectionTitle').innerHTML = globalize.translate('Alerts');
|
||||
}
|
||||
|
||||
view.addEventListener('viewshow', function () {
|
||||
if (!activityLog) {
|
||||
activityLog = new ActivityLog({
|
||||
serverId: ApiClient.serverId(),
|
||||
element: view.querySelector('.activityItems')
|
||||
});
|
||||
}
|
||||
});
|
||||
view.addEventListener('viewdestroy', function () {
|
||||
if (activityLog) {
|
||||
activityLog.destroy();
|
||||
}
|
||||
|
||||
activityLog = null;
|
||||
});
|
||||
}
|
||||
|
|
@ -22,13 +22,13 @@ function onSubmit() {
|
|||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/encodingsettings.html',
|
||||
href: '#/dashboard/playback/transcoding',
|
||||
name: globalize.translate('Transcoding')
|
||||
}, {
|
||||
href: '#/playbackconfiguration.html',
|
||||
href: '#/dashboard/playback/resume',
|
||||
name: globalize.translate('ButtonResume')
|
||||
}, {
|
||||
href: '#/streamingsettings.html',
|
||||
href: '#/dashboard/playback/streaming',
|
||||
name: globalize.translate('TabStreaming')
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue