mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge branch 'master' into Sorting-Only-Thumbnail-Fix-5584
This commit is contained in:
commit
0f8d29a573
193 changed files with 5197 additions and 1973 deletions
|
@ -16,7 +16,7 @@ jobs:
|
||||||
- task: NodeTool@0
|
- task: NodeTool@0
|
||||||
displayName: 'Install Node'
|
displayName: 'Install Node'
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: '16.x'
|
versionSpec: '20.x'
|
||||||
|
|
||||||
- task: Cache@2
|
- task: Cache@2
|
||||||
displayName: 'Cache node_modules'
|
displayName: 'Cache node_modules'
|
||||||
|
|
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'],
|
'no-unused-expressions': ['off'],
|
||||||
'@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
|
'@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
|
||||||
'no-unused-private-class-members': ['error'],
|
'no-unused-private-class-members': ['error'],
|
||||||
|
'no-useless-rename': ['error'],
|
||||||
'no-useless-constructor': ['off'],
|
'no-useless-constructor': ['off'],
|
||||||
'@typescript-eslint/no-useless-constructor': ['error'],
|
'@typescript-eslint/no-useless-constructor': ['error'],
|
||||||
'no-var': ['error'],
|
'no-var': ['error'],
|
||||||
|
|
8
.github/workflows/lint.yml
vendored
8
.github/workflows/lint.yml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
||||||
- name: Setup node environment
|
- name: Setup node environment
|
||||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 20
|
||||||
check-latest: true
|
check-latest: true
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ jobs:
|
||||||
- name: Setup node environment
|
- name: Setup node environment
|
||||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 20
|
||||||
check-latest: true
|
check-latest: true
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ jobs:
|
||||||
- name: Setup node environment
|
- name: Setup node environment
|
||||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 20
|
||||||
check-latest: true
|
check-latest: true
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ jobs:
|
||||||
- name: Setup node environment
|
- name: Setup node environment
|
||||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 20
|
||||||
check-latest: true
|
check-latest: true
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
|
|
5
.github/workflows/tsc.yml
vendored
5
.github/workflows/tsc.yml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
||||||
- name: Setup node environment
|
- name: Setup node environment
|
||||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 20
|
||||||
check-latest: true
|
check-latest: true
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
|
@ -27,3 +27,6 @@ jobs:
|
||||||
|
|
||||||
- name: Run tsc
|
- name: Run tsc
|
||||||
run: npm run build:check
|
run: npm run build:check
|
||||||
|
|
||||||
|
- name: Run test suite
|
||||||
|
run: npm run test
|
||||||
|
|
|
@ -67,6 +67,7 @@
|
||||||
- [sleepycatcoding](https://github.com/sleepycatcoding)
|
- [sleepycatcoding](https://github.com/sleepycatcoding)
|
||||||
- [TheMelmacian](https://github.com/TheMelmacian)
|
- [TheMelmacian](https://github.com/TheMelmacian)
|
||||||
- [v0idMrK](https://github.com/v0idMrK)
|
- [v0idMrK](https://github.com/v0idMrK)
|
||||||
|
- [tehciolo](https://github.com/tehciolo)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
|
|
@ -77,8 +77,9 @@ Jellyfin Web is the frontend used for most of the clients available for end user
|
||||||
.
|
.
|
||||||
└── src
|
└── src
|
||||||
├── apps
|
├── apps
|
||||||
│ ├── experimental # New experimental app layout
|
│ ├── dashboard # Admin dashboard app layout and routes
|
||||||
│ └── stable # Classic (stable) app layout
|
│ ├── experimental # New experimental app layout and routes
|
||||||
|
│ └── stable # Classic (stable) app layout and routes
|
||||||
├── assets # Static assets
|
├── assets # Static assets
|
||||||
├── components # Higher order visual components and React components
|
├── components # Higher order visual components and React components
|
||||||
├── controllers # Legacy page views and controllers 🧹
|
├── controllers # Legacy page views and controllers 🧹
|
||||||
|
@ -87,7 +88,6 @@ Jellyfin Web is the frontend used for most of the clients available for end user
|
||||||
├── legacy # Polyfills for legacy browsers
|
├── legacy # Polyfills for legacy browsers
|
||||||
├── libraries # Third party libraries 🧹
|
├── libraries # Third party libraries 🧹
|
||||||
├── plugins # Client plugins
|
├── plugins # Client plugins
|
||||||
├── routes # React routes/pages
|
|
||||||
├── scripts # Random assortment of visual components and utilities 🐉
|
├── scripts # Random assortment of visual components and utilities 🐉
|
||||||
├── strings # Translation files
|
├── strings # Translation files
|
||||||
├── styles # Common app Sass stylesheets
|
├── styles # Common app Sass stylesheets
|
||||||
|
|
|
@ -13,8 +13,8 @@ ENV IS_DOCKER=YES
|
||||||
RUN yum update -y \
|
RUN yum update -y \
|
||||||
&& yum install -y epel-release \
|
&& yum install -y epel-release \
|
||||||
&& yum install -y rpmdevtools git autoconf automake glibc-devel gcc-c++ make \
|
&& yum install -y rpmdevtools git autoconf automake glibc-devel gcc-c++ make \
|
||||||
&& curl -fsSL https://rpm.nodesource.com/setup_16.x | bash - \
|
&& yum install https://rpm.nodesource.com/pub_20.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm -y \
|
||||||
&& yum install -y nodejs
|
&& yum install nodejs -y --setopt=nodesource-nodejs.module_hotfixes=1
|
||||||
|
|
||||||
# Link to build script
|
# Link to build script
|
||||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.centos /build.sh
|
RUN ln -sf ${SOURCE_DIR}/deployment/build.centos /build.sh
|
||||||
|
|
|
@ -12,11 +12,13 @@ ENV IS_DOCKER=YES
|
||||||
|
|
||||||
# Prepare Debian build environment
|
# Prepare Debian build environment
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y debhelper mmv git curl \
|
&& apt-get install -y debhelper mmv git curl gnupg ca-certificates \
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_16.x | bash - \
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update \
|
||||||
&& apt-get install -y nodejs
|
&& apt-get install -y nodejs
|
||||||
|
|
||||||
|
|
||||||
# Link to build script
|
# Link to build script
|
||||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.debian /build.sh
|
RUN ln -sf ${SOURCE_DIR}/deployment/build.debian /build.sh
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:lts-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
ARG SOURCE_DIR=/src
|
ARG SOURCE_DIR=/src
|
||||||
ARG ARTIFACT_DIR=/jellyfin-web
|
ARG ARTIFACT_DIR=/jellyfin-web
|
||||||
|
|
|
@ -11,7 +11,8 @@ ENV IS_DOCKER=YES
|
||||||
|
|
||||||
# Prepare Fedora environment
|
# Prepare Fedora environment
|
||||||
RUN dnf update -y \
|
RUN dnf update -y \
|
||||||
&& dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core nodejs autoconf automake glibc-devel make
|
&& yum install https://rpm.nodesource.com/pub_20.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm -y \
|
||||||
|
&& dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core nodejs autoconf automake glibc-devel make --setopt=nodesource-nodejs.module_hotfixes=1
|
||||||
|
|
||||||
# Link to build script
|
# Link to build script
|
||||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora /build.sh
|
RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora /build.sh
|
||||||
|
|
|
@ -11,8 +11,11 @@ ENV IS_DOCKER=YES
|
||||||
|
|
||||||
# Prepare Debian build environment
|
# Prepare Debian build environment
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y mmv curl git \
|
&& apt-get install -y mmv curl git gnupg ca-certificates \
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_16.x | bash - \
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update \
|
||||||
&& apt-get install -y nodejs
|
&& apt-get install -y nodejs
|
||||||
|
|
||||||
# Link to build script
|
# Link to build script
|
||||||
|
|
|
@ -7,7 +7,7 @@ RELEASE := $(shell set -x; sed -ne '/^Release:/s/.* *\(.*\)%{.*}.*/\1/p' $(DIR)
|
||||||
SRPM := jellyfin-web-$(subst -,~,$(VERSION))-$(RELEASE)$(shell rpm --eval %dist).src.rpm
|
SRPM := jellyfin-web-$(subst -,~,$(VERSION))-$(RELEASE)$(shell rpm --eval %dist).src.rpm
|
||||||
TARBALL :=$(NAME)-$(subst -,~,$(VERSION)).tar.gz
|
TARBALL :=$(NAME)-$(subst -,~,$(VERSION)).tar.gz
|
||||||
|
|
||||||
epel-7-x86_64_repos := https://rpm.nodesource.com/pub_16.x/el/\$$releasever/\$$basearch/
|
epel-7-x86_64_repos := https://rpm.nodesource.com/pub_20.x/nodistro/\$$basearch/
|
||||||
|
|
||||||
fed_ver := $(shell rpm -E %fedora)
|
fed_ver := $(shell rpm -E %fedora)
|
||||||
# fallback when not running on Fedora
|
# fallback when not running on Fedora
|
||||||
|
|
|
@ -14,10 +14,10 @@ BuildArch: noarch
|
||||||
BuildRequires: nodejs
|
BuildRequires: nodejs
|
||||||
%else
|
%else
|
||||||
BuildRequires: git
|
BuildRequires: git
|
||||||
# Nodejs 16 is required and npm >= 8 should bring in NodeJS 16
|
# Nodejs 20 is required and npm >= 10 should bring in NodeJS 20
|
||||||
# This requires the build environment to use the nodejs:16 module stream:
|
# This requires the build environment to use the nodejs:20 module stream:
|
||||||
# dnf module {install|switch-to}:web nodejs:16
|
# dnf module {install|switch-to}:web nodejs:20
|
||||||
BuildRequires: npm >= 8
|
BuildRequires: npm >= 10
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
%description
|
%description
|
||||||
|
|
1678
package-lock.json
generated
1678
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -58,6 +58,7 @@
|
||||||
"stylelint-scss": "5.0.0",
|
"stylelint-scss": "5.0.0",
|
||||||
"ts-loader": "9.4.4",
|
"ts-loader": "9.4.4",
|
||||||
"typescript": "5.0.4",
|
"typescript": "5.0.4",
|
||||||
|
"vitest": "0.34.6",
|
||||||
"webpack": "5.88.1",
|
"webpack": "5.88.1",
|
||||||
"webpack-bundle-analyzer": "4.9.1",
|
"webpack-bundle-analyzer": "4.9.1",
|
||||||
"webpack-cli": "5.1.4",
|
"webpack-cli": "5.1.4",
|
||||||
|
@ -145,13 +146,15 @@
|
||||||
"build:check": "tsc --noEmit",
|
"build:check": "tsc --noEmit",
|
||||||
"escheck": "es-check",
|
"escheck": "es-check",
|
||||||
"lint": "eslint \"./\"",
|
"lint": "eslint \"./\"",
|
||||||
|
"test": "vitest --watch=false",
|
||||||
|
"test:watch": "vitest",
|
||||||
"stylelint": "npm run stylelint:css && npm run stylelint:scss",
|
"stylelint": "npm run stylelint:css && npm run stylelint:scss",
|
||||||
"stylelint:css": "stylelint \"src/**/*.css\"",
|
"stylelint:css": "stylelint \"src/**/*.css\"",
|
||||||
"stylelint:scss": "stylelint --config=\".stylelintrc.scss.json\" \"src/**/*.scss\""
|
"stylelint:scss": "stylelint --config=\".stylelintrc.scss.json\" \"src/**/*.scss\""
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.13.1",
|
"node": ">=20.0.0",
|
||||||
"npm": ">=8.1.2",
|
"npm": ">=9.6.4",
|
||||||
"yarn": "YARN NO LONGER USED - use npm instead."
|
"yarn": "YARN NO LONGER USED - use npm instead."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,62 @@
|
||||||
import loadable from '@loadable/component';
|
import loadable from '@loadable/component';
|
||||||
|
import { ThemeProvider } from '@mui/material/styles';
|
||||||
import { History } from '@remix-run/router';
|
import { History } from '@remix-run/router';
|
||||||
import React from 'react';
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
|
import React from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import StableApp from './apps/stable/App';
|
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
||||||
import { HistoryRouter } from './components/router/HistoryRouter';
|
import AppHeader from 'components/AppHeader';
|
||||||
import { ApiProvider } from './hooks/useApi';
|
import Backdrop from 'components/Backdrop';
|
||||||
import { WebConfigProvider } from './hooks/useWebConfig';
|
import { HistoryRouter } from 'components/router/HistoryRouter';
|
||||||
|
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 ExperimentalApp = loadable(() => import('./apps/experimental/App'));
|
||||||
|
const StableApp = loadable(() => import('./apps/stable/App'));
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const RootApp = ({ history }: { history: History }) => {
|
const RootAppLayout = () => {
|
||||||
const layoutMode = localStorage.getItem('layout');
|
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 (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<>
|
||||||
<ApiProvider>
|
<Backdrop />
|
||||||
<WebConfigProvider>
|
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
|
||||||
<HistoryRouter history={history}>
|
|
||||||
{
|
{
|
||||||
layoutMode === 'experimental' ?
|
isExperimentalLayout ?
|
||||||
<ExperimentalApp /> :
|
<ExperimentalApp /> :
|
||||||
<StableApp />
|
<StableApp />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<DashboardApp />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RootApp = ({ history }: { history: History }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<ApiProvider>
|
||||||
|
<WebConfigProvider>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<HistoryRouter history={history}>
|
||||||
|
<RootAppLayout />
|
||||||
</HistoryRouter>
|
</HistoryRouter>
|
||||||
|
</ThemeProvider>
|
||||||
</WebConfigProvider>
|
</WebConfigProvider>
|
||||||
</ApiProvider>
|
</ApiProvider>
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
export default RootApp;
|
export default RootApp;
|
||||||
|
|
66
src/apps/dashboard/App.tsx
Normal file
66
src/apps/dashboard/App.tsx
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import loadable from '@loadable/component';
|
||||||
|
import React from 'react';
|
||||||
|
import { Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ConnectionRequired from 'components/ConnectionRequired';
|
||||||
|
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||||
|
import { AsyncPageProps, AsyncRoute, toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||||
|
import { toRedirectRoute } from 'components/router/Redirect';
|
||||||
|
import ServerContentPage from 'components/ServerContentPage';
|
||||||
|
|
||||||
|
import AppLayout from './AppLayout';
|
||||||
|
import { REDIRECTS } from './routes/_redirects';
|
||||||
|
import { ASYNC_ADMIN_ROUTES } from './routes/_asyncRoutes';
|
||||||
|
import { LEGACY_ADMIN_ROUTES } from './routes/_legacyRoutes';
|
||||||
|
|
||||||
|
const DashboardAsyncPage = loadable(
|
||||||
|
(props: { page: string }) => import(/* webpackChunkName: "[request]" */ `./routes/${props.page}`),
|
||||||
|
{ cacheKey: (props: AsyncPageProps) => props.page }
|
||||||
|
);
|
||||||
|
|
||||||
|
const toDashboardAsyncPageRoute = (route: AsyncRoute) => (
|
||||||
|
toAsyncPageRoute({
|
||||||
|
...route,
|
||||||
|
element: DashboardAsyncPage
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DASHBOARD_APP_PATHS = {
|
||||||
|
Dashboard: 'dashboard',
|
||||||
|
MetadataManager: 'metadata',
|
||||||
|
PluginConfig: 'configurationpage'
|
||||||
|
};
|
||||||
|
|
||||||
|
const DashboardApp = () => (
|
||||||
|
<Routes>
|
||||||
|
<Route element={<ConnectionRequired isAdminRequired />}>
|
||||||
|
<Route element={<AppLayout drawerlessPaths={[ DASHBOARD_APP_PATHS.MetadataManager ]} />}>
|
||||||
|
<Route path={DASHBOARD_APP_PATHS.Dashboard}>
|
||||||
|
{ASYNC_ADMIN_ROUTES.map(toDashboardAsyncPageRoute)}
|
||||||
|
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* NOTE: The metadata editor might deserve a dedicated app in the future */}
|
||||||
|
{toViewManagerPageRoute({
|
||||||
|
path: DASHBOARD_APP_PATHS.MetadataManager,
|
||||||
|
pageProps: {
|
||||||
|
controller: 'edititemmetadata',
|
||||||
|
view: 'edititemmetadata.html'
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Route path={DASHBOARD_APP_PATHS.PluginConfig} element={
|
||||||
|
<ServerContentPage view='/web/configurationpage' />
|
||||||
|
} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* Suppress warnings for unhandled routes */}
|
||||||
|
<Route path='*' element={null} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* Redirects for old paths */}
|
||||||
|
{REDIRECTS.map(toRedirectRoute)}
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default DashboardApp;
|
108
src/apps/dashboard/AppLayout.tsx
Normal file
108
src/apps/dashboard/AppLayout.tsx
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
import AppBar from '@mui/material/AppBar';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { Outlet, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import AppBody from 'components/AppBody';
|
||||||
|
import AppToolbar from 'components/toolbar/AppToolbar';
|
||||||
|
import ElevationScroll from 'components/ElevationScroll';
|
||||||
|
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
||||||
|
import { useApi } from 'hooks/useApi';
|
||||||
|
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||||
|
|
||||||
|
import AppDrawer from './components/drawer/AppDrawer';
|
||||||
|
|
||||||
|
import './AppOverrides.scss';
|
||||||
|
|
||||||
|
interface AppLayoutProps {
|
||||||
|
drawerlessPaths: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DashboardAppSettings {
|
||||||
|
isDrawerPinned: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_APP_SETTINGS: DashboardAppSettings = {
|
||||||
|
isDrawerPinned: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppLayout: FC<AppLayoutProps> = ({
|
||||||
|
drawerlessPaths
|
||||||
|
}) => {
|
||||||
|
const [ appSettings, setAppSettings ] = useLocalStorage<DashboardAppSettings>('DashboardAppSettings', DEFAULT_APP_SETTINGS);
|
||||||
|
const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned);
|
||||||
|
const location = useLocation();
|
||||||
|
const theme = useTheme();
|
||||||
|
const { user } = useApi();
|
||||||
|
|
||||||
|
const isDrawerAvailable = !drawerlessPaths.some(path => location.pathname.startsWith(`/${path}`));
|
||||||
|
const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDrawerActive !== appSettings.isDrawerPinned) {
|
||||||
|
setAppSettings({
|
||||||
|
...appSettings,
|
||||||
|
isDrawerPinned: isDrawerActive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [ appSettings, isDrawerActive, setAppSettings ]);
|
||||||
|
|
||||||
|
const onToggleDrawer = useCallback(() => {
|
||||||
|
setIsDrawerActive(!isDrawerActive);
|
||||||
|
}, [ isDrawerActive, setIsDrawerActive ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<ElevationScroll elevate={isDrawerOpen}>
|
||||||
|
<AppBar
|
||||||
|
position='fixed'
|
||||||
|
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.drawer + 1 }}
|
||||||
|
>
|
||||||
|
<AppToolbar
|
||||||
|
isDrawerAvailable={isDrawerAvailable}
|
||||||
|
isDrawerOpen={isDrawerOpen}
|
||||||
|
onDrawerButtonClick={onToggleDrawer}
|
||||||
|
/>
|
||||||
|
</AppBar>
|
||||||
|
</ElevationScroll>
|
||||||
|
|
||||||
|
<AppDrawer
|
||||||
|
open={isDrawerOpen}
|
||||||
|
onClose={onToggleDrawer}
|
||||||
|
onOpen={onToggleDrawer}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
component='main'
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
flexGrow: 1,
|
||||||
|
transition: theme.transitions.create('margin', {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.leavingScreen
|
||||||
|
}),
|
||||||
|
marginLeft: 0,
|
||||||
|
...(isDrawerAvailable && {
|
||||||
|
marginLeft: {
|
||||||
|
sm: `-${DRAWER_WIDTH}px`
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
...(isDrawerActive && {
|
||||||
|
transition: theme.transitions.create('margin', {
|
||||||
|
easing: theme.transitions.easing.easeOut,
|
||||||
|
duration: theme.transitions.duration.enteringScreen
|
||||||
|
}),
|
||||||
|
marginLeft: 0
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AppBody>
|
||||||
|
<Outlet />
|
||||||
|
</AppBody>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
22
src/apps/dashboard/AppOverrides.scss
Normal file
22
src/apps/dashboard/AppOverrides.scss
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// Default MUI breakpoints
|
||||||
|
// https://mui.com/material-ui/customization/breakpoints/#default-breakpoints
|
||||||
|
$mui-bp-sm: 600px;
|
||||||
|
$mui-bp-md: 900px;
|
||||||
|
$mui-bp-lg: 1200px;
|
||||||
|
$mui-bp-xl: 1536px;
|
||||||
|
|
||||||
|
// Fix dashboard pages layout to work with drawer
|
||||||
|
.dashboardDocument {
|
||||||
|
.mainAnimatedPage {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skinBody {
|
||||||
|
position: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix the padding of dashboard pages
|
||||||
|
.content-primary.content-primary {
|
||||||
|
padding-top: 3.25rem !important;
|
||||||
|
}
|
||||||
|
}
|
29
src/apps/dashboard/components/drawer/AppDrawer.tsx
Normal file
29
src/apps/dashboard/components/drawer/AppDrawer.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';
|
||||||
|
|
||||||
|
import ServerDrawerSection from './sections/ServerDrawerSection';
|
||||||
|
import DevicesDrawerSection from './sections/DevicesDrawerSection';
|
||||||
|
import LiveTvDrawerSection from './sections/LiveTvDrawerSection';
|
||||||
|
import AdvancedDrawerSection from './sections/AdvancedDrawerSection';
|
||||||
|
import PluginDrawerSection from './sections/PluginDrawerSection';
|
||||||
|
|
||||||
|
const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
||||||
|
open = false,
|
||||||
|
onClose,
|
||||||
|
onOpen
|
||||||
|
}) => (
|
||||||
|
<ResponsiveDrawer
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
onOpen={onOpen}
|
||||||
|
>
|
||||||
|
<ServerDrawerSection />
|
||||||
|
<DevicesDrawerSection />
|
||||||
|
<LiveTvDrawerSection />
|
||||||
|
<AdvancedDrawerSection />
|
||||||
|
<PluginDrawerSection />
|
||||||
|
</ResponsiveDrawer>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AppDrawer;
|
|
@ -15,15 +15,14 @@ import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ListItemLink from 'components/ListItemLink';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import ListItemLink from '../ListItemLink';
|
|
||||||
|
|
||||||
const PLUGIN_PATHS = [
|
const PLUGIN_PATHS = [
|
||||||
'/installedplugins.html',
|
'/dashboard/plugins',
|
||||||
'/availableplugins.html',
|
'/dashboard/plugins/catalog',
|
||||||
'/repositories.html',
|
'/dashboard/plugins/repositories',
|
||||||
'/addplugin.html',
|
'/dashboard/plugins/add',
|
||||||
'/configurationpage'
|
'/configurationpage'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -42,7 +41,7 @@ const AdvancedDrawerSection = () => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/networking.html'>
|
<ListItemLink to='/dashboard/networking'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Lan />
|
<Lan />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -50,7 +49,7 @@ const AdvancedDrawerSection = () => {
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/apikeys.html'>
|
<ListItemLink to='/dashboard/keys'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<VpnKey />
|
<VpnKey />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -58,7 +57,7 @@ const AdvancedDrawerSection = () => {
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/log.html'>
|
<ListItemLink to='/dashboard/logs'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Article />
|
<Article />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -66,7 +65,7 @@ const AdvancedDrawerSection = () => {
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/notificationsettings.html'>
|
<ListItemLink to='/dashboard/notifications'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<EditNotifications />
|
<EditNotifications />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -74,7 +73,7 @@ const AdvancedDrawerSection = () => {
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/installedplugins.html' selected={false}>
|
<ListItemLink to='/dashboard/plugins' selected={false}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Extension />
|
<Extension />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -84,19 +83,19 @@ const AdvancedDrawerSection = () => {
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Collapse in={isPluginSectionOpen} timeout='auto' unmountOnExit>
|
<Collapse in={isPluginSectionOpen} timeout='auto' unmountOnExit>
|
||||||
<List component='div' disablePadding>
|
<List component='div' disablePadding>
|
||||||
<ListItemLink to='/installedplugins.html' sx={{ pl: 4 }}>
|
<ListItemLink to='/dashboard/plugins' sx={{ pl: 4 }}>
|
||||||
<ListItemText inset primary={globalize.translate('TabMyPlugins')} />
|
<ListItemText inset primary={globalize.translate('TabMyPlugins')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
<ListItemLink to='/availableplugins.html' sx={{ pl: 4 }}>
|
<ListItemLink to='/dashboard/plugins/catalog' sx={{ pl: 4 }}>
|
||||||
<ListItemText inset primary={globalize.translate('TabCatalog')} />
|
<ListItemText inset primary={globalize.translate('TabCatalog')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
<ListItemLink to='/repositories.html' sx={{ pl: 4 }}>
|
<ListItemLink to='/dashboard/plugins/repositories' sx={{ pl: 4 }}>
|
||||||
<ListItemText inset primary={globalize.translate('TabRepositories')} />
|
<ListItemText inset primary={globalize.translate('TabRepositories')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</List>
|
</List>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/scheduledtasks.html'>
|
<ListItemLink to='/dashboard/tasks'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Schedule />
|
<Schedule />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
|
@ -8,13 +8,12 @@ import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ListItemLink from 'components/ListItemLink';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import ListItemLink from '../ListItemLink';
|
|
||||||
|
|
||||||
const DLNA_PATHS = [
|
const DLNA_PATHS = [
|
||||||
'/dlnasettings.html',
|
'/dashboard/dlna',
|
||||||
'/dlnaprofiles.html'
|
'/dashboard/dlna/profiles'
|
||||||
];
|
];
|
||||||
|
|
||||||
const DevicesDrawerSection = () => {
|
const DevicesDrawerSection = () => {
|
||||||
|
@ -32,7 +31,7 @@ const DevicesDrawerSection = () => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/devices.html'>
|
<ListItemLink to='/dashboard/devices'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Devices />
|
<Devices />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -48,7 +47,7 @@ const DevicesDrawerSection = () => {
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/dlnasettings.html' selected={false}>
|
<ListItemLink to='/dashboard/dlna' selected={false}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Input />
|
<Input />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -58,10 +57,10 @@ const DevicesDrawerSection = () => {
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Collapse in={isDlnaSectionOpen} timeout='auto' unmountOnExit>
|
<Collapse in={isDlnaSectionOpen} timeout='auto' unmountOnExit>
|
||||||
<List component='div' disablePadding>
|
<List component='div' disablePadding>
|
||||||
<ListItemLink to='/dlnasettings.html' sx={{ pl: 4 }}>
|
<ListItemLink to='/dashboard/dlna' sx={{ pl: 4 }}>
|
||||||
<ListItemText inset primary={globalize.translate('Settings')} />
|
<ListItemText inset primary={globalize.translate('Settings')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
<ListItemLink to='/dlnaprofiles.html' sx={{ pl: 4 }}>
|
<ListItemLink to='/dashboard/dlna/profiles' sx={{ pl: 4 }}>
|
||||||
<ListItemText inset primary={globalize.translate('TabProfiles')} />
|
<ListItemText inset primary={globalize.translate('TabProfiles')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</List>
|
</List>
|
|
@ -6,10 +6,9 @@ import ListItemText from '@mui/material/ListItemText';
|
||||||
import ListSubheader from '@mui/material/ListSubheader';
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import ListItemLink from 'components/ListItemLink';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import ListItemLink from '../ListItemLink';
|
|
||||||
|
|
||||||
const LiveTvDrawerSection = () => {
|
const LiveTvDrawerSection = () => {
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
|
@ -21,7 +20,7 @@ const LiveTvDrawerSection = () => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/livetvstatus.html'>
|
<ListItemLink to='/dashboard/livetv'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LiveTv />
|
<LiveTv />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -29,7 +28,7 @@ const LiveTvDrawerSection = () => {
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/livetvsettings.html'>
|
<ListItemLink to='/dashboard/recordings'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Dvr />
|
<Dvr />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
|
@ -8,12 +8,11 @@ import ListItemText from '@mui/material/ListItemText';
|
||||||
import ListSubheader from '@mui/material/ListSubheader';
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import ListItemLink from 'components/ListItemLink';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
import Dashboard from 'utils/dashboard';
|
import Dashboard from 'utils/dashboard';
|
||||||
|
|
||||||
import ListItemLink from '../ListItemLink';
|
|
||||||
|
|
||||||
const PluginDrawerSection = () => {
|
const PluginDrawerSection = () => {
|
||||||
const { api } = useApi();
|
const { api } = useApi();
|
||||||
const [ pagesInfo, setPagesInfo ] = useState<ConfigurationPageInfo[]>([]);
|
const [ pagesInfo, setPagesInfo ] = useState<ConfigurationPageInfo[]>([]);
|
|
@ -8,21 +8,20 @@ import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ListItemLink from 'components/ListItemLink';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import ListItemLink from '../ListItemLink';
|
|
||||||
|
|
||||||
const LIBRARY_PATHS = [
|
const LIBRARY_PATHS = [
|
||||||
'/library.html',
|
'/dashboard/libraries',
|
||||||
'/librarydisplay.html',
|
'/dashboard/libraries/display',
|
||||||
'/metadataimages.html',
|
'/dashboard/libraries/metadata',
|
||||||
'/metadatanfo.html'
|
'/dashboard/libraries/nfo'
|
||||||
];
|
];
|
||||||
|
|
||||||
const PLAYBACK_PATHS = [
|
const PLAYBACK_PATHS = [
|
||||||
'/encodingsettings.html',
|
'/dashboard/playback/transcoding',
|
||||||
'/playbackconfiguration.html',
|
'/dashboard/playback/resume',
|
||||||
'/streamingsettings.html'
|
'/dashboard/playback/streaming'
|
||||||
];
|
];
|
||||||
|
|
||||||
const ServerDrawerSection = () => {
|
const ServerDrawerSection = () => {
|
||||||
|
@ -41,7 +40,7 @@ const ServerDrawerSection = () => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/dashboard.html'>
|
<ListItemLink to='/dashboard'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -49,7 +48,7 @@ const ServerDrawerSection = () => {
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/dashboardgeneral.html'>
|
<ListItemLink to='/dashboard/settings'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Settings />
|
<Settings />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -57,7 +56,7 @@ const ServerDrawerSection = () => {
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/userprofiles.html'>
|
<ListItemLink to='/dashboard/users'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<People />
|
<People />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -65,7 +64,7 @@ const ServerDrawerSection = () => {
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/library.html' selected={false}>
|
<ListItemLink to='/dashboard/libraries' selected={false}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LibraryAdd />
|
<LibraryAdd />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -75,22 +74,22 @@ const ServerDrawerSection = () => {
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Collapse in={isLibrarySectionOpen} timeout='auto' unmountOnExit>
|
<Collapse in={isLibrarySectionOpen} timeout='auto' unmountOnExit>
|
||||||
<List component='div' disablePadding>
|
<List component='div' disablePadding>
|
||||||
<ListItemLink to='/library.html' sx={{ pl: 4 }}>
|
<ListItemLink to='/dashboard/libraries' sx={{ pl: 4 }}>
|
||||||
<ListItemText inset primary={globalize.translate('HeaderLibraries')} />
|
<ListItemText inset primary={globalize.translate('HeaderLibraries')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
<ListItemLink to='/librarydisplay.html' sx={{ pl: 4 }}>
|
<ListItemLink to='/dashboard/libraries/display' sx={{ pl: 4 }}>
|
||||||
<ListItemText inset primary={globalize.translate('Display')} />
|
<ListItemText inset primary={globalize.translate('Display')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
<ListItemLink to='/metadataimages.html' sx={{ pl: 4 }}>
|
<ListItemLink to='/dashboard/libraries/metadata' sx={{ pl: 4 }}>
|
||||||
<ListItemText inset primary={globalize.translate('Metadata')} />
|
<ListItemText inset primary={globalize.translate('Metadata')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
<ListItemLink to='/metadatanfo.html' sx={{ pl: 4 }}>
|
<ListItemLink to='/dashboard/libraries/nfo' sx={{ pl: 4 }}>
|
||||||
<ListItemText inset primary={globalize.translate('TabNfoSettings')} />
|
<ListItemText inset primary={globalize.translate('TabNfoSettings')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</List>
|
</List>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/encodingsettings.html' selected={false}>
|
<ListItemLink to='/dashboard/playback/transcoding' selected={false}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<PlayCircle />
|
<PlayCircle />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -100,13 +99,13 @@ const ServerDrawerSection = () => {
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Collapse in={isPlaybackSectionOpen} timeout='auto' unmountOnExit>
|
<Collapse in={isPlaybackSectionOpen} timeout='auto' unmountOnExit>
|
||||||
<List component='div' disablePadding>
|
<List component='div' disablePadding>
|
||||||
<ListItemLink to='/encodingsettings.html' sx={{ pl: 4 }}>
|
<ListItemLink to='/dashboard/playback/transcoding' sx={{ pl: 4 }}>
|
||||||
<ListItemText inset primary={globalize.translate('Transcoding')} />
|
<ListItemText inset primary={globalize.translate('Transcoding')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
<ListItemLink to='/playbackconfiguration.html' sx={{ pl: 4 }}>
|
<ListItemLink to='/dashboard/playback/resume' sx={{ pl: 4 }}>
|
||||||
<ListItemText inset primary={globalize.translate('ButtonResume')} />
|
<ListItemText inset primary={globalize.translate('ButtonResume')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
<ListItemLink to='/streamingsettings.html' sx={{ pl: 4 }}>
|
<ListItemLink to='/dashboard/playback/streaming' sx={{ pl: 4 }}>
|
||||||
<ListItemText inset primary={globalize.translate('TabStreaming')} />
|
<ListItemText inset primary={globalize.translate('TabStreaming')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</List>
|
</List>
|
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,176 +1,164 @@
|
||||||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
import type { LegacyRoute } from 'components/router/LegacyRoute';
|
||||||
|
|
||||||
export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||||
{
|
{
|
||||||
path: 'dashboard.html',
|
path: '/dashboard',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/dashboard',
|
controller: 'dashboard/dashboard',
|
||||||
view: 'dashboard/dashboard.html'
|
view: 'dashboard/dashboard.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'dashboardgeneral.html',
|
path: 'settings',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/general',
|
controller: 'dashboard/general',
|
||||||
view: 'dashboard/general.html'
|
view: 'dashboard/general.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'networking.html',
|
path: 'networking',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/networking',
|
controller: 'dashboard/networking',
|
||||||
view: 'dashboard/networking.html'
|
view: 'dashboard/networking.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'devices.html',
|
path: 'devices',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/devices/devices',
|
controller: 'dashboard/devices/devices',
|
||||||
view: 'dashboard/devices/devices.html'
|
view: 'dashboard/devices/devices.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'device.html',
|
path: 'devices/edit',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/devices/device',
|
controller: 'dashboard/devices/device',
|
||||||
view: 'dashboard/devices/device.html'
|
view: 'dashboard/devices/device.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'quickConnect.html',
|
path: 'dlna/profiles/edit',
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/quickConnect',
|
|
||||||
view: 'dashboard/quickConnect.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'dlnaprofile.html',
|
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/dlna/profile',
|
controller: 'dashboard/dlna/profile',
|
||||||
view: 'dashboard/dlna/profile.html'
|
view: 'dashboard/dlna/profile.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'dlnaprofiles.html',
|
path: 'dlna/profiles',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/dlna/profiles',
|
controller: 'dashboard/dlna/profiles',
|
||||||
view: 'dashboard/dlna/profiles.html'
|
view: 'dashboard/dlna/profiles.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'dlnasettings.html',
|
path: 'dlna',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/dlna/settings',
|
controller: 'dashboard/dlna/settings',
|
||||||
view: 'dashboard/dlna/settings.html'
|
view: 'dashboard/dlna/settings.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'addplugin.html',
|
path: 'plugins/add',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/plugins/add/index',
|
controller: 'dashboard/plugins/add/index',
|
||||||
view: 'dashboard/plugins/add/index.html'
|
view: 'dashboard/plugins/add/index.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'library.html',
|
path: 'libraries',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/library',
|
controller: 'dashboard/library',
|
||||||
view: 'dashboard/library.html'
|
view: 'dashboard/library.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'librarydisplay.html',
|
path: 'libraries/display',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/librarydisplay',
|
controller: 'dashboard/librarydisplay',
|
||||||
view: 'dashboard/librarydisplay.html'
|
view: 'dashboard/librarydisplay.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'edititemmetadata.html',
|
path: 'playback/transcoding',
|
||||||
pageProps: {
|
|
||||||
controller: 'edititemmetadata',
|
|
||||||
view: 'edititemmetadata.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'encodingsettings.html',
|
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/encodingsettings',
|
controller: 'dashboard/encodingsettings',
|
||||||
view: 'dashboard/encodingsettings.html'
|
view: 'dashboard/encodingsettings.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'log.html',
|
path: 'logs',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/logs',
|
controller: 'dashboard/logs',
|
||||||
view: 'dashboard/logs.html'
|
view: 'dashboard/logs.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'metadataimages.html',
|
path: 'libraries/metadata',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/metadataImages',
|
controller: 'dashboard/metadataImages',
|
||||||
view: 'dashboard/metadataimages.html'
|
view: 'dashboard/metadataimages.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'metadatanfo.html',
|
path: 'libraries/nfo',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/metadatanfo',
|
controller: 'dashboard/metadatanfo',
|
||||||
view: 'dashboard/metadatanfo.html'
|
view: 'dashboard/metadatanfo.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'playbackconfiguration.html',
|
path: 'playback/resume',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/playback',
|
controller: 'dashboard/playback',
|
||||||
view: 'dashboard/playback.html'
|
view: 'dashboard/playback.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'availableplugins.html',
|
path: 'plugins/catalog',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/plugins/available/index',
|
controller: 'dashboard/plugins/available/index',
|
||||||
view: 'dashboard/plugins/available/index.html'
|
view: 'dashboard/plugins/available/index.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'repositories.html',
|
path: 'plugins/repositories',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/plugins/repositories/index',
|
controller: 'dashboard/plugins/repositories/index',
|
||||||
view: 'dashboard/plugins/repositories/index.html'
|
view: 'dashboard/plugins/repositories/index.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'livetvguideprovider.html',
|
path: 'livetv/guide',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'livetvguideprovider',
|
controller: 'livetvguideprovider',
|
||||||
view: 'livetvguideprovider.html'
|
view: 'livetvguideprovider.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'livetvsettings.html',
|
path: 'recordings',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'livetvsettings',
|
controller: 'livetvsettings',
|
||||||
view: 'livetvsettings.html'
|
view: 'livetvsettings.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'livetvstatus.html',
|
path: 'livetv',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'livetvstatus',
|
controller: 'livetvstatus',
|
||||||
view: 'livetvstatus.html'
|
view: 'livetvstatus.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'livetvtuner.html',
|
path: 'livetv/tuner',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'livetvtuner',
|
controller: 'livetvtuner',
|
||||||
view: 'livetvtuner.html'
|
view: 'livetvtuner.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'installedplugins.html',
|
path: 'plugins',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/plugins/installed/index',
|
controller: 'dashboard/plugins/installed/index',
|
||||||
view: 'dashboard/plugins/installed/index.html'
|
view: 'dashboard/plugins/installed/index.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'scheduledtask.html',
|
path: 'tasks/edit',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/scheduledtasks/scheduledtask',
|
controller: 'dashboard/scheduledtasks/scheduledtask',
|
||||||
view: 'dashboard/scheduledtasks/scheduledtask.html'
|
view: 'dashboard/scheduledtasks/scheduledtask.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'scheduledtasks.html',
|
path: 'tasks',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/scheduledtasks/scheduledtasks',
|
controller: 'dashboard/scheduledtasks/scheduledtasks',
|
||||||
view: 'dashboard/scheduledtasks/scheduledtasks.html'
|
view: 'dashboard/scheduledtasks/scheduledtasks.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'apikeys.html',
|
path: 'keys',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/apikeys',
|
controller: 'dashboard/apikeys',
|
||||||
view: 'dashboard/apikeys.html'
|
view: 'dashboard/apikeys.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'streamingsettings.html',
|
path: 'playback/streaming',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
view: 'dashboard/streaming.html',
|
view: 'dashboard/streaming.html',
|
||||||
controller: 'dashboard/streaming'
|
controller: 'dashboard/streaming'
|
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 globalize from 'scripts/globalize';
|
||||||
import { toBoolean } from 'utils/string';
|
import { toBoolean } from 'utils/string';
|
||||||
|
|
||||||
import LogLevelChip from '../../components/activityTable/LogLevelChip';
|
import LogLevelChip from '../components/activityTable/LogLevelChip';
|
||||||
import OverviewCell from '../../components/activityTable/OverviewCell';
|
import OverviewCell from '../components/activityTable/OverviewCell';
|
||||||
import GridActionsCellLink from '../../components/GridActionsCellLink';
|
import GridActionsCellLink from '../components/dataGrid/GridActionsCellLink';
|
||||||
|
|
||||||
const DEFAULT_PAGE_SIZE = 25;
|
const DEFAULT_PAGE_SIZE = 25;
|
||||||
const VIEW_PARAM = 'useractivity';
|
const VIEW_PARAM = 'useractivity';
|
||||||
|
@ -68,7 +68,7 @@ const Activity = () => {
|
||||||
sx={{ padding: 0 }}
|
sx={{ padding: 0 }}
|
||||||
title={users[row.UserId]?.Name ?? undefined}
|
title={users[row.UserId]?.Name ?? undefined}
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/useredit.html?userId=${row.UserId}`}
|
to={`/dashboard/users/profile?userId=${row.UserId}`}
|
||||||
>
|
>
|
||||||
<UserAvatar user={users[row.UserId]} />
|
<UserAvatar user={users[row.UserId]} />
|
||||||
</IconButton>
|
</IconButton>
|
|
@ -9,7 +9,7 @@ const PluginLink = () => (
|
||||||
__html: `<a
|
__html: `<a
|
||||||
is='emby-linkbutton'
|
is='emby-linkbutton'
|
||||||
class='button-link'
|
class='button-link'
|
||||||
href='#/addplugin.html?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173'
|
href='#/dashboard/plugins/add?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173'
|
||||||
>
|
>
|
||||||
${globalize.translate('GetThePlugin')}
|
${globalize.translate('GetThePlugin')}
|
||||||
</a>`
|
</a>`
|
|
@ -140,7 +140,7 @@ const UserNew: FunctionComponent = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||||
Dashboard.navigate('useredit.html?userId=' + user.Id)
|
Dashboard.navigate('/dashboard/users/profile?userId=' + user.Id)
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('[usernew] failed to navigate to edit user page', err);
|
console.error('[usernew] failed to navigate to edit user page', err);
|
||||||
});
|
});
|
|
@ -85,21 +85,21 @@ const UserProfiles: FunctionComponent = () => {
|
||||||
callback: function (id: string) {
|
callback: function (id: string) {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case 'open':
|
case 'open':
|
||||||
Dashboard.navigate('useredit.html?userId=' + userId)
|
Dashboard.navigate('/dashboard/users/profile?userId=' + userId)
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('[userprofiles] failed to navigate to user edit page', err);
|
console.error('[userprofiles] failed to navigate to user edit page', err);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'access':
|
case 'access':
|
||||||
Dashboard.navigate('userlibraryaccess.html?userId=' + userId)
|
Dashboard.navigate('/dashboard/users/access?userId=' + userId)
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('[userprofiles] failed to navigate to user library page', err);
|
console.error('[userprofiles] failed to navigate to user library page', err);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'parentalcontrol':
|
case 'parentalcontrol':
|
||||||
Dashboard.navigate('userparentalcontrol.html?userId=' + userId)
|
Dashboard.navigate('/dashboard/users/parentalcontrol?userId=' + userId)
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('[userprofiles] failed to navigate to parental control page', err);
|
console.error('[userprofiles] failed to navigate to parental control page', err);
|
||||||
});
|
});
|
||||||
|
@ -146,7 +146,7 @@ const UserProfiles: FunctionComponent = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
(page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
|
(page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
|
||||||
Dashboard.navigate('usernew.html')
|
Dashboard.navigate('/dashboard/users/add')
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('[userprofiles] failed to navigate to new user page', err);
|
console.error('[userprofiles] failed to navigate to new user page', err);
|
||||||
});
|
});
|
|
@ -32,7 +32,7 @@ const getCheckedElementDataIds = (elements: NodeListOf<Element>) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
function onSaveComplete() {
|
function onSaveComplete() {
|
||||||
Dashboard.navigate('userprofiles.html')
|
Dashboard.navigate('/dashboard/users')
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('[useredit] failed to navigate to user profile', err);
|
console.error('[useredit] failed to navigate to user profile', err);
|
||||||
});
|
});
|
|
@ -1,14 +1,16 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
||||||
|
import { REDIRECTS } from 'apps/stable/routes/_redirects';
|
||||||
import ConnectionRequired from 'components/ConnectionRequired';
|
import ConnectionRequired from 'components/ConnectionRequired';
|
||||||
import ServerContentPage from 'components/ServerContentPage';
|
|
||||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||||
|
import { toRedirectRoute } from 'components/router/Redirect';
|
||||||
|
|
||||||
import AppLayout from './AppLayout';
|
import AppLayout from './AppLayout';
|
||||||
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
import { ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||||
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
||||||
|
|
||||||
const ExperimentalApp = () => {
|
const ExperimentalApp = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -20,26 +22,25 @@ const ExperimentalApp = () => {
|
||||||
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
|
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Admin routes */}
|
|
||||||
<Route element={<ConnectionRequired isAdminRequired />}>
|
|
||||||
{ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)}
|
|
||||||
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
|
|
||||||
|
|
||||||
<Route path='configurationpage' element={
|
|
||||||
<ServerContentPage view='/web/configurationpage' />
|
|
||||||
} />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* Public routes */}
|
{/* Public routes */}
|
||||||
<Route element={<ConnectionRequired isUserRequired={false} />}>
|
<Route element={<ConnectionRequired isUserRequired={false} />}>
|
||||||
<Route index element={<Navigate replace to='/home.html' />} />
|
<Route index element={<Navigate replace to='/home.html' />} />
|
||||||
|
|
||||||
{LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)}
|
{LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)}
|
||||||
</Route>
|
</Route>
|
||||||
|
</Route>
|
||||||
|
|
||||||
{/* Redirects for old paths */}
|
{/* Redirects for old paths */}
|
||||||
<Route path='serveractivity.html' element={<Navigate replace to='/dashboard/activity' />} />
|
{REDIRECTS.map(toRedirectRoute)}
|
||||||
</Route>
|
|
||||||
|
{/* Ignore dashboard routes */}
|
||||||
|
{Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => (
|
||||||
|
<Route
|
||||||
|
key={key}
|
||||||
|
path={`/${path}/*`}
|
||||||
|
element={null}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import AppBar from '@mui/material/AppBar';
|
import AppBar from '@mui/material/AppBar';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { ThemeProvider } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import { Outlet, useLocation } from 'react-router-dom';
|
import { Outlet, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import AppHeader from 'components/AppHeader';
|
import AppBody from 'components/AppBody';
|
||||||
import Backdrop from 'components/Backdrop';
|
import ElevationScroll from 'components/ElevationScroll';
|
||||||
|
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||||
|
|
||||||
import AppToolbar from './components/AppToolbar';
|
import AppToolbar from './components/AppToolbar';
|
||||||
import AppDrawer, { DRAWER_WIDTH, isDrawerPath } from './components/drawers/AppDrawer';
|
import AppDrawer, { isDrawerPath } from './components/drawers/AppDrawer';
|
||||||
import ElevationScroll from './components/ElevationScroll';
|
|
||||||
import theme from './theme';
|
|
||||||
|
|
||||||
import './AppOverrides.scss';
|
import './AppOverrides.scss';
|
||||||
|
|
||||||
|
@ -29,6 +28,7 @@ const AppLayout = () => {
|
||||||
const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned);
|
const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned);
|
||||||
const { user } = useApi();
|
const { user } = useApi();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
const isDrawerAvailable = isDrawerPath(location.pathname);
|
const isDrawerAvailable = isDrawerPath(location.pathname);
|
||||||
const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user);
|
const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user);
|
||||||
|
@ -47,17 +47,6 @@ const AppLayout = () => {
|
||||||
}, [ isDrawerActive, setIsDrawerActive ]);
|
}, [ isDrawerActive, setIsDrawerActive ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
|
||||||
<Backdrop />
|
|
||||||
|
|
||||||
<div style={{ display: 'none' }}>
|
|
||||||
{/*
|
|
||||||
* TODO: These components are not used, but views interact with them directly so the need to be
|
|
||||||
* present in the dom. We add them in a hidden element to prevent errors.
|
|
||||||
*/}
|
|
||||||
<AppHeader />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
<ElevationScroll elevate={isDrawerOpen}>
|
<ElevationScroll elevate={isDrawerOpen}>
|
||||||
<AppBar
|
<AppBar
|
||||||
|
@ -101,13 +90,11 @@ const AppLayout = () => {
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='mainAnimatedPages skinBody' />
|
<AppBody>
|
||||||
<div className='skinBody'>
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</AppBody>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</ThemeProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,6 @@ $mui-bp-xl: 1536px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix dashboard pages layout to work with drawer
|
|
||||||
.dashboardDocument .skinBody {
|
|
||||||
position: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide some items from the user "settings" page that are in the drawer
|
// Hide some items from the user "settings" page that are in the drawer
|
||||||
#myPreferencesMenuPage {
|
#myPreferencesMenuPage {
|
||||||
.lnkQuickConnectPreferences,
|
.lnkQuickConnectPreferences,
|
||||||
|
@ -26,8 +21,7 @@ $mui-bp-xl: 1536px;
|
||||||
|
|
||||||
// Fix the padding of some pages
|
// Fix the padding of some pages
|
||||||
.homePage.libraryPage, // Home page
|
.homePage.libraryPage, // Home page
|
||||||
.libraryPage:not(.withTabs), // Tabless library pages
|
.libraryPage:not(.withTabs) { // Tabless library pages
|
||||||
.content-primary.content-primary { // Dashboard pages
|
|
||||||
padding-top: 3.25rem !important;
|
padding-top: 3.25rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,14 @@
|
||||||
import ArrowBack from '@mui/icons-material/ArrowBack';
|
|
||||||
import MenuIcon from '@mui/icons-material/Menu';
|
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import appIcon from 'assets/img/icon-transparent.png';
|
import AppToolbar from 'components/toolbar/AppToolbar';
|
||||||
import { appRouter } from 'components/router/appRouter';
|
|
||||||
import { useApi } from 'hooks/useApi';
|
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import AppTabs from '../tabs/AppTabs';
|
import AppTabs from '../tabs/AppTabs';
|
||||||
import { isDrawerPath } from '../drawers/AppDrawer';
|
import { isDrawerPath } from '../drawers/AppDrawer';
|
||||||
import UserMenuButton from './UserMenuButton';
|
|
||||||
import RemotePlayButton from './RemotePlayButton';
|
import RemotePlayButton from './RemotePlayButton';
|
||||||
import SyncPlayButton from './SyncPlayButton';
|
import SyncPlayButton from './SyncPlayButton';
|
||||||
|
|
||||||
|
@ -25,97 +17,17 @@ interface AppToolbarProps {
|
||||||
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const onBackButtonClick = () => {
|
const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
|
||||||
appRouter.back()
|
|
||||||
.catch(err => {
|
|
||||||
console.error('[AppToolbar] error calling appRouter.back', err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppToolbar: FC<AppToolbarProps> = ({
|
|
||||||
isDrawerOpen,
|
isDrawerOpen,
|
||||||
onDrawerButtonClick
|
onDrawerButtonClick
|
||||||
}) => {
|
}) => {
|
||||||
const { user } = useApi();
|
|
||||||
const isUserLoggedIn = Boolean(user);
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const isDrawerAvailable = isDrawerPath(location.pathname);
|
const isDrawerAvailable = isDrawerPath(location.pathname);
|
||||||
const isBackButtonAvailable = appRouter.canGoBack();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toolbar
|
<AppToolbar
|
||||||
variant='dense'
|
buttons={
|
||||||
sx={{
|
|
||||||
flexWrap: {
|
|
||||||
xs: 'wrap',
|
|
||||||
lg: 'nowrap'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isUserLoggedIn && isDrawerAvailable && (
|
|
||||||
<Tooltip title={globalize.translate(isDrawerOpen ? 'MenuClose' : 'MenuOpen')}>
|
|
||||||
<IconButton
|
|
||||||
size='large'
|
|
||||||
edge='start'
|
|
||||||
color='inherit'
|
|
||||||
aria-label={globalize.translate(isDrawerOpen ? 'MenuClose' : 'MenuOpen')}
|
|
||||||
onClick={onDrawerButtonClick}
|
|
||||||
>
|
|
||||||
<MenuIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isBackButtonAvailable && (
|
|
||||||
<Tooltip title={globalize.translate('ButtonBack')}>
|
|
||||||
<IconButton
|
|
||||||
size='large'
|
|
||||||
// Set the edge if the drawer button is not shown
|
|
||||||
edge={!(isUserLoggedIn && isDrawerAvailable) ? 'start' : undefined}
|
|
||||||
color='inherit'
|
|
||||||
aria-label={globalize.translate('ButtonBack')}
|
|
||||||
onClick={onBackButtonClick}
|
|
||||||
>
|
|
||||||
<ArrowBack />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box
|
|
||||||
component={Link}
|
|
||||||
to='/'
|
|
||||||
color='inherit'
|
|
||||||
aria-label={globalize.translate('Home')}
|
|
||||||
sx={{
|
|
||||||
ml: 2,
|
|
||||||
display: 'inline-flex',
|
|
||||||
textDecoration: 'none'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
component='img'
|
|
||||||
src={appIcon}
|
|
||||||
sx={{
|
|
||||||
height: '2rem',
|
|
||||||
marginInlineEnd: 1
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Typography
|
|
||||||
variant='h6'
|
|
||||||
noWrap
|
|
||||||
component='div'
|
|
||||||
sx={{ display: { xs: 'none', sm: 'inline-block' } }}
|
|
||||||
>
|
|
||||||
Jellyfin
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<AppTabs isDrawerOpen={isDrawerOpen} />
|
|
||||||
|
|
||||||
{isUserLoggedIn && (
|
|
||||||
<>
|
<>
|
||||||
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'flex-end' }}>
|
|
||||||
<SyncPlayButton />
|
<SyncPlayButton />
|
||||||
<RemotePlayButton />
|
<RemotePlayButton />
|
||||||
|
|
||||||
|
@ -130,15 +42,15 @@ const AppToolbar: FC<AppToolbarProps> = ({
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 0 }}>
|
|
||||||
<UserMenuButton />
|
|
||||||
</Box>
|
|
||||||
</>
|
</>
|
||||||
)}
|
}
|
||||||
</Toolbar>
|
isDrawerAvailable={isDrawerAvailable}
|
||||||
|
isDrawerOpen={isDrawerOpen}
|
||||||
|
onDrawerButtonClick={onDrawerButtonClick}
|
||||||
|
>
|
||||||
|
<AppTabs isDrawerOpen={isDrawerOpen} />
|
||||||
|
</AppToolbar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AppToolbar;
|
export default ExperimentalAppToolbar;
|
||||||
|
|
|
@ -19,6 +19,7 @@ import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { pluginManager } from 'components/pluginManager';
|
import { pluginManager } from 'components/pluginManager';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
|
import { useSyncPlayGroups } from 'hooks/useSyncPlayGroups';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
import { PluginType } from 'types/plugin';
|
import { PluginType } from 'types/plugin';
|
||||||
import Events from 'utils/events';
|
import Events from 'utils/events';
|
||||||
|
@ -47,7 +48,6 @@ const SyncPlayMenu: FC<SyncPlayMenuProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [ syncPlay, setSyncPlay ] = useState<SyncPlayInstance>();
|
const [ syncPlay, setSyncPlay ] = useState<SyncPlayInstance>();
|
||||||
const { __legacyApiClient__, api, user } = useApi();
|
const { __legacyApiClient__, api, user } = useApi();
|
||||||
const [ groups, setGroups ] = useState<GroupInfoDto[]>([]);
|
|
||||||
const [ currentGroup, setCurrentGroup ] = useState<GroupInfoDto>();
|
const [ currentGroup, setCurrentGroup ] = useState<GroupInfoDto>();
|
||||||
const isSyncPlayEnabled = Boolean(currentGroup);
|
const isSyncPlayEnabled = Boolean(currentGroup);
|
||||||
|
|
||||||
|
@ -55,18 +55,7 @@ const SyncPlayMenu: FC<SyncPlayMenuProps> = ({
|
||||||
setSyncPlay(pluginManager.firstOfType(PluginType.SyncPlay)?.instance);
|
setSyncPlay(pluginManager.firstOfType(PluginType.SyncPlay)?.instance);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: groups } = useSyncPlayGroups();
|
||||||
const fetchGroups = async () => {
|
|
||||||
if (api) {
|
|
||||||
setGroups((await getSyncPlayApi(api).syncPlayGetGroups()).data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchGroups()
|
|
||||||
.catch(err => {
|
|
||||||
console.error('[SyncPlayMenu] unable to fetch SyncPlay groups', err);
|
|
||||||
});
|
|
||||||
}, [ api ]);
|
|
||||||
|
|
||||||
const onGroupAddClick = useCallback(() => {
|
const onGroupAddClick = useCallback(() => {
|
||||||
if (api && user) {
|
if (api && user) {
|
||||||
|
@ -224,7 +213,7 @@ const SyncPlayMenu: FC<SyncPlayMenuProps> = ({
|
||||||
/>
|
/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
} else if (groups.length === 0 && user?.Policy?.SyncPlayAccess !== SyncPlayUserAccessType.CreateAndJoinGroups) {
|
} else if (!groups?.length && user?.Policy?.SyncPlayAccess !== SyncPlayUserAccessType.CreateAndJoinGroups) {
|
||||||
menuItems.push(
|
menuItems.push(
|
||||||
<MenuItem key='sync-play-unavailable' disabled>
|
<MenuItem key='sync-play-unavailable' disabled>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
|
@ -234,7 +223,7 @@ const SyncPlayMenu: FC<SyncPlayMenuProps> = ({
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (groups.length > 0) {
|
if (groups && groups.length > 0) {
|
||||||
groups.forEach(group => {
|
groups.forEach(group => {
|
||||||
menuItems.push(
|
menuItems.push(
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|
|
@ -1,21 +1,15 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Route, Routes } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from '../../routes/asyncRoutes';
|
import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';
|
||||||
import { LEGACY_ADMIN_ROUTES, LEGACY_USER_ROUTES } from '../../routes/legacyRoutes';
|
|
||||||
|
import { ASYNC_USER_ROUTES } from '../../routes/asyncRoutes';
|
||||||
|
import { LEGACY_USER_ROUTES } from '../../routes/legacyRoutes';
|
||||||
|
import { isTabPath } from '../tabs/tabRoutes';
|
||||||
|
|
||||||
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 MainDrawerContent from './MainDrawerContent';
|
||||||
import ResponsiveDrawer, { ResponsiveDrawerProps } from './ResponsiveDrawer';
|
|
||||||
|
|
||||||
export const DRAWER_WIDTH = 240;
|
|
||||||
|
|
||||||
const DRAWERLESS_ROUTES = [
|
const DRAWERLESS_ROUTES = [
|
||||||
'edititemmetadata.html', // metadata manager
|
|
||||||
'video' // video player
|
'video' // video player
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -24,63 +18,29 @@ const MAIN_DRAWER_ROUTES = [
|
||||||
...LEGACY_USER_ROUTES
|
...LEGACY_USER_ROUTES
|
||||||
].filter(route => !DRAWERLESS_ROUTES.includes(route.path));
|
].filter(route => !DRAWERLESS_ROUTES.includes(route.path));
|
||||||
|
|
||||||
const ADMIN_DRAWER_ROUTES = [
|
|
||||||
...ASYNC_ADMIN_ROUTES,
|
|
||||||
...LEGACY_ADMIN_ROUTES,
|
|
||||||
{ path: '/configurationpage' } // Plugin configuration page
|
|
||||||
].filter(route => !DRAWERLESS_ROUTES.includes(route.path));
|
|
||||||
|
|
||||||
/** Utility function to check if a path has a drawer. */
|
/** Utility function to check if a path has a drawer. */
|
||||||
export const isDrawerPath = (path: string) => (
|
export const isDrawerPath = (path: string) => (
|
||||||
MAIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path)
|
MAIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path)
|
||||||
|| ADMIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
||||||
open = false,
|
open = false,
|
||||||
onClose,
|
onClose,
|
||||||
onOpen
|
onOpen
|
||||||
}) => (
|
}) => {
|
||||||
<Routes>
|
const location = useLocation();
|
||||||
{
|
const hasSecondaryToolBar = isTabPath(location.pathname);
|
||||||
MAIN_DRAWER_ROUTES.map(route => (
|
|
||||||
<Route
|
return (
|
||||||
key={route.path}
|
|
||||||
path={route.path}
|
|
||||||
element={
|
|
||||||
<ResponsiveDrawer
|
<ResponsiveDrawer
|
||||||
|
hasSecondaryToolBar={hasSecondaryToolBar}
|
||||||
open={open}
|
open={open}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onOpen={onOpen}
|
onOpen={onOpen}
|
||||||
>
|
>
|
||||||
<MainDrawerContent />
|
<MainDrawerContent />
|
||||||
</ResponsiveDrawer>
|
</ResponsiveDrawer>
|
||||||
}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
{
|
|
||||||
ADMIN_DRAWER_ROUTES.map(route => (
|
|
||||||
<Route
|
|
||||||
key={route.path}
|
|
||||||
path={route.path}
|
|
||||||
element={
|
|
||||||
<ResponsiveDrawer
|
|
||||||
open={open}
|
|
||||||
onClose={onClose}
|
|
||||||
onOpen={onOpen}
|
|
||||||
>
|
|
||||||
<ServerDrawerSection />
|
|
||||||
<DevicesDrawerSection />
|
|
||||||
<LiveTvDrawerSection />
|
|
||||||
<AdvancedDrawerSection />
|
|
||||||
<PluginDrawerSection />
|
|
||||||
</ResponsiveDrawer>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</Routes>
|
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default AppDrawer;
|
export default AppDrawer;
|
||||||
|
|
|
@ -17,12 +17,12 @@ import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ListItemLink from 'components/ListItemLink';
|
||||||
|
import { appRouter } from 'components/router/appRouter';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
import { useWebConfig } from 'hooks/useWebConfig';
|
import { useWebConfig } from 'hooks/useWebConfig';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
import { appRouter } from 'components/router/appRouter';
|
|
||||||
|
|
||||||
import ListItemLink from './ListItemLink';
|
|
||||||
import LibraryIcon from '../LibraryIcon';
|
import LibraryIcon from '../LibraryIcon';
|
||||||
|
|
||||||
const MainDrawerContent = () => {
|
const MainDrawerContent = () => {
|
||||||
|
@ -150,7 +150,7 @@ const MainDrawerContent = () => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/dashboard.html'>
|
<ListItemLink to='/dashboard'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -158,7 +158,7 @@ const MainDrawerContent = () => {
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/edititemmetadata.html'>
|
<ListItemLink to='/metadata'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Edit />
|
<Edit />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
|
85
src/apps/experimental/components/library/AlphabetPicker.tsx
Normal file
85
src/apps/experimental/components/library/AlphabetPicker.tsx
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import ToggleButton from '@mui/material/ToggleButton';
|
||||||
|
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||||
|
|
||||||
|
import { LibraryViewSettings } from 'types/library';
|
||||||
|
import 'components/alphaPicker/style.scss';
|
||||||
|
|
||||||
|
interface AlphabetPickerProps {
|
||||||
|
className?: string;
|
||||||
|
libraryViewSettings: LibraryViewSettings;
|
||||||
|
setLibraryViewSettings: React.Dispatch<
|
||||||
|
React.SetStateAction<LibraryViewSettings>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlphabetPicker: React.FC<AlphabetPickerProps> = ({
|
||||||
|
className,
|
||||||
|
libraryViewSettings,
|
||||||
|
setLibraryViewSettings
|
||||||
|
}) => {
|
||||||
|
const handleValue = useCallback(
|
||||||
|
(
|
||||||
|
event: React.MouseEvent<HTMLElement>,
|
||||||
|
newValue: string | null | undefined
|
||||||
|
) => {
|
||||||
|
setLibraryViewSettings((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
StartIndex: 0,
|
||||||
|
Alphabet: newValue
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[setLibraryViewSettings]
|
||||||
|
);
|
||||||
|
|
||||||
|
const containerClassName = classNames(
|
||||||
|
'alphaPicker',
|
||||||
|
className,
|
||||||
|
'alphaPicker-fixed-right'
|
||||||
|
);
|
||||||
|
|
||||||
|
const btnClassName = classNames(
|
||||||
|
'paper-icon-button-light',
|
||||||
|
'alphaPickerButton'
|
||||||
|
);
|
||||||
|
|
||||||
|
const letters = ['#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className={containerClassName}
|
||||||
|
sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '1.5em',
|
||||||
|
fontSize: {
|
||||||
|
xs: '80%',
|
||||||
|
lg: '88%'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
orientation='vertical'
|
||||||
|
value={libraryViewSettings.Alphabet}
|
||||||
|
exclusive
|
||||||
|
color='primary'
|
||||||
|
size='small'
|
||||||
|
onChange={handleValue}
|
||||||
|
>
|
||||||
|
{letters.map((l) => (
|
||||||
|
<ToggleButton
|
||||||
|
key={l}
|
||||||
|
value={l}
|
||||||
|
className={btnClassName}
|
||||||
|
>
|
||||||
|
{l}
|
||||||
|
</ToggleButton>
|
||||||
|
))}
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlphabetPicker;
|
|
@ -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;
|
91
src/apps/experimental/components/library/Pagination.tsx
Normal file
91
src/apps/experimental/components/library/Pagination.tsx
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
|
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import * as userSettings from 'scripts/settings/userSettings';
|
||||||
|
import { LibraryViewSettings } from 'types/library';
|
||||||
|
|
||||||
|
interface PaginationProps {
|
||||||
|
libraryViewSettings: LibraryViewSettings;
|
||||||
|
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||||
|
totalRecordCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Pagination: FC<PaginationProps> = ({
|
||||||
|
libraryViewSettings,
|
||||||
|
setLibraryViewSettings,
|
||||||
|
totalRecordCount
|
||||||
|
}) => {
|
||||||
|
const limit = userSettings.libraryPageSize(undefined);
|
||||||
|
const startIndex = libraryViewSettings.StartIndex || 0;
|
||||||
|
const recordsStart = totalRecordCount ? startIndex + 1 : 0;
|
||||||
|
const recordsEnd = limit ?
|
||||||
|
Math.min(startIndex + limit, totalRecordCount) :
|
||||||
|
totalRecordCount;
|
||||||
|
const showControls = limit > 0 && limit < totalRecordCount;
|
||||||
|
|
||||||
|
const onNextPageClick = useCallback(() => {
|
||||||
|
if (limit > 0) {
|
||||||
|
const newIndex = startIndex + limit;
|
||||||
|
setLibraryViewSettings((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
StartIndex: newIndex
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [limit, setLibraryViewSettings, startIndex]);
|
||||||
|
|
||||||
|
const onPreviousPageClick = useCallback(() => {
|
||||||
|
if (limit > 0) {
|
||||||
|
const newIndex = Math.max(0, startIndex - limit);
|
||||||
|
setLibraryViewSettings((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
StartIndex: newIndex
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [limit, setLibraryViewSettings, startIndex]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className='paging'>
|
||||||
|
<Box
|
||||||
|
className='listPaging'
|
||||||
|
style={{ display: 'flex', alignItems: 'center' }}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{globalize.translate(
|
||||||
|
'ListPaging',
|
||||||
|
recordsStart,
|
||||||
|
recordsEnd,
|
||||||
|
totalRecordCount
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{showControls && (
|
||||||
|
<ButtonGroup>
|
||||||
|
<IconButton
|
||||||
|
title={globalize.translate('Previous')}
|
||||||
|
className='paper-icon-button-light btnPreviousPage autoSize'
|
||||||
|
disabled={startIndex == 0}
|
||||||
|
onClick={onPreviousPageClick}
|
||||||
|
>
|
||||||
|
<ArrowBackIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
title={globalize.translate('Next')}
|
||||||
|
className='paper-icon-button-light btnNextPage autoSize'
|
||||||
|
disabled={startIndex + limit >= totalRecordCount }
|
||||||
|
onClick={onNextPageClick}
|
||||||
|
>
|
||||||
|
<ArrowForwardIcon />
|
||||||
|
</IconButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pagination;
|
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')}
|
title={globalize.translate('Sort')}
|
||||||
sx={{ ml: 2 }}
|
sx={{ ml: 2 }}
|
||||||
aria-describedby={id}
|
aria-describedby={id}
|
||||||
className='paper-icon-button-light btnShuffle autoSize'
|
className='paper-icon-button-light btnSort autoSize'
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<SortByAlphaIcon />
|
<SortByAlphaIcon />
|
||||||
|
|
|
@ -100,7 +100,7 @@ const ViewSettingsButton: FC<ViewSettingsButtonProps> = ({
|
||||||
title={globalize.translate('ButtonSelectView')}
|
title={globalize.translate('ButtonSelectView')}
|
||||||
sx={{ ml: 2 }}
|
sx={{ ml: 2 }}
|
||||||
aria-describedby={id}
|
aria-describedby={id}
|
||||||
className='paper-icon-button-light btnShuffle autoSize'
|
className='paper-icon-button-light btnSelectView autoSize'
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<ViewComfyIcon />
|
<ViewComfyIcon />
|
||||||
|
|
|
@ -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';
|
export * from './user';
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { AsyncRoute, AsyncRouteType } from '../../../../components/router/AsyncRoute';
|
import { AsyncRoute, AsyncRouteType } from '../../../../components/router/AsyncRoute';
|
||||||
|
|
||||||
export const ASYNC_USER_ROUTES: AsyncRoute[] = [
|
export const ASYNC_USER_ROUTES: AsyncRoute[] = [
|
||||||
|
{ path: 'quickconnect', page: 'quickConnect' },
|
||||||
{ path: 'search.html', page: 'search' },
|
{ path: 'search.html', page: 'search' },
|
||||||
{ path: 'userprofile.html', page: 'user/userprofile' },
|
{ path: 'userprofile.html', page: 'user/userprofile' },
|
||||||
{ path: 'home.html', page: 'home', type: AsyncRouteType.Experimental },
|
{ path: 'home.html', page: 'home', type: AsyncRouteType.Experimental },
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
export * from './admin';
|
|
||||||
export * from './public';
|
export * from './public';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
|
|
|
@ -49,12 +49,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
|
||||||
controller: 'user/home/index',
|
controller: 'user/home/index',
|
||||||
view: 'user/home/index.html'
|
view: 'user/home/index.html'
|
||||||
}
|
}
|
||||||
}, {
|
|
||||||
path: 'mypreferencesquickconnect.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'user/quickConnect/index',
|
|
||||||
view: 'user/quickConnect/index.html'
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
path: 'mypreferencesplayback.html',
|
path: 'mypreferencesplayback.html',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
|
|
|
@ -1,26 +1,21 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
|
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
import AppHeader from 'components/AppHeader';
|
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
||||||
import Backdrop from 'components/Backdrop';
|
import AppBody from 'components/AppBody';
|
||||||
import ServerContentPage from 'components/ServerContentPage';
|
|
||||||
import ConnectionRequired from 'components/ConnectionRequired';
|
import ConnectionRequired from 'components/ConnectionRequired';
|
||||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||||
|
import { toRedirectRoute } from 'components/router/Redirect';
|
||||||
|
|
||||||
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
import { ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||||
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
||||||
|
import { REDIRECTS } from './routes/_redirects';
|
||||||
|
|
||||||
const Layout = () => (
|
const Layout = () => (
|
||||||
<>
|
<AppBody>
|
||||||
<Backdrop />
|
|
||||||
<AppHeader />
|
|
||||||
|
|
||||||
<div className='mainAnimatedPages skinBody' />
|
|
||||||
<div className='skinBody'>
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</AppBody>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const StableApp = () => (
|
const StableApp = () => (
|
||||||
|
@ -32,16 +27,6 @@ const StableApp = () => (
|
||||||
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
|
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Admin routes */}
|
|
||||||
<Route path='/' element={<ConnectionRequired isAdminRequired />}>
|
|
||||||
{ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)}
|
|
||||||
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
|
|
||||||
|
|
||||||
<Route path='configurationpage' element={
|
|
||||||
<ServerContentPage view='/web/configurationpage' />
|
|
||||||
} />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* Public routes */}
|
{/* Public routes */}
|
||||||
<Route path='/' element={<ConnectionRequired isUserRequired={false} />}>
|
<Route path='/' element={<ConnectionRequired isUserRequired={false} />}>
|
||||||
<Route index element={<Navigate replace to='/home.html' />} />
|
<Route index element={<Navigate replace to='/home.html' />} />
|
||||||
|
@ -51,10 +36,19 @@ const StableApp = () => (
|
||||||
|
|
||||||
{/* Suppress warnings for unhandled routes */}
|
{/* Suppress warnings for unhandled routes */}
|
||||||
<Route path='*' element={null} />
|
<Route path='*' element={null} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
{/* Redirects for old paths */}
|
{/* Redirects for old paths */}
|
||||||
<Route path='/serveractivity.html' element={<Navigate replace to='/dashboard/activity' />} />
|
{REDIRECTS.map(toRedirectRoute)}
|
||||||
</Route>
|
|
||||||
|
{/* Ignore dashboard routes */}
|
||||||
|
{Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => (
|
||||||
|
<Route
|
||||||
|
key={key}
|
||||||
|
path={`/${path}/*`}
|
||||||
|
element={null}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
5
src/apps/stable/routes/_redirects.ts
Normal file
5
src/apps/stable/routes/_redirects.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import type { Redirect } from 'components/router/Redirect';
|
||||||
|
|
||||||
|
export const REDIRECTS: Redirect[] = [
|
||||||
|
{ 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';
|
export * from './user';
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
|
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
|
||||||
|
|
||||||
export const ASYNC_USER_ROUTES: AsyncRoute[] = [
|
export const ASYNC_USER_ROUTES: AsyncRoute[] = [
|
||||||
|
{ path: 'quickconnect', page: 'quickConnect' },
|
||||||
{ path: 'search.html', page: 'search' },
|
{ path: 'search.html', page: 'search' },
|
||||||
{ path: 'userprofile.html', page: 'user/userprofile' }
|
{ path: 'userprofile.html', page: 'user/userprofile' }
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,185 +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: 'quickConnect.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/quickConnect',
|
|
||||||
view: 'dashboard/quickConnect.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'dlnaprofile.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/dlna/profile',
|
|
||||||
view: 'dashboard/dlna/profile.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'dlnaprofiles.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/dlna/profiles',
|
|
||||||
view: 'dashboard/dlna/profiles.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'dlnasettings.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/dlna/settings',
|
|
||||||
view: 'dashboard/dlna/settings.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'addplugin.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/plugins/add/index',
|
|
||||||
view: 'dashboard/plugins/add/index.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'library.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/library',
|
|
||||||
view: 'dashboard/library.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'librarydisplay.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/librarydisplay',
|
|
||||||
view: 'dashboard/librarydisplay.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'edititemmetadata.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'edititemmetadata',
|
|
||||||
view: 'edititemmetadata.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'encodingsettings.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/encodingsettings',
|
|
||||||
view: 'dashboard/encodingsettings.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'log.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/logs',
|
|
||||||
view: 'dashboard/logs.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'metadataimages.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/metadataImages',
|
|
||||||
view: 'dashboard/metadataimages.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'metadatanfo.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/metadatanfo',
|
|
||||||
view: 'dashboard/metadatanfo.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'playbackconfiguration.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/playback',
|
|
||||||
view: 'dashboard/playback.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'availableplugins.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/plugins/available/index',
|
|
||||||
view: 'dashboard/plugins/available/index.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'repositories.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/plugins/repositories/index',
|
|
||||||
view: 'dashboard/plugins/repositories/index.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'livetvguideprovider.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'livetvguideprovider',
|
|
||||||
view: 'livetvguideprovider.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'livetvsettings.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'livetvsettings',
|
|
||||||
view: 'livetvsettings.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'livetvstatus.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'livetvstatus',
|
|
||||||
view: 'livetvstatus.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'livetvtuner.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'livetvtuner',
|
|
||||||
view: 'livetvtuner.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'installedplugins.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/plugins/installed/index',
|
|
||||||
view: 'dashboard/plugins/installed/index.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'scheduledtask.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/scheduledtasks/scheduledtask',
|
|
||||||
view: 'dashboard/scheduledtasks/scheduledtask.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'scheduledtasks.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/scheduledtasks/scheduledtasks',
|
|
||||||
view: 'dashboard/scheduledtasks/scheduledtasks.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: '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 './public';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
|
|
|
@ -49,12 +49,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
|
||||||
controller: 'user/home/index',
|
controller: 'user/home/index',
|
||||||
view: 'user/home/index.html'
|
view: 'user/home/index.html'
|
||||||
}
|
}
|
||||||
}, {
|
|
||||||
path: 'mypreferencesquickconnect.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'user/quickConnect/index',
|
|
||||||
view: 'user/quickConnect/index.html'
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
path: 'mypreferencesplayback.html',
|
path: 'mypreferencesplayback.html',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
|
|
116
src/apps/stable/routes/quickConnect/index.tsx
Normal file
116
src/apps/stable/routes/quickConnect/index.tsx
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import { getQuickConnectApi } from '@jellyfin/sdk/lib/utils/api/quick-connect-api';
|
||||||
|
import React, { FC, FormEvent, useCallback, useMemo, useState } from 'react';
|
||||||
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import Page from 'components/Page';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import InputElement from 'elements/InputElement';
|
||||||
|
import ButtonElement from 'elements/ButtonElement';
|
||||||
|
import { useApi } from 'hooks/useApi';
|
||||||
|
|
||||||
|
import './quickConnect.scss';
|
||||||
|
|
||||||
|
const QuickConnectPage: FC = () => {
|
||||||
|
const { api, user } = useApi();
|
||||||
|
const [ searchParams ] = useSearchParams();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
const initialValue = useMemo(() => searchParams.get('code') ?? '', []);
|
||||||
|
const [ code, setCode ] = useState(initialValue);
|
||||||
|
const [ error, setError ] = useState<string>();
|
||||||
|
const [ success, setSuccess ] = useState(false);
|
||||||
|
|
||||||
|
const onCodeChange = useCallback((value: string) => {
|
||||||
|
setCode(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSubmitCode = useCallback((e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError(undefined);
|
||||||
|
|
||||||
|
const form = e.currentTarget;
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
setError('QuickConnectInvalidCode');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!api) {
|
||||||
|
console.error('[QuickConnect] cannot authorize, missing api instance');
|
||||||
|
setError('UnknownError');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = searchParams.get('userId') ?? user?.Id;
|
||||||
|
const normalizedCode = code.replace(/\s/g, '');
|
||||||
|
console.log('[QuickConnect] authorizing code %s as user %s', normalizedCode, userId);
|
||||||
|
|
||||||
|
getQuickConnectApi(api)
|
||||||
|
.authorizeQuickConnect({
|
||||||
|
code: normalizedCode,
|
||||||
|
userId
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
setSuccess(true);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setError('QuickConnectAuthorizeFail');
|
||||||
|
});
|
||||||
|
}, [api, code, searchParams, user?.Id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
id='quickConnectPreferencesPage'
|
||||||
|
title={globalize.translate('QuickConnect')}
|
||||||
|
className='mainAnimatedPage libraryPage userPreferencesPage noSecondaryNavPage'
|
||||||
|
>
|
||||||
|
<div className='padded-left padded-right padded-bottom-page'>
|
||||||
|
<form onSubmit={onSubmitCode}>
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<h2 className='sectionTitle'>
|
||||||
|
{globalize.translate('QuickConnect')}
|
||||||
|
</h2>
|
||||||
|
<div>
|
||||||
|
{globalize.translate('QuickConnectDescription')}
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className='quickConnectError'>
|
||||||
|
{globalize.translate(error)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success ? (
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<p>
|
||||||
|
{globalize.translate('QuickConnectAuthorizeSuccess')}
|
||||||
|
</p>
|
||||||
|
<Link to='/home.html' className='button-link emby-button'>
|
||||||
|
{globalize.translate('GoHome')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<InputElement
|
||||||
|
containerClassName='inputContainer'
|
||||||
|
initialValue={initialValue}
|
||||||
|
onChange={onCodeChange}
|
||||||
|
id='txtQuickConnectCode'
|
||||||
|
label='LabelQuickConnectCode'
|
||||||
|
type='text'
|
||||||
|
options="inputmode='numeric' pattern='[0-9\s]*' minlength='6' required autocomplete='off'"
|
||||||
|
/>
|
||||||
|
<ButtonElement
|
||||||
|
type='submit'
|
||||||
|
className='raised button-submit block'
|
||||||
|
title={globalize.translate('Authorize')}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QuickConnectPage;
|
7
src/apps/stable/routes/quickConnect/quickConnect.scss
Normal file
7
src/apps/stable/routes/quickConnect/quickConnect.scss
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.quickConnectError {
|
||||||
|
border-radius: 0.2em;
|
||||||
|
background-color: #160b0b;
|
||||||
|
color: #f4c7c3;
|
||||||
|
padding: 0.7em 0.5em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
24
src/components/AppBody.tsx
Normal file
24
src/components/AppBody.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import React, { FC, useEffect } from 'react';
|
||||||
|
import viewContainer from './viewContainer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple component that includes the correct structure for ViewManager pages
|
||||||
|
* to exist alongside standard React pages.
|
||||||
|
*/
|
||||||
|
const AppBody: FC = ({ children }) => {
|
||||||
|
useEffect(() => () => {
|
||||||
|
// Reset view container state on unload
|
||||||
|
viewContainer.reset();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='mainAnimatedPages skinBody' />
|
||||||
|
<div className='skinBody'>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppBody;
|
|
@ -1,19 +1,29 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { FC, useEffect } from 'react';
|
||||||
|
|
||||||
const AppHeader = () => {
|
interface AppHeaderParams {
|
||||||
|
isHidden?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppHeader: FC<AppHeaderParams> = ({
|
||||||
|
isHidden = false
|
||||||
|
}) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Initialize the UI components after first render
|
// Initialize the UI components after first render
|
||||||
import('../scripts/libraryMenu');
|
import('../scripts/libraryMenu');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
/**
|
||||||
|
* NOTE: These components are not used with the new layouts, but legacy views interact with the elements
|
||||||
|
* directly so they need to be present in the DOM. We use display: none to hide them and prevent errors.
|
||||||
|
*/
|
||||||
|
<div style={isHidden ? { display: 'none' } : undefined}>
|
||||||
<div className='mainDrawer hide'>
|
<div className='mainDrawer hide'>
|
||||||
<div className='mainDrawer-scrollContainer scrollContainer focuscontainer-y' />
|
<div className='mainDrawer-scrollContainer scrollContainer focuscontainer-y' />
|
||||||
</div>
|
</div>
|
||||||
<div className='skinHeader focuscontainer-x' />
|
<div className='skinHeader focuscontainer-x' />
|
||||||
<div className='mainDrawerHandle' />
|
<div className='mainDrawerHandle' />
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,14 +5,13 @@ import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
|
|
||||||
import browser from 'scripts/browser';
|
import browser from 'scripts/browser';
|
||||||
|
|
||||||
import { DRAWER_WIDTH } from './AppDrawer';
|
export const DRAWER_WIDTH = 240;
|
||||||
import { isTabPath } from '../tabs/tabRoutes';
|
|
||||||
|
|
||||||
export interface ResponsiveDrawerProps {
|
export interface ResponsiveDrawerProps {
|
||||||
|
hasSecondaryToolBar?: boolean
|
||||||
open: boolean
|
open: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onOpen: () => void
|
onOpen: () => void
|
||||||
|
@ -20,18 +19,17 @@ export interface ResponsiveDrawerProps {
|
||||||
|
|
||||||
const ResponsiveDrawer: FC<ResponsiveDrawerProps> = ({
|
const ResponsiveDrawer: FC<ResponsiveDrawerProps> = ({
|
||||||
children,
|
children,
|
||||||
|
hasSecondaryToolBar = false,
|
||||||
open = false,
|
open = false,
|
||||||
onClose,
|
onClose,
|
||||||
onOpen
|
onOpen
|
||||||
}) => {
|
}) => {
|
||||||
const location = useLocation();
|
|
||||||
const isSmallScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
|
const isSmallScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
|
||||||
const isLargeScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('lg'));
|
const isLargeScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('lg'));
|
||||||
const isTallToolbar = isTabPath(location.pathname) && !isLargeScreen;
|
|
||||||
|
|
||||||
const getToolbarStyles = useCallback((theme: Theme) => ({
|
const getToolbarStyles = useCallback((theme: Theme) => ({
|
||||||
marginBottom: isTallToolbar ? theme.spacing(6) : 0
|
marginBottom: (hasSecondaryToolBar && !isLargeScreen) ? theme.spacing(6) : 0
|
||||||
}), [ isTallToolbar ]);
|
}), [ hasSecondaryToolBar, isLargeScreen ]);
|
||||||
|
|
||||||
return ( isSmallScreen ? (
|
return ( isSmallScreen ? (
|
||||||
/* DESKTOP DRAWER */
|
/* DESKTOP DRAWER */
|
|
@ -5,24 +5,30 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import escapeHtml from 'escape-html';
|
import escapeHtml from 'escape-html';
|
||||||
import datetime from '../../scripts/datetime';
|
|
||||||
import imageLoader from '../images/imageLoader';
|
import cardBuilderUtils from './cardBuilderUtils';
|
||||||
import itemHelper from '../itemHelper';
|
import browser from 'scripts/browser';
|
||||||
|
import datetime from 'scripts/datetime';
|
||||||
|
import dom from 'scripts/dom';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import imageHelper from 'scripts/imagehelper';
|
||||||
|
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
|
||||||
|
import { randomInt } from 'utils/number';
|
||||||
|
|
||||||
import focusManager from '../focusManager';
|
import focusManager from '../focusManager';
|
||||||
|
import imageLoader from '../images/imageLoader';
|
||||||
import indicators from '../indicators/indicators';
|
import indicators from '../indicators/indicators';
|
||||||
import globalize from '../../scripts/globalize';
|
import itemHelper from '../itemHelper';
|
||||||
import layoutManager from '../layoutManager';
|
import layoutManager from '../layoutManager';
|
||||||
import dom from '../../scripts/dom';
|
|
||||||
import browser from '../../scripts/browser';
|
|
||||||
import { playbackManager } from '../playback/playbackmanager';
|
import { playbackManager } from '../playback/playbackmanager';
|
||||||
import itemShortcuts from '../shortcuts';
|
|
||||||
import imageHelper from '../../scripts/imagehelper';
|
|
||||||
import { randomInt } from '../../utils/number.ts';
|
|
||||||
import './card.scss';
|
|
||||||
import '../../elements/emby-button/paper-icon-button-light';
|
|
||||||
import '../guide/programs.scss';
|
|
||||||
import ServerConnections from '../ServerConnections';
|
|
||||||
import { appRouter } from '../router/appRouter';
|
import { appRouter } from '../router/appRouter';
|
||||||
|
import ServerConnections from '../ServerConnections';
|
||||||
|
import itemShortcuts from '../shortcuts';
|
||||||
|
|
||||||
|
import 'elements/emby-button/paper-icon-button-light';
|
||||||
|
|
||||||
|
import './card.scss';
|
||||||
|
import '../guide/programs.scss';
|
||||||
|
|
||||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||||
|
|
||||||
|
@ -41,217 +47,6 @@ export function getCardsHtml(items, options) {
|
||||||
return buildCardsHtmlInternal(items, options);
|
return buildCardsHtmlInternal(items, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes the number of posters per row.
|
|
||||||
* @param {string} shape - Shape of the cards.
|
|
||||||
* @param {number} screenWidth - Width of the screen.
|
|
||||||
* @param {boolean} isOrientationLandscape - Flag for the orientation of the screen.
|
|
||||||
* @returns {number} Number of cards per row for an itemsContainer.
|
|
||||||
*/
|
|
||||||
function getPostersPerRow(shape, screenWidth, isOrientationLandscape) {
|
|
||||||
switch (shape) {
|
|
||||||
case 'portrait':
|
|
||||||
if (layoutManager.tv) {
|
|
||||||
return 100 / 16.66666667;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 2200) {
|
|
||||||
return 100 / 10;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1920) {
|
|
||||||
return 100 / 11.1111111111;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1600) {
|
|
||||||
return 100 / 12.5;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1400) {
|
|
||||||
return 100 / 14.28571428571;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1200) {
|
|
||||||
return 100 / 16.66666667;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 800) {
|
|
||||||
return 5;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 700) {
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 500) {
|
|
||||||
return 100 / 33.33333333;
|
|
||||||
}
|
|
||||||
return 100 / 33.33333333;
|
|
||||||
case 'square':
|
|
||||||
if (layoutManager.tv) {
|
|
||||||
return 100 / 16.66666667;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 2200) {
|
|
||||||
return 100 / 10;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1920) {
|
|
||||||
return 100 / 11.1111111111;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1600) {
|
|
||||||
return 100 / 12.5;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1400) {
|
|
||||||
return 100 / 14.28571428571;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1200) {
|
|
||||||
return 100 / 16.66666667;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 800) {
|
|
||||||
return 5;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 700) {
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 500) {
|
|
||||||
return 100 / 33.33333333;
|
|
||||||
}
|
|
||||||
return 2;
|
|
||||||
case 'banner':
|
|
||||||
if (screenWidth >= 2200) {
|
|
||||||
return 100 / 25;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1200) {
|
|
||||||
return 100 / 33.33333333;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 800) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
case 'backdrop':
|
|
||||||
if (layoutManager.tv) {
|
|
||||||
return 100 / 25;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 2500) {
|
|
||||||
return 6;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1600) {
|
|
||||||
return 5;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1200) {
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 770) {
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 420) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
case 'smallBackdrop':
|
|
||||||
if (screenWidth >= 1600) {
|
|
||||||
return 100 / 12.5;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1400) {
|
|
||||||
return 100 / 14.2857142857;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1200) {
|
|
||||||
return 100 / 16.66666667;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1000) {
|
|
||||||
return 5;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 800) {
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 500) {
|
|
||||||
return 100 / 33.33333333;
|
|
||||||
}
|
|
||||||
return 2;
|
|
||||||
case 'overflowSmallBackdrop':
|
|
||||||
if (layoutManager.tv) {
|
|
||||||
return 100 / 18.9;
|
|
||||||
}
|
|
||||||
if (isOrientationLandscape) {
|
|
||||||
if (screenWidth >= 800) {
|
|
||||||
return 100 / 15.5;
|
|
||||||
}
|
|
||||||
return 100 / 23.3;
|
|
||||||
} else {
|
|
||||||
if (screenWidth >= 540) {
|
|
||||||
return 100 / 30;
|
|
||||||
}
|
|
||||||
return 100 / 72;
|
|
||||||
}
|
|
||||||
case 'overflowPortrait':
|
|
||||||
|
|
||||||
if (layoutManager.tv) {
|
|
||||||
return 100 / 15.5;
|
|
||||||
}
|
|
||||||
if (isOrientationLandscape) {
|
|
||||||
if (screenWidth >= 1700) {
|
|
||||||
return 100 / 11.6;
|
|
||||||
}
|
|
||||||
return 100 / 15.5;
|
|
||||||
} else {
|
|
||||||
if (screenWidth >= 1400) {
|
|
||||||
return 100 / 15;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1200) {
|
|
||||||
return 100 / 18;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 760) {
|
|
||||||
return 100 / 23;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 400) {
|
|
||||||
return 100 / 31.5;
|
|
||||||
}
|
|
||||||
return 100 / 42;
|
|
||||||
}
|
|
||||||
case 'overflowSquare':
|
|
||||||
if (layoutManager.tv) {
|
|
||||||
return 100 / 15.5;
|
|
||||||
}
|
|
||||||
if (isOrientationLandscape) {
|
|
||||||
if (screenWidth >= 1700) {
|
|
||||||
return 100 / 11.6;
|
|
||||||
}
|
|
||||||
return 100 / 15.5;
|
|
||||||
} else {
|
|
||||||
if (screenWidth >= 1400) {
|
|
||||||
return 100 / 15;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1200) {
|
|
||||||
return 100 / 18;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 760) {
|
|
||||||
return 100 / 23;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 540) {
|
|
||||||
return 100 / 31.5;
|
|
||||||
}
|
|
||||||
return 100 / 42;
|
|
||||||
}
|
|
||||||
case 'overflowBackdrop':
|
|
||||||
if (layoutManager.tv) {
|
|
||||||
return 100 / 23.3;
|
|
||||||
}
|
|
||||||
if (isOrientationLandscape) {
|
|
||||||
if (screenWidth >= 1700) {
|
|
||||||
return 100 / 18.5;
|
|
||||||
}
|
|
||||||
return 100 / 23.3;
|
|
||||||
} else {
|
|
||||||
if (screenWidth >= 1800) {
|
|
||||||
return 100 / 23.5;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 1400) {
|
|
||||||
return 100 / 30;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 760) {
|
|
||||||
return 100 / 40;
|
|
||||||
}
|
|
||||||
if (screenWidth >= 640) {
|
|
||||||
return 100 / 56;
|
|
||||||
}
|
|
||||||
return 100 / 72;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the window is resizable.
|
* Checks if the window is resizable.
|
||||||
* @param {number} windowWidth - Width of the device's screen.
|
* @param {number} windowWidth - Width of the device's screen.
|
||||||
|
@ -278,7 +73,7 @@ function isResizable(windowWidth) {
|
||||||
* @returns {number} Width of the image for a card.
|
* @returns {number} Width of the image for a card.
|
||||||
*/
|
*/
|
||||||
function getImageWidth(shape, screenWidth, isOrientationLandscape) {
|
function getImageWidth(shape, screenWidth, isOrientationLandscape) {
|
||||||
const imagesPerRow = getPostersPerRow(shape, screenWidth, isOrientationLandscape);
|
const imagesPerRow = cardBuilderUtils.getPostersPerRow(shape, screenWidth, isOrientationLandscape, layoutManager.tv);
|
||||||
return Math.round(screenWidth / imagesPerRow);
|
return Math.round(screenWidth / imagesPerRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,16 +96,16 @@ function setCardData(items, options) {
|
||||||
options.shape = 'banner';
|
options.shape = 'banner';
|
||||||
options.coverImage = true;
|
options.coverImage = true;
|
||||||
} else if (primaryImageAspectRatio >= 1.33) {
|
} else if (primaryImageAspectRatio >= 1.33) {
|
||||||
options.shape = requestedShape === 'autooverflow' ? 'overflowBackdrop' : 'backdrop';
|
options.shape = getBackdropShape(requestedShape === 'autooverflow');
|
||||||
} else if (primaryImageAspectRatio > 0.71) {
|
} else if (primaryImageAspectRatio > 0.71) {
|
||||||
options.shape = requestedShape === 'autooverflow' ? 'overflowSquare' : 'square';
|
options.shape = getSquareShape(requestedShape === 'autooverflow');
|
||||||
} else {
|
} else {
|
||||||
options.shape = requestedShape === 'autooverflow' ? 'overflowPortrait' : 'portrait';
|
options.shape = getPortraitShape(requestedShape === 'autooverflow');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.shape) {
|
if (!options.shape) {
|
||||||
options.shape = options.defaultShape || (requestedShape === 'autooverflow' ? 'overflowSquare' : 'square');
|
options.shape = options.defaultShape || getSquareShape(requestedShape === 'autooverflow');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,7 +113,7 @@ function setCardData(items, options) {
|
||||||
options.preferThumb = options.shape === 'backdrop' || options.shape === 'overflowBackdrop';
|
options.preferThumb = options.shape === 'backdrop' || options.shape === 'overflowBackdrop';
|
||||||
}
|
}
|
||||||
|
|
||||||
options.uiAspect = getDesiredAspect(options.shape);
|
options.uiAspect = cardBuilderUtils.getDesiredAspect(options.shape);
|
||||||
options.primaryImageAspectRatio = primaryImageAspectRatio;
|
options.primaryImageAspectRatio = primaryImageAspectRatio;
|
||||||
|
|
||||||
if (!options.width && options.widths) {
|
if (!options.width && options.widths) {
|
||||||
|
@ -460,30 +255,6 @@ function buildCardsHtmlInternal(items, options) {
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes the aspect ratio for a card given its shape.
|
|
||||||
* @param {string} shape - Shape for which to get the aspect ratio.
|
|
||||||
* @returns {null|number} Ratio of the shape.
|
|
||||||
*/
|
|
||||||
function getDesiredAspect(shape) {
|
|
||||||
if (shape) {
|
|
||||||
shape = shape.toLowerCase();
|
|
||||||
if (shape.indexOf('portrait') !== -1) {
|
|
||||||
return (2 / 3);
|
|
||||||
}
|
|
||||||
if (shape.indexOf('backdrop') !== -1) {
|
|
||||||
return (16 / 9);
|
|
||||||
}
|
|
||||||
if (shape.indexOf('square') !== -1) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (shape.indexOf('banner') !== -1) {
|
|
||||||
return (1000 / 185);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} CardImageUrl
|
* @typedef {Object} CardImageUrl
|
||||||
* @property {string} imgUrl - Image URL.
|
* @property {string} imgUrl - Image URL.
|
||||||
|
@ -509,7 +280,7 @@ function getCardImageUrl(item, apiClient, options, shape) {
|
||||||
let imgUrl = null;
|
let imgUrl = null;
|
||||||
let imgTag = null;
|
let imgTag = null;
|
||||||
let coverImage = false;
|
let coverImage = false;
|
||||||
const uiAspect = getDesiredAspect(shape);
|
const uiAspect = cardBuilderUtils.getDesiredAspect(shape);
|
||||||
let imgType = null;
|
let imgType = null;
|
||||||
let itemId = null;
|
let itemId = null;
|
||||||
|
|
||||||
|
|
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"
|
is="emby-linkbutton"
|
||||||
data-role="button"
|
data-role="button"
|
||||||
class="${activeTab === 'useredit' ? 'ui-btn-active' : ''}"
|
class="${activeTab === 'useredit' ? 'ui-btn-active' : ''}"
|
||||||
onclick="Dashboard.navigate('useredit.html', true);">
|
onclick="Dashboard.navigate('/dashboard/users/profile', true);">
|
||||||
${globalize.translate('Profile')}
|
${globalize.translate('Profile')}
|
||||||
</a>
|
</a>
|
||||||
<a href="#"
|
<a href="#"
|
||||||
is="emby-linkbutton"
|
is="emby-linkbutton"
|
||||||
data-role="button"
|
data-role="button"
|
||||||
class="${activeTab === 'userlibraryaccess' ? 'ui-btn-active' : ''}"
|
class="${activeTab === 'userlibraryaccess' ? 'ui-btn-active' : ''}"
|
||||||
onclick="Dashboard.navigate('userlibraryaccess.html', true);">
|
onclick="Dashboard.navigate('/dashboard/users/access', true);">
|
||||||
${globalize.translate('TabAccess')}
|
${globalize.translate('TabAccess')}
|
||||||
</a>
|
</a>
|
||||||
<a href="#"
|
<a href="#"
|
||||||
is="emby-linkbutton"
|
is="emby-linkbutton"
|
||||||
data-role="button"
|
data-role="button"
|
||||||
class="${activeTab === 'userparentalcontrol' ? 'ui-btn-active' : ''}"
|
class="${activeTab === 'userparentalcontrol' ? 'ui-btn-active' : ''}"
|
||||||
onclick="Dashboard.navigate('userparentalcontrol.html', true);">
|
onclick="Dashboard.navigate('/dashboard/users/parentalcontrol', true);">
|
||||||
${globalize.translate('TabParentalControl')}
|
${globalize.translate('TabParentalControl')}
|
||||||
</a>
|
</a>
|
||||||
<a href="#"
|
<a href="#"
|
||||||
is="emby-linkbutton"
|
is="emby-linkbutton"
|
||||||
data-role="button"
|
data-role="button"
|
||||||
class="${activeTab === 'userpassword' ? 'ui-btn-active' : ''}"
|
class="${activeTab === 'userpassword' ? 'ui-btn-active' : ''}"
|
||||||
onclick="Dashboard.navigate('userpassword.html', true);">
|
onclick="Dashboard.navigate('/dashboard/users/password', true);">
|
||||||
${globalize.translate('HeaderPassword')}
|
${globalize.translate('HeaderPassword')}
|
||||||
</a>`
|
</a>`
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,7 +11,7 @@ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl
|
||||||
__html: `<a
|
__html: `<a
|
||||||
is="emby-linkbutton"
|
is="emby-linkbutton"
|
||||||
class="cardContent"
|
class="cardContent"
|
||||||
href="#/useredit.html?userId=${user.Id}"
|
href="#/dashboard/users/profile?userId=${user.Id}"
|
||||||
>
|
>
|
||||||
${renderImgUrl}
|
${renderImgUrl}
|
||||||
</a>`
|
</a>`
|
||||||
|
|
|
@ -36,7 +36,7 @@ function loadScreensavers(context, userSettings) {
|
||||||
const selectScreensaver = context.querySelector('.selectScreensaver');
|
const selectScreensaver = context.querySelector('.selectScreensaver');
|
||||||
const options = pluginManager.ofType(PluginType.Screensaver).map(plugin => {
|
const options = pluginManager.ofType(PluginType.Screensaver).map(plugin => {
|
||||||
return {
|
return {
|
||||||
name: plugin.name,
|
name: globalize.translate(plugin.name),
|
||||||
value: plugin.id
|
value: plugin.id
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,50 +1,42 @@
|
||||||
import loading from './loading/loading';
|
import dom from 'scripts/dom';
|
||||||
import cardBuilder from './cardbuilder/cardBuilder';
|
import globalize from 'scripts/globalize';
|
||||||
import dom from '../scripts/dom';
|
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
|
||||||
|
import { getParameterByName } from 'utils/url';
|
||||||
|
|
||||||
import { appHost } from './apphost';
|
import { appHost } from './apphost';
|
||||||
|
import cardBuilder from './cardbuilder/cardBuilder';
|
||||||
import imageLoader from './images/imageLoader';
|
import imageLoader from './images/imageLoader';
|
||||||
import globalize from '../scripts/globalize';
|
|
||||||
import layoutManager from './layoutManager';
|
import layoutManager from './layoutManager';
|
||||||
import { getParameterByName } from '../utils/url.ts';
|
import loading from './loading/loading';
|
||||||
import '../styles/scrollstyles.scss';
|
|
||||||
import '../elements/emby-itemscontainer/emby-itemscontainer';
|
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
|
||||||
|
import 'styles/scrollstyles.scss';
|
||||||
|
|
||||||
function enableScrollX() {
|
function enableScrollX() {
|
||||||
return !layoutManager.desktop;
|
return !layoutManager.desktop;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getThumbShape() {
|
|
||||||
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPosterShape() {
|
|
||||||
return enableScrollX() ? 'overflowPortrait' : 'portrait';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSquareShape() {
|
|
||||||
return enableScrollX() ? 'overflowSquare' : 'square';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSections() {
|
function getSections() {
|
||||||
return [{
|
return [{
|
||||||
name: 'Movies',
|
name: 'Movies',
|
||||||
types: 'Movie',
|
types: 'Movie',
|
||||||
id: 'favoriteMovies',
|
id: 'favoriteMovies',
|
||||||
shape: getPosterShape(),
|
shape: getPortraitShape(enableScrollX()),
|
||||||
showTitle: false,
|
showTitle: false,
|
||||||
overlayPlayButton: true
|
overlayPlayButton: true
|
||||||
}, {
|
}, {
|
||||||
name: 'Shows',
|
name: 'Shows',
|
||||||
types: 'Series',
|
types: 'Series',
|
||||||
id: 'favoriteShows',
|
id: 'favoriteShows',
|
||||||
shape: getPosterShape(),
|
shape: getPortraitShape(enableScrollX()),
|
||||||
showTitle: false,
|
showTitle: false,
|
||||||
overlayPlayButton: true
|
overlayPlayButton: true
|
||||||
}, {
|
}, {
|
||||||
name: 'Episodes',
|
name: 'Episodes',
|
||||||
types: 'Episode',
|
types: 'Episode',
|
||||||
id: 'favoriteEpisode',
|
id: 'favoriteEpisode',
|
||||||
shape: getThumbShape(),
|
shape: getBackdropShape(enableScrollX()),
|
||||||
preferThumb: false,
|
preferThumb: false,
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
showParentTitle: true,
|
showParentTitle: true,
|
||||||
|
@ -55,7 +47,7 @@ function getSections() {
|
||||||
name: 'Videos',
|
name: 'Videos',
|
||||||
types: 'Video,MusicVideo',
|
types: 'Video,MusicVideo',
|
||||||
id: 'favoriteVideos',
|
id: 'favoriteVideos',
|
||||||
shape: getThumbShape(),
|
shape: getBackdropShape(enableScrollX()),
|
||||||
preferThumb: true,
|
preferThumb: true,
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
overlayPlayButton: true,
|
overlayPlayButton: true,
|
||||||
|
@ -65,7 +57,7 @@ function getSections() {
|
||||||
name: 'Artists',
|
name: 'Artists',
|
||||||
types: 'MusicArtist',
|
types: 'MusicArtist',
|
||||||
id: 'favoriteArtists',
|
id: 'favoriteArtists',
|
||||||
shape: getSquareShape(),
|
shape: getSquareShape(enableScrollX()),
|
||||||
preferThumb: false,
|
preferThumb: false,
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
overlayText: false,
|
overlayText: false,
|
||||||
|
@ -77,7 +69,7 @@ function getSections() {
|
||||||
name: 'Albums',
|
name: 'Albums',
|
||||||
types: 'MusicAlbum',
|
types: 'MusicAlbum',
|
||||||
id: 'favoriteAlbums',
|
id: 'favoriteAlbums',
|
||||||
shape: getSquareShape(),
|
shape: getSquareShape(enableScrollX()),
|
||||||
preferThumb: false,
|
preferThumb: false,
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
overlayText: false,
|
overlayText: false,
|
||||||
|
@ -89,7 +81,7 @@ function getSections() {
|
||||||
name: 'Songs',
|
name: 'Songs',
|
||||||
types: 'Audio',
|
types: 'Audio',
|
||||||
id: 'favoriteSongs',
|
id: 'favoriteSongs',
|
||||||
shape: getSquareShape(),
|
shape: getSquareShape(enableScrollX()),
|
||||||
preferThumb: false,
|
preferThumb: false,
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
overlayText: false,
|
overlayText: false,
|
||||||
|
|
|
@ -1,18 +1,23 @@
|
||||||
import escapeHtml from 'escape-html';
|
import escapeHtml from 'escape-html';
|
||||||
|
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import imageHelper from 'scripts/imagehelper';
|
||||||
|
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
|
||||||
|
import Dashboard from 'utils/dashboard';
|
||||||
|
|
||||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||||
import layoutManager from '../layoutManager';
|
|
||||||
import imageLoader from '../images/imageLoader';
|
import imageLoader from '../images/imageLoader';
|
||||||
import globalize from '../../scripts/globalize';
|
import layoutManager from '../layoutManager';
|
||||||
import { appRouter } from '../router/appRouter';
|
import { appRouter } from '../router/appRouter';
|
||||||
import imageHelper from '../../scripts/imagehelper';
|
|
||||||
import '../../elements/emby-button/paper-icon-button-light';
|
|
||||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
|
||||||
import '../../elements/emby-scroller/emby-scroller';
|
|
||||||
import '../../elements/emby-button/emby-button';
|
|
||||||
import './homesections.scss';
|
|
||||||
import Dashboard from '../../utils/dashboard';
|
|
||||||
import ServerConnections from '../ServerConnections';
|
import ServerConnections from '../ServerConnections';
|
||||||
|
|
||||||
|
import 'elements/emby-button/paper-icon-button-light';
|
||||||
|
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
import 'elements/emby-scroller/emby-scroller';
|
||||||
|
import 'elements/emby-button/emby-button';
|
||||||
|
|
||||||
|
import './homesections.scss';
|
||||||
|
|
||||||
export function getDefaultSection(index) {
|
export function getDefaultSection(index) {
|
||||||
switch (index) {
|
switch (index) {
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -69,7 +74,10 @@ export function loadSections(elem, apiClient, user, userSettings) {
|
||||||
promises.push(loadSection(elem, apiClient, user, userSettings, userViews, sections, i));
|
promises.push(loadSection(elem, apiClient, user, userSettings, userViews, sections, i));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(promises).then(function () {
|
return Promise.all(promises)
|
||||||
|
// Timeout for polyfilled CustomElements (webOS 1.2)
|
||||||
|
.then(() => new Promise((resolve) => setTimeout(resolve, 0)))
|
||||||
|
.then(() => {
|
||||||
return resume(elem, {
|
return resume(elem, {
|
||||||
refresh: true
|
refresh: true
|
||||||
});
|
});
|
||||||
|
@ -91,7 +99,7 @@ export function loadSections(elem, apiClient, user, userSettings) {
|
||||||
const createNowLink = elem.querySelector('#button-createLibrary');
|
const createNowLink = elem.querySelector('#button-createLibrary');
|
||||||
if (createNowLink) {
|
if (createNowLink) {
|
||||||
createNowLink.addEventListener('click', function () {
|
createNowLink.addEventListener('click', function () {
|
||||||
Dashboard.navigate('library.html');
|
Dashboard.navigate('dashboard/libraries');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -166,18 +174,6 @@ function enableScrollX() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSquareShape() {
|
|
||||||
return enableScrollX() ? 'overflowSquare' : 'square';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThumbShape() {
|
|
||||||
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPortraitShape() {
|
|
||||||
return enableScrollX() ? 'overflowPortrait' : 'portrait';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLibraryButtonsHtml(items) {
|
function getLibraryButtonsHtml(items) {
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
|
@ -241,11 +237,11 @@ function getLatestItemsHtmlFn(itemType, viewType) {
|
||||||
const cardLayout = false;
|
const cardLayout = false;
|
||||||
let shape;
|
let shape;
|
||||||
if (itemType === 'Channel' || viewType === 'movies' || viewType === 'books' || viewType === 'tvshows') {
|
if (itemType === 'Channel' || viewType === 'movies' || viewType === 'books' || viewType === 'tvshows') {
|
||||||
shape = getPortraitShape();
|
shape = getPortraitShape(enableScrollX());
|
||||||
} else if (viewType === 'music' || viewType === 'homevideos') {
|
} else if (viewType === 'music' || viewType === 'homevideos') {
|
||||||
shape = getSquareShape();
|
shape = getSquareShape(enableScrollX());
|
||||||
} else {
|
} else {
|
||||||
shape = getThumbShape();
|
shape = getBackdropShape(enableScrollX());
|
||||||
}
|
}
|
||||||
|
|
||||||
return cardBuilder.getCardsHtml({
|
return cardBuilder.getCardsHtml({
|
||||||
|
@ -342,7 +338,7 @@ export function loadLibraryTiles(elem, apiClient, user, userSettings, shape, use
|
||||||
|
|
||||||
html += cardBuilder.getCardsHtml({
|
html += cardBuilder.getCardsHtml({
|
||||||
items: userViews,
|
items: userViews,
|
||||||
shape: getThumbShape(),
|
shape: getBackdropShape(enableScrollX()),
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
centerText: true,
|
centerText: true,
|
||||||
overlayText: false,
|
overlayText: false,
|
||||||
|
@ -420,7 +416,9 @@ function getItemsToResumeHtmlFn(useEpisodeImages, mediaType) {
|
||||||
items: items,
|
items: items,
|
||||||
preferThumb: true,
|
preferThumb: true,
|
||||||
inheritThumb: !useEpisodeImages,
|
inheritThumb: !useEpisodeImages,
|
||||||
shape: (mediaType === 'Book') ? getPortraitShape() : getThumbShape(),
|
shape: (mediaType === 'Book') ?
|
||||||
|
getPortraitShape(enableScrollX()) :
|
||||||
|
getBackdropShape(enableScrollX()),
|
||||||
overlayText: false,
|
overlayText: false,
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
showParentTitle: true,
|
showParentTitle: true,
|
||||||
|
@ -468,7 +466,7 @@ function getOnNowItemsHtml(items) {
|
||||||
showChannelName: false,
|
showChannelName: false,
|
||||||
showAirDateTime: false,
|
showAirDateTime: false,
|
||||||
showAirEndTime: true,
|
showAirEndTime: true,
|
||||||
defaultShape: getThumbShape(),
|
defaultShape: getBackdropShape(enableScrollX()),
|
||||||
lines: 3,
|
lines: 3,
|
||||||
overlayPlayButton: true
|
overlayPlayButton: true
|
||||||
});
|
});
|
||||||
|
@ -611,7 +609,7 @@ function getNextUpItemsHtmlFn(useEpisodeImages) {
|
||||||
items: items,
|
items: items,
|
||||||
preferThumb: true,
|
preferThumb: true,
|
||||||
inheritThumb: !useEpisodeImages,
|
inheritThumb: !useEpisodeImages,
|
||||||
shape: getThumbShape(),
|
shape: getBackdropShape(enableScrollX()),
|
||||||
overlayText: false,
|
overlayText: false,
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
showParentTitle: true,
|
showParentTitle: true,
|
||||||
|
|
|
@ -445,7 +445,7 @@ function executeCommand(item, id, options) {
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'multiSelect':
|
case 'multiSelect':
|
||||||
import('./multiSelect/multiSelect').then(({ startMultiSelect: startMultiSelect }) => {
|
import('./multiSelect/multiSelect').then(({ startMultiSelect }) => {
|
||||||
const card = dom.parentWithClass(options.positionTo, 'card');
|
const card = dom.parentWithClass(options.positionTo, 'card');
|
||||||
startMultiSelect(card);
|
startMultiSelect(card);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import loadable from '@loadable/component';
|
import loadable, { LoadableComponent } from '@loadable/component';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Route } from 'react-router-dom';
|
import { Route } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -10,13 +10,18 @@ export enum AsyncRouteType {
|
||||||
export interface AsyncRoute {
|
export interface AsyncRoute {
|
||||||
/** The URL path for this route. */
|
/** The URL path for this route. */
|
||||||
path: string
|
path: string
|
||||||
/** The relative path to the page component in the routes directory. */
|
/**
|
||||||
page: string
|
* The relative path to the page component in the routes directory.
|
||||||
/** The route should use the page component from the experimental app. */
|
* Will fallback to using the `path` value if not specified.
|
||||||
|
*/
|
||||||
|
page?: string
|
||||||
|
/** The page element to render. */
|
||||||
|
element?: LoadableComponent<AsyncPageProps>
|
||||||
|
/** The page type used to load the correct page element. */
|
||||||
type?: AsyncRouteType
|
type?: AsyncRouteType
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AsyncPageProps {
|
export interface AsyncPageProps {
|
||||||
/** The relative path to the page component in the routes directory. */
|
/** The relative path to the page component in the routes directory. */
|
||||||
page: string
|
page: string
|
||||||
}
|
}
|
||||||
|
@ -31,14 +36,19 @@ const StableAsyncPage = loadable(
|
||||||
{ cacheKey: (props: AsyncPageProps) => props.page }
|
{ cacheKey: (props: AsyncPageProps) => props.page }
|
||||||
);
|
);
|
||||||
|
|
||||||
export const toAsyncPageRoute = ({ path, page, type = AsyncRouteType.Stable }: AsyncRoute) => (
|
export const toAsyncPageRoute = ({ path, page, element, type = AsyncRouteType.Stable }: AsyncRoute) => {
|
||||||
|
const Element = element
|
||||||
|
|| (
|
||||||
|
type === AsyncRouteType.Experimental ?
|
||||||
|
ExperimentalAsyncPage :
|
||||||
|
StableAsyncPage
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
<Route
|
<Route
|
||||||
key={path}
|
key={path}
|
||||||
path={path}
|
path={path}
|
||||||
element={(
|
element={<Element page={page ?? path} />}
|
||||||
type === AsyncRouteType.Experimental ?
|
|
||||||
<ExperimentalAsyncPage page={page} /> :
|
|
||||||
<StableAsyncPage page={page} />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
28
src/components/router/Redirect.tsx
Normal file
28
src/components/router/Redirect.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Navigate, Route, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
export interface Redirect {
|
||||||
|
from: string
|
||||||
|
to: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const RedirectWithSearch = ({ to }: { to: string }) => {
|
||||||
|
const { search } = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Navigate
|
||||||
|
replace
|
||||||
|
to={`${to}${search}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function toRedirectRoute({ from, to }: Redirect) {
|
||||||
|
return (
|
||||||
|
<Route
|
||||||
|
key={from}
|
||||||
|
path={from}
|
||||||
|
element={<RedirectWithSearch to={to} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -527,7 +527,7 @@ class AppRouter {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item === 'manageserver') {
|
if (item === 'manageserver') {
|
||||||
return '#/dashboard.html';
|
return '#/dashboard';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item === 'recordedtv') {
|
if (item === 'recordedtv') {
|
||||||
|
|
|
@ -46,15 +46,8 @@ function init(instance) {
|
||||||
if (inputOffset) {
|
if (inputOffset) {
|
||||||
inputOffset = inputOffset[0];
|
inputOffset = inputOffset[0];
|
||||||
inputOffset = parseFloat(inputOffset);
|
inputOffset = parseFloat(inputOffset);
|
||||||
inputOffset = Math.min(30, Math.max(-30, inputOffset));
|
|
||||||
|
|
||||||
// replace current text by considered offset
|
subtitleSyncSlider.updateOffset(inputOffset);
|
||||||
this.textContent = inputOffset + 's';
|
|
||||||
// set new offset
|
|
||||||
playbackManager.setSubtitleOffset(inputOffset, player);
|
|
||||||
// synchronize with slider value
|
|
||||||
subtitleSyncSlider.updateOffset(
|
|
||||||
getSliderValueFromOffset(inputOffset));
|
|
||||||
} else {
|
} else {
|
||||||
this.textContent = (playbackManager.getPlayerSubtitleOffset(player) || 0) + 's';
|
this.textContent = (playbackManager.getPlayerSubtitleOffset(player) || 0) + 's';
|
||||||
}
|
}
|
||||||
|
@ -79,23 +72,26 @@ function init(instance) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function updateSubtitleOffset() {
|
||||||
|
const value = parseFloat(subtitleSyncSlider.value);
|
||||||
|
// set new offset
|
||||||
|
playbackManager.setSubtitleOffset(value, player);
|
||||||
|
// synchronize with textField value
|
||||||
|
subtitleSyncTextField.updateOffset(value);
|
||||||
|
}
|
||||||
|
|
||||||
subtitleSyncSlider.updateOffset = function (sliderValue) {
|
subtitleSyncSlider.updateOffset = function (sliderValue) {
|
||||||
// default value is 0s = 0ms
|
// default value is 0s = 0ms
|
||||||
this.value = sliderValue === undefined ? 0 : sliderValue;
|
this.value = sliderValue === undefined ? 0 : sliderValue;
|
||||||
|
|
||||||
|
updateSubtitleOffset();
|
||||||
};
|
};
|
||||||
|
|
||||||
subtitleSyncSlider.addEventListener('change', function () {
|
subtitleSyncSlider.addEventListener('change', () => updateSubtitleOffset());
|
||||||
// set new offset
|
|
||||||
playbackManager.setSubtitleOffset(getOffsetFromSliderValue(this.value), player);
|
|
||||||
// synchronize with textField value
|
|
||||||
subtitleSyncTextField.updateOffset(
|
|
||||||
getOffsetFromSliderValue(this.value));
|
|
||||||
});
|
|
||||||
|
|
||||||
subtitleSyncSlider.getBubbleHtml = function (value) {
|
subtitleSyncSlider.getBubbleHtml = function (_, value) {
|
||||||
const newOffset = getOffsetFromPercentage(value);
|
|
||||||
return '<h1 class="sliderBubbleText">'
|
return '<h1 class="sliderBubbleText">'
|
||||||
+ (newOffset > 0 ? '+' : '') + parseFloat(newOffset) + 's'
|
+ (value > 0 ? '+' : '') + parseFloat(value) + 's'
|
||||||
+ '</h1>';
|
+ '</h1>';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -107,25 +103,6 @@ function init(instance) {
|
||||||
instance.element = parent;
|
instance.element = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOffsetFromPercentage(value) {
|
|
||||||
// convert percentage to fraction
|
|
||||||
let offset = (value - 50) / 50;
|
|
||||||
// multiply by offset min/max range value (-x to +x) :
|
|
||||||
offset *= 30;
|
|
||||||
return offset.toFixed(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOffsetFromSliderValue(value) {
|
|
||||||
// convert slider value to offset
|
|
||||||
const offset = value / 10;
|
|
||||||
return offset.toFixed(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSliderValueFromOffset(value) {
|
|
||||||
const sliderValue = value * 10;
|
|
||||||
return Math.min(300, Math.max(-300, sliderValue.toFixed(1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
class SubtitleSync {
|
class SubtitleSync {
|
||||||
constructor(currentPlayer) {
|
constructor(currentPlayer) {
|
||||||
player = currentPlayer;
|
player = currentPlayer;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<button type="button" is="paper-icon-button-light" class="subtitleSync-closeButton"><span class="material-icons close" aria-hidden="true"></span></button>
|
<button type="button" is="paper-icon-button-light" class="subtitleSync-closeButton"><span class="material-icons close" aria-hidden="true"></span></button>
|
||||||
<div class="subtitleSyncTextField" contenteditable="true" spellcheck="false">0s</div>
|
<div class="subtitleSyncTextField" contenteditable="true" spellcheck="false">0s</div>
|
||||||
<div class="sliderContainer subtitleSyncSliderContainer">
|
<div class="sliderContainer subtitleSyncSliderContainer">
|
||||||
<input is="emby-slider" type="range" step="1" min="-300" max="300" value="0" class="subtitleSyncSlider" data-slider-keep-progress="true" />
|
<input is="emby-slider" type="range" step="0.1" min="-30" max="30" value="0" class="subtitleSyncSlider" data-slider-keep-progress="true" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import escapeHtml from 'escape-html';
|
import escapeHtml from 'escape-html';
|
||||||
|
|
||||||
|
import { getSubtitleApi } from '@jellyfin/sdk/lib/utils/api/subtitle-api';
|
||||||
|
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||||
import dialogHelper from '../../components/dialogHelper/dialogHelper';
|
import dialogHelper from '../../components/dialogHelper/dialogHelper';
|
||||||
import ServerConnections from '../ServerConnections';
|
import ServerConnections from '../ServerConnections';
|
||||||
import dom from '../../scripts/dom';
|
import dom from '../../scripts/dom';
|
||||||
|
@ -13,6 +15,7 @@ import '../../elements/emby-button/emby-button';
|
||||||
import '../../elements/emby-select/emby-select';
|
import '../../elements/emby-select/emby-select';
|
||||||
import '../formdialog.scss';
|
import '../formdialog.scss';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
import { readFileAsBase64 } from 'utils/file';
|
||||||
|
|
||||||
let currentItemId;
|
let currentItemId;
|
||||||
let currentServerId;
|
let currentServerId;
|
||||||
|
@ -75,7 +78,7 @@ function setFiles(page, files) {
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSubmit(e) {
|
async function onSubmit(e) {
|
||||||
const file = currentFile;
|
const file = currentFile;
|
||||||
|
|
||||||
if (!isValidSubtitleFile(file)) {
|
if (!isValidSubtitleFile(file)) {
|
||||||
|
@ -89,8 +92,17 @@ function onSubmit(e) {
|
||||||
const dlg = dom.parentWithClass(this, 'dialog');
|
const dlg = dom.parentWithClass(this, 'dialog');
|
||||||
const language = dlg.querySelector('#selectLanguage').value;
|
const language = dlg.querySelector('#selectLanguage').value;
|
||||||
const isForced = dlg.querySelector('#chkIsForced').checked;
|
const isForced = dlg.querySelector('#chkIsForced').checked;
|
||||||
|
const isHearingImpaired = dlg.querySelector('#chkIsHearingImpaired').checked;
|
||||||
|
|
||||||
ServerConnections.getApiClient(currentServerId).uploadItemSubtitle(currentItemId, language, isForced, file).then(function () {
|
const subtitleApi = getSubtitleApi(toApi(ServerConnections.getApiClient(currentServerId)));
|
||||||
|
|
||||||
|
const data = await readFileAsBase64(file);
|
||||||
|
const format = file.name.substring(file.name.lastIndexOf('.') + 1).toLowerCase();
|
||||||
|
|
||||||
|
subtitleApi.uploadSubtitle({
|
||||||
|
itemId: currentItemId,
|
||||||
|
uploadSubtitleDto: { Data: data, Language: language, IsForced: isForced, Format: format, IsHearingImpaired: isHearingImpaired }
|
||||||
|
}).then(function () {
|
||||||
dlg.querySelector('#uploadSubtitle').value = '';
|
dlg.querySelector('#uploadSubtitle').value = '';
|
||||||
loading.hide();
|
loading.hide();
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
|
|
|
@ -31,6 +31,10 @@
|
||||||
<input type="checkbox" is="emby-checkbox" id="chkIsForced" />
|
<input type="checkbox" is="emby-checkbox" id="chkIsForced" />
|
||||||
<span>${LabelIsForced}</span>
|
<span>${LabelIsForced}</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" is="emby-checkbox" id="chkIsHearingImpaired" />
|
||||||
|
<span>${LabelIsHearingImpaired}</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="selectContainer flex-grow">
|
<div class="selectContainer flex-grow">
|
||||||
<select is="emby-select" id="selectLanguage" required="required" label="${LabelLanguage}"></select>
|
<select is="emby-select" id="selectLanguage" required="required" label="${LabelLanguage}"></select>
|
||||||
|
|
129
src/components/toolbar/AppToolbar.tsx
Normal file
129
src/components/toolbar/AppToolbar.tsx
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import ArrowBack from '@mui/icons-material/ArrowBack';
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import React, { FC, ReactNode } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import appIcon from 'assets/img/icon-transparent.png';
|
||||||
|
import { appRouter } from 'components/router/appRouter';
|
||||||
|
import { useApi } from 'hooks/useApi';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
|
import UserMenuButton from './UserMenuButton';
|
||||||
|
|
||||||
|
interface AppToolbarProps {
|
||||||
|
buttons?: ReactNode
|
||||||
|
isDrawerAvailable: boolean
|
||||||
|
isDrawerOpen: boolean
|
||||||
|
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBackButtonClick = () => {
|
||||||
|
appRouter.back()
|
||||||
|
.catch(err => {
|
||||||
|
console.error('[AppToolbar] error calling appRouter.back', err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppToolbar: FC<AppToolbarProps> = ({
|
||||||
|
buttons,
|
||||||
|
children,
|
||||||
|
isDrawerAvailable,
|
||||||
|
isDrawerOpen,
|
||||||
|
onDrawerButtonClick
|
||||||
|
}) => {
|
||||||
|
const { user } = useApi();
|
||||||
|
const isUserLoggedIn = Boolean(user);
|
||||||
|
|
||||||
|
const isBackButtonAvailable = appRouter.canGoBack();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Toolbar
|
||||||
|
variant='dense'
|
||||||
|
sx={{
|
||||||
|
flexWrap: {
|
||||||
|
xs: 'wrap',
|
||||||
|
lg: 'nowrap'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isUserLoggedIn && isDrawerAvailable && (
|
||||||
|
<Tooltip title={globalize.translate(isDrawerOpen ? 'MenuClose' : 'MenuOpen')}>
|
||||||
|
<IconButton
|
||||||
|
size='large'
|
||||||
|
edge='start'
|
||||||
|
color='inherit'
|
||||||
|
aria-label={globalize.translate(isDrawerOpen ? 'MenuClose' : 'MenuOpen')}
|
||||||
|
onClick={onDrawerButtonClick}
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isBackButtonAvailable && (
|
||||||
|
<Tooltip title={globalize.translate('ButtonBack')}>
|
||||||
|
<IconButton
|
||||||
|
size='large'
|
||||||
|
// Set the edge if the drawer button is not shown
|
||||||
|
edge={!(isUserLoggedIn && isDrawerAvailable) ? 'start' : undefined}
|
||||||
|
color='inherit'
|
||||||
|
aria-label={globalize.translate('ButtonBack')}
|
||||||
|
onClick={onBackButtonClick}
|
||||||
|
>
|
||||||
|
<ArrowBack />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
component={Link}
|
||||||
|
to='/'
|
||||||
|
color='inherit'
|
||||||
|
aria-label={globalize.translate('Home')}
|
||||||
|
sx={{
|
||||||
|
ml: 2,
|
||||||
|
display: 'inline-flex',
|
||||||
|
textDecoration: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component='img'
|
||||||
|
src={appIcon}
|
||||||
|
sx={{
|
||||||
|
height: '2rem',
|
||||||
|
marginInlineEnd: 1
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
variant='h6'
|
||||||
|
noWrap
|
||||||
|
component='div'
|
||||||
|
sx={{ display: { xs: 'none', sm: 'inline-block' } }}
|
||||||
|
>
|
||||||
|
Jellyfin
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{isUserLoggedIn && (
|
||||||
|
<>
|
||||||
|
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'flex-end' }}>
|
||||||
|
{buttons}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ flexGrow: 0 }}>
|
||||||
|
<UserMenuButton />
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Toolbar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppToolbar;
|
|
@ -115,7 +115,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key='admin-dashboard-link'
|
key='admin-dashboard-link'
|
||||||
component={Link}
|
component={Link}
|
||||||
to='/dashboard.html'
|
to='/dashboard'
|
||||||
onClick={onMenuClose}
|
onClick={onMenuClose}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key='admin-metadata-link'
|
key='admin-metadata-link'
|
||||||
component={Link}
|
component={Link}
|
||||||
to='/edititemmetadata.html'
|
to='/metadata'
|
||||||
onClick={onMenuClose}
|
onClick={onMenuClose}
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
|
@ -140,7 +140,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
|
||||||
<Divider />
|
<Divider />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
component={Link}
|
component={Link}
|
||||||
to='/mypreferencesquickconnect.html'
|
to='/quickconnect'
|
||||||
onClick={onMenuClose}
|
onClick={onMenuClose}
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
|
@ -2,11 +2,11 @@ import IconButton from '@mui/material/IconButton';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import UserAvatar from 'components/UserAvatar';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import AppUserMenu, { ID } from './menus/AppUserMenu';
|
import AppUserMenu, { ID } from './AppUserMenu';
|
||||||
import UserAvatar from 'components/UserAvatar';
|
|
||||||
|
|
||||||
const UserMenuButton = () => {
|
const UserMenuButton = () => {
|
||||||
const { user } = useApi();
|
const { user } = useApi();
|
|
@ -3,11 +3,7 @@ import './viewManager/viewContainer.scss';
|
||||||
import Dashboard from '../utils/dashboard';
|
import Dashboard from '../utils/dashboard';
|
||||||
|
|
||||||
const getMainAnimatedPages = () => {
|
const getMainAnimatedPages = () => {
|
||||||
if (!mainAnimatedPages) {
|
return document.querySelector('.mainAnimatedPages');
|
||||||
mainAnimatedPages = document.querySelector('.mainAnimatedPages');
|
|
||||||
}
|
|
||||||
|
|
||||||
return mainAnimatedPages;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function setControllerClass(view, options) {
|
function setControllerClass(view, options) {
|
||||||
|
@ -61,7 +57,9 @@ export function loadView(options) {
|
||||||
|
|
||||||
view.classList.add('mainAnimatedPage');
|
view.classList.add('mainAnimatedPage');
|
||||||
|
|
||||||
if (!getMainAnimatedPages()) {
|
const mainAnimatedPages = getMainAnimatedPages();
|
||||||
|
|
||||||
|
if (!mainAnimatedPages) {
|
||||||
console.warn('[viewContainer] main animated pages element is not present');
|
console.warn('[viewContainer] main animated pages element is not present');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -187,6 +185,7 @@ export function setOnBeforeChange(fn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tryRestoreView(options) {
|
export function tryRestoreView(options) {
|
||||||
|
console.debug('[viewContainer] tryRestoreView', options);
|
||||||
const url = options.url;
|
const url = options.url;
|
||||||
const index = currentUrls.indexOf(url);
|
const index = currentUrls.indexOf(url);
|
||||||
|
|
||||||
|
@ -232,14 +231,15 @@ function triggerDestroy(view) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reset() {
|
export function reset() {
|
||||||
|
console.debug('[viewContainer] resetting view cache');
|
||||||
allPages = [];
|
allPages = [];
|
||||||
currentUrls = [];
|
currentUrls = [];
|
||||||
|
const mainAnimatedPages = getMainAnimatedPages();
|
||||||
if (mainAnimatedPages) mainAnimatedPages.innerHTML = '';
|
if (mainAnimatedPages) mainAnimatedPages.innerHTML = '';
|
||||||
selectedPageIndex = -1;
|
selectedPageIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let onBeforeChange;
|
let onBeforeChange;
|
||||||
let mainAnimatedPages;
|
|
||||||
let allPages = [];
|
let allPages = [];
|
||||||
let currentUrls = [];
|
let currentUrls = [];
|
||||||
const pageContainerCount = 3;
|
const pageContainerCount = 3;
|
||||||
|
@ -248,8 +248,8 @@ reset();
|
||||||
getMainAnimatedPages()?.classList.remove('hide');
|
getMainAnimatedPages()?.classList.remove('hide');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
loadView: loadView,
|
loadView,
|
||||||
tryRestoreView: tryRestoreView,
|
tryRestoreView,
|
||||||
reset: reset,
|
reset,
|
||||||
setOnBeforeChange: setOnBeforeChange
|
setOnBeforeChange
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="dashboardSections" style="padding-top:.5em;">
|
<div class="dashboardSections" style="padding-top:.5em;">
|
||||||
<div class="dashboardColumn dashboardColumn-2-60 dashboardColumn-3-46">
|
<div class="dashboardColumn dashboardColumn-2-60 dashboardColumn-3-46">
|
||||||
<div class="dashboardSection">
|
<div class="dashboardSection">
|
||||||
<a is="emby-linkbutton" href="#/dashboardgeneral.html" class="button-flat sectionTitleTextButton">
|
<a is="emby-linkbutton" href="#/dashboard/settings" class="button-flat sectionTitleTextButton">
|
||||||
<h3>${TabServer}</h3>
|
<h3>${TabServer}</h3>
|
||||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboardSection">
|
<div class="dashboardSection">
|
||||||
<a is="emby-linkbutton" href="#/devices.html" class="button-flat sectionTitleTextButton">
|
<a is="emby-linkbutton" href="#/dashboard/devices" class="button-flat sectionTitleTextButton">
|
||||||
<h3>${HeaderActiveDevices}</h3>
|
<h3>${HeaderActiveDevices}</h3>
|
||||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -70,7 +70,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboardSection">
|
<div class="dashboardSection">
|
||||||
<a is="emby-linkbutton" href="#/dashboardgeneral.html" class="button-flat sectionTitleTextButton">
|
<a is="emby-linkbutton" href="#/dashboard/settings" class="button-flat sectionTitleTextButton">
|
||||||
<h3>${HeaderPaths}</h3>
|
<h3>${HeaderPaths}</h3>
|
||||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -73,7 +73,7 @@ function showDeviceMenu(view, btn, deviceId) {
|
||||||
callback: function (id) {
|
callback: function (id) {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case 'open':
|
case 'open':
|
||||||
Dashboard.navigate('device.html?id=' + deviceId);
|
Dashboard.navigate('dashboard/devices/edit?id=' + deviceId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'delete':
|
case 'delete':
|
||||||
|
@ -94,7 +94,7 @@ function load(page, devices) {
|
||||||
deviceHtml += '<div class="cardBox visualCardBox">';
|
deviceHtml += '<div class="cardBox visualCardBox">';
|
||||||
deviceHtml += '<div class="cardScalable">';
|
deviceHtml += '<div class="cardScalable">';
|
||||||
deviceHtml += '<div class="cardPadder cardPadder-backdrop"></div>';
|
deviceHtml += '<div class="cardPadder cardPadder-backdrop"></div>';
|
||||||
deviceHtml += `<a is="emby-linkbutton" href="#!/device.html?id=${escapeHtml(device.Id)}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`;
|
deviceHtml += `<a is="emby-linkbutton" href="#/dashboard/devices/edit?id=${escapeHtml(device.Id)}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`;
|
||||||
// audit note: getDeviceIcon returns static text
|
// audit note: getDeviceIcon returns static text
|
||||||
const iconUrl = imageHelper.getDeviceIcon(device);
|
const iconUrl = imageHelper.getDeviceIcon(device);
|
||||||
|
|
||||||
|
|
|
@ -264,7 +264,7 @@
|
||||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||||
<span>${Save}</span>
|
<span>${Save}</span>
|
||||||
</button>
|
</button>
|
||||||
<button is="emby-button" type="button" class="button-cancel raised block" onclick="Dashboard.navigate('dlnaprofiles.html');">
|
<button is="emby-button" type="button" class="button-cancel raised block" onclick="Dashboard.navigate('dashboard/dlna/profiles');">
|
||||||
<span>${ButtonCancel}</span>
|
<span>${ButtonCancel}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -639,7 +639,7 @@ function saveProfile(page, profile) {
|
||||||
data: JSON.stringify(profile),
|
data: JSON.stringify(profile),
|
||||||
contentType: 'application/json'
|
contentType: 'application/json'
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
Dashboard.navigate('dlnaprofiles.html');
|
Dashboard.navigate('dashboard/dlna/profiles');
|
||||||
}, Dashboard.processErrorResponse);
|
}, Dashboard.processErrorResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<div class="verticalSection verticalSection-extrabottompadding">
|
<div class="verticalSection verticalSection-extrabottompadding">
|
||||||
<div class="sectionTitleContainer flex align-items-center">
|
<div class="sectionTitleContainer flex align-items-center">
|
||||||
<h2 class="sectionTitle">${HeaderCustomDlnaProfiles}</h2>
|
<h2 class="sectionTitle">${HeaderCustomDlnaProfiles}</h2>
|
||||||
<a is="emby-linkbutton" href="#/dlnaprofile.html" class="fab submit" style="margin:0 0 0 1em">
|
<a is="emby-linkbutton" href="#/dashboard/dlna/profiles/edit" class="fab submit" style="margin:0 0 0 1em">
|
||||||
<span class="material-icons add" aria-hidden="true"></span>
|
<span class="material-icons add" aria-hidden="true"></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
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