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
|
||||
displayName: 'Install Node'
|
||||
inputs:
|
||||
versionSpec: '16.x'
|
||||
versionSpec: '20.x'
|
||||
|
||||
- task: Cache@2
|
||||
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'],
|
||||
'@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
|
||||
'no-unused-private-class-members': ['error'],
|
||||
'no-useless-rename': ['error'],
|
||||
'no-useless-constructor': ['off'],
|
||||
'@typescript-eslint/no-useless-constructor': ['error'],
|
||||
'no-var': ['error'],
|
||||
|
|
8
.github/workflows/lint.yml
vendored
8
.github/workflows/lint.yml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
|||
- name: Setup node environment
|
||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
cache: npm
|
||||
|
||||
|
@ -42,7 +42,7 @@ jobs:
|
|||
- name: Setup node environment
|
||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
cache: npm
|
||||
|
||||
|
@ -63,7 +63,7 @@ jobs:
|
|||
- name: Setup node environment
|
||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
cache: npm
|
||||
|
||||
|
@ -87,7 +87,7 @@ jobs:
|
|||
- name: Setup node environment
|
||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
cache: npm
|
||||
|
||||
|
|
5
.github/workflows/tsc.yml
vendored
5
.github/workflows/tsc.yml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
|||
- name: Setup node environment
|
||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
cache: npm
|
||||
|
||||
|
@ -27,3 +27,6 @@ jobs:
|
|||
|
||||
- name: Run tsc
|
||||
run: npm run build:check
|
||||
|
||||
- name: Run test suite
|
||||
run: npm run test
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
- [sleepycatcoding](https://github.com/sleepycatcoding)
|
||||
- [TheMelmacian](https://github.com/TheMelmacian)
|
||||
- [v0idMrK](https://github.com/v0idMrK)
|
||||
- [tehciolo](https://github.com/tehciolo)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
|
|
@ -77,8 +77,9 @@ Jellyfin Web is the frontend used for most of the clients available for end user
|
|||
.
|
||||
└── src
|
||||
├── apps
|
||||
│ ├── experimental # New experimental app layout
|
||||
│ └── stable # Classic (stable) app layout
|
||||
│ ├── dashboard # Admin dashboard app layout and routes
|
||||
│ ├── experimental # New experimental app layout and routes
|
||||
│ └── stable # Classic (stable) app layout and routes
|
||||
├── assets # Static assets
|
||||
├── components # Higher order visual components and React components
|
||||
├── controllers # Legacy page views and controllers 🧹
|
||||
|
@ -87,7 +88,6 @@ Jellyfin Web is the frontend used for most of the clients available for end user
|
|||
├── legacy # Polyfills for legacy browsers
|
||||
├── libraries # Third party libraries 🧹
|
||||
├── plugins # Client plugins
|
||||
├── routes # React routes/pages
|
||||
├── scripts # Random assortment of visual components and utilities 🐉
|
||||
├── strings # Translation files
|
||||
├── styles # Common app Sass stylesheets
|
||||
|
|
|
@ -13,8 +13,8 @@ ENV IS_DOCKER=YES
|
|||
RUN yum update -y \
|
||||
&& yum install -y epel-release \
|
||||
&& yum install -y rpmdevtools git autoconf automake glibc-devel gcc-c++ make \
|
||||
&& curl -fsSL https://rpm.nodesource.com/setup_16.x | bash - \
|
||||
&& yum install -y nodejs
|
||||
&& yum install https://rpm.nodesource.com/pub_20.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm -y \
|
||||
&& yum install nodejs -y --setopt=nodesource-nodejs.module_hotfixes=1
|
||||
|
||||
# Link to build script
|
||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.centos /build.sh
|
||||
|
|
|
@ -12,11 +12,13 @@ ENV IS_DOCKER=YES
|
|||
|
||||
# Prepare Debian build environment
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y debhelper mmv git curl \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_16.x | bash - \
|
||||
&& apt-get install -y debhelper mmv git curl gnupg ca-certificates \
|
||||
&& 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
|
||||
|
||||
|
||||
# Link to build script
|
||||
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 ARTIFACT_DIR=/jellyfin-web
|
||||
|
|
|
@ -11,7 +11,8 @@ ENV IS_DOCKER=YES
|
|||
|
||||
# Prepare Fedora environment
|
||||
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
|
||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora /build.sh
|
||||
|
|
|
@ -11,8 +11,11 @@ ENV IS_DOCKER=YES
|
|||
|
||||
# Prepare Debian build environment
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y mmv curl git \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_16.x | bash - \
|
||||
&& apt-get install -y mmv curl git gnupg ca-certificates \
|
||||
&& 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
|
||||
|
||||
# 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
|
||||
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)
|
||||
# fallback when not running on Fedora
|
||||
|
|
|
@ -14,10 +14,10 @@ BuildArch: noarch
|
|||
BuildRequires: nodejs
|
||||
%else
|
||||
BuildRequires: git
|
||||
# Nodejs 16 is required and npm >= 8 should bring in NodeJS 16
|
||||
# This requires the build environment to use the nodejs:16 module stream:
|
||||
# dnf module {install|switch-to}:web nodejs:16
|
||||
BuildRequires: npm >= 8
|
||||
# Nodejs 20 is required and npm >= 10 should bring in NodeJS 20
|
||||
# This requires the build environment to use the nodejs:20 module stream:
|
||||
# dnf module {install|switch-to}:web nodejs:20
|
||||
BuildRequires: npm >= 10
|
||||
%endif
|
||||
|
||||
%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",
|
||||
"ts-loader": "9.4.4",
|
||||
"typescript": "5.0.4",
|
||||
"vitest": "0.34.6",
|
||||
"webpack": "5.88.1",
|
||||
"webpack-bundle-analyzer": "4.9.1",
|
||||
"webpack-cli": "5.1.4",
|
||||
|
@ -145,13 +146,15 @@
|
|||
"build:check": "tsc --noEmit",
|
||||
"escheck": "es-check",
|
||||
"lint": "eslint \"./\"",
|
||||
"test": "vitest --watch=false",
|
||||
"test:watch": "vitest",
|
||||
"stylelint": "npm run stylelint:css && npm run stylelint:scss",
|
||||
"stylelint:css": "stylelint \"src/**/*.css\"",
|
||||
"stylelint:scss": "stylelint --config=\".stylelintrc.scss.json\" \"src/**/*.scss\""
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.13.1",
|
||||
"npm": ">=8.1.2",
|
||||
"node": ">=20.0.0",
|
||||
"npm": ">=9.6.4",
|
||||
"yarn": "YARN NO LONGER USED - use npm instead."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,37 +1,62 @@
|
|||
import loadable from '@loadable/component';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { History } from '@remix-run/router';
|
||||
import React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import StableApp from './apps/stable/App';
|
||||
import { HistoryRouter } from './components/router/HistoryRouter';
|
||||
import { ApiProvider } from './hooks/useApi';
|
||||
import { WebConfigProvider } from './hooks/useWebConfig';
|
||||
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
||||
import AppHeader from 'components/AppHeader';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
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 StableApp = loadable(() => import('./apps/stable/App'));
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const RootApp = ({ history }: { history: History }) => {
|
||||
const RootAppLayout = () => {
|
||||
const layoutMode = localStorage.getItem('layout');
|
||||
const isExperimentalLayout = layoutMode === 'experimental';
|
||||
|
||||
const location = useLocation();
|
||||
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
|
||||
.some(path => location.pathname.startsWith(`/${path}`));
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ApiProvider>
|
||||
<WebConfigProvider>
|
||||
<HistoryRouter history={history}>
|
||||
{
|
||||
layoutMode === 'experimental' ?
|
||||
<ExperimentalApp /> :
|
||||
<StableApp />
|
||||
}
|
||||
</HistoryRouter>
|
||||
</WebConfigProvider>
|
||||
</ApiProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
<>
|
||||
<Backdrop />
|
||||
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
|
||||
|
||||
{
|
||||
isExperimentalLayout ?
|
||||
<ExperimentalApp /> :
|
||||
<StableApp />
|
||||
}
|
||||
|
||||
<DashboardApp />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const RootApp = ({ history }: { history: History }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ApiProvider>
|
||||
<WebConfigProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<HistoryRouter history={history}>
|
||||
<RootAppLayout />
|
||||
</HistoryRouter>
|
||||
</ThemeProvider>
|
||||
</WebConfigProvider>
|
||||
</ApiProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
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 { useLocation } from 'react-router-dom';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import ListItemLink from '../ListItemLink';
|
||||
|
||||
const PLUGIN_PATHS = [
|
||||
'/installedplugins.html',
|
||||
'/availableplugins.html',
|
||||
'/repositories.html',
|
||||
'/addplugin.html',
|
||||
'/dashboard/plugins',
|
||||
'/dashboard/plugins/catalog',
|
||||
'/dashboard/plugins/repositories',
|
||||
'/dashboard/plugins/add',
|
||||
'/configurationpage'
|
||||
];
|
||||
|
||||
|
@ -42,7 +41,7 @@ const AdvancedDrawerSection = () => {
|
|||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/networking.html'>
|
||||
<ListItemLink to='/dashboard/networking'>
|
||||
<ListItemIcon>
|
||||
<Lan />
|
||||
</ListItemIcon>
|
||||
|
@ -50,7 +49,7 @@ const AdvancedDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/apikeys.html'>
|
||||
<ListItemLink to='/dashboard/keys'>
|
||||
<ListItemIcon>
|
||||
<VpnKey />
|
||||
</ListItemIcon>
|
||||
|
@ -58,7 +57,7 @@ const AdvancedDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/log.html'>
|
||||
<ListItemLink to='/dashboard/logs'>
|
||||
<ListItemIcon>
|
||||
<Article />
|
||||
</ListItemIcon>
|
||||
|
@ -66,7 +65,7 @@ const AdvancedDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/notificationsettings.html'>
|
||||
<ListItemLink to='/dashboard/notifications'>
|
||||
<ListItemIcon>
|
||||
<EditNotifications />
|
||||
</ListItemIcon>
|
||||
|
@ -74,7 +73,7 @@ const AdvancedDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/installedplugins.html' selected={false}>
|
||||
<ListItemLink to='/dashboard/plugins' selected={false}>
|
||||
<ListItemIcon>
|
||||
<Extension />
|
||||
</ListItemIcon>
|
||||
|
@ -84,19 +83,19 @@ const AdvancedDrawerSection = () => {
|
|||
</ListItem>
|
||||
<Collapse in={isPluginSectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/installedplugins.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/plugins' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabMyPlugins')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/availableplugins.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/plugins/catalog' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabCatalog')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/repositories.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/plugins/repositories' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabRepositories')} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
</Collapse>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/scheduledtasks.html'>
|
||||
<ListItemLink to='/dashboard/tasks'>
|
||||
<ListItemIcon>
|
||||
<Schedule />
|
||||
</ListItemIcon>
|
|
@ -8,13 +8,12 @@ import ListSubheader from '@mui/material/ListSubheader';
|
|||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import ListItemLink from '../ListItemLink';
|
||||
|
||||
const DLNA_PATHS = [
|
||||
'/dlnasettings.html',
|
||||
'/dlnaprofiles.html'
|
||||
'/dashboard/dlna',
|
||||
'/dashboard/dlna/profiles'
|
||||
];
|
||||
|
||||
const DevicesDrawerSection = () => {
|
||||
|
@ -32,7 +31,7 @@ const DevicesDrawerSection = () => {
|
|||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/devices.html'>
|
||||
<ListItemLink to='/dashboard/devices'>
|
||||
<ListItemIcon>
|
||||
<Devices />
|
||||
</ListItemIcon>
|
||||
|
@ -48,7 +47,7 @@ const DevicesDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dlnasettings.html' selected={false}>
|
||||
<ListItemLink to='/dashboard/dlna' selected={false}>
|
||||
<ListItemIcon>
|
||||
<Input />
|
||||
</ListItemIcon>
|
||||
|
@ -58,10 +57,10 @@ const DevicesDrawerSection = () => {
|
|||
</ListItem>
|
||||
<Collapse in={isDlnaSectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/dlnasettings.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/dlna' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Settings')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dlnaprofiles.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/dlna/profiles' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabProfiles')} />
|
||||
</ListItemLink>
|
||||
</List>
|
|
@ -6,10 +6,9 @@ import ListItemText from '@mui/material/ListItemText';
|
|||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React from 'react';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import ListItemLink from '../ListItemLink';
|
||||
|
||||
const LiveTvDrawerSection = () => {
|
||||
return (
|
||||
<List
|
||||
|
@ -21,7 +20,7 @@ const LiveTvDrawerSection = () => {
|
|||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/livetvstatus.html'>
|
||||
<ListItemLink to='/dashboard/livetv'>
|
||||
<ListItemIcon>
|
||||
<LiveTv />
|
||||
</ListItemIcon>
|
||||
|
@ -29,7 +28,7 @@ const LiveTvDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/livetvsettings.html'>
|
||||
<ListItemLink to='/dashboard/recordings'>
|
||||
<ListItemIcon>
|
||||
<Dvr />
|
||||
</ListItemIcon>
|
|
@ -8,12 +8,11 @@ import ListItemText from '@mui/material/ListItemText';
|
|||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import globalize from 'scripts/globalize';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
|
||||
import ListItemLink from '../ListItemLink';
|
||||
|
||||
const PluginDrawerSection = () => {
|
||||
const { api } = useApi();
|
||||
const [ pagesInfo, setPagesInfo ] = useState<ConfigurationPageInfo[]>([]);
|
|
@ -8,21 +8,20 @@ import ListSubheader from '@mui/material/ListSubheader';
|
|||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import ListItemLink from '../ListItemLink';
|
||||
|
||||
const LIBRARY_PATHS = [
|
||||
'/library.html',
|
||||
'/librarydisplay.html',
|
||||
'/metadataimages.html',
|
||||
'/metadatanfo.html'
|
||||
'/dashboard/libraries',
|
||||
'/dashboard/libraries/display',
|
||||
'/dashboard/libraries/metadata',
|
||||
'/dashboard/libraries/nfo'
|
||||
];
|
||||
|
||||
const PLAYBACK_PATHS = [
|
||||
'/encodingsettings.html',
|
||||
'/playbackconfiguration.html',
|
||||
'/streamingsettings.html'
|
||||
'/dashboard/playback/transcoding',
|
||||
'/dashboard/playback/resume',
|
||||
'/dashboard/playback/streaming'
|
||||
];
|
||||
|
||||
const ServerDrawerSection = () => {
|
||||
|
@ -41,7 +40,7 @@ const ServerDrawerSection = () => {
|
|||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard.html'>
|
||||
<ListItemLink to='/dashboard'>
|
||||
<ListItemIcon>
|
||||
<Dashboard />
|
||||
</ListItemIcon>
|
||||
|
@ -49,7 +48,7 @@ const ServerDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboardgeneral.html'>
|
||||
<ListItemLink to='/dashboard/settings'>
|
||||
<ListItemIcon>
|
||||
<Settings />
|
||||
</ListItemIcon>
|
||||
|
@ -57,7 +56,7 @@ const ServerDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/userprofiles.html'>
|
||||
<ListItemLink to='/dashboard/users'>
|
||||
<ListItemIcon>
|
||||
<People />
|
||||
</ListItemIcon>
|
||||
|
@ -65,7 +64,7 @@ const ServerDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/library.html' selected={false}>
|
||||
<ListItemLink to='/dashboard/libraries' selected={false}>
|
||||
<ListItemIcon>
|
||||
<LibraryAdd />
|
||||
</ListItemIcon>
|
||||
|
@ -75,22 +74,22 @@ const ServerDrawerSection = () => {
|
|||
</ListItem>
|
||||
<Collapse in={isLibrarySectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/library.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/libraries' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('HeaderLibraries')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/librarydisplay.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/libraries/display' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Display')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/metadataimages.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/libraries/metadata' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Metadata')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/metadatanfo.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/libraries/nfo' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabNfoSettings')} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
</Collapse>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/encodingsettings.html' selected={false}>
|
||||
<ListItemLink to='/dashboard/playback/transcoding' selected={false}>
|
||||
<ListItemIcon>
|
||||
<PlayCircle />
|
||||
</ListItemIcon>
|
||||
|
@ -100,13 +99,13 @@ const ServerDrawerSection = () => {
|
|||
</ListItem>
|
||||
<Collapse in={isPlaybackSectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/encodingsettings.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/playback/transcoding' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Transcoding')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/playbackconfiguration.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/playback/resume' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('ButtonResume')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/streamingsettings.html' sx={{ pl: 4 }}>
|
||||
<ListItemLink to='/dashboard/playback/streaming' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabStreaming')} />
|
||||
</ListItemLink>
|
||||
</List>
|
12
src/apps/dashboard/routes/_asyncRoutes.ts
Normal file
12
src/apps/dashboard/routes/_asyncRoutes.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import type { AsyncRoute } from 'components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'activity' },
|
||||
{ path: 'notifications' },
|
||||
{ path: 'users' },
|
||||
{ path: 'users/access' },
|
||||
{ path: 'users/add' },
|
||||
{ path: 'users/parentalcontrol' },
|
||||
{ path: 'users/password' },
|
||||
{ path: 'users/profile' }
|
||||
];
|
|
@ -1,176 +1,164 @@
|
|||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
import type { LegacyRoute } from 'components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||
{
|
||||
path: 'dashboard.html',
|
||||
path: '/dashboard',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dashboard',
|
||||
view: 'dashboard/dashboard.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dashboardgeneral.html',
|
||||
path: 'settings',
|
||||
pageProps: {
|
||||
controller: 'dashboard/general',
|
||||
view: 'dashboard/general.html'
|
||||
}
|
||||
}, {
|
||||
path: 'networking.html',
|
||||
path: 'networking',
|
||||
pageProps: {
|
||||
controller: 'dashboard/networking',
|
||||
view: 'dashboard/networking.html'
|
||||
}
|
||||
}, {
|
||||
path: 'devices.html',
|
||||
path: 'devices',
|
||||
pageProps: {
|
||||
controller: 'dashboard/devices/devices',
|
||||
view: 'dashboard/devices/devices.html'
|
||||
}
|
||||
}, {
|
||||
path: 'device.html',
|
||||
path: 'devices/edit',
|
||||
pageProps: {
|
||||
controller: 'dashboard/devices/device',
|
||||
view: 'dashboard/devices/device.html'
|
||||
}
|
||||
}, {
|
||||
path: 'quickConnect.html',
|
||||
pageProps: {
|
||||
controller: 'dashboard/quickConnect',
|
||||
view: 'dashboard/quickConnect.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dlnaprofile.html',
|
||||
path: 'dlna/profiles/edit',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dlna/profile',
|
||||
view: 'dashboard/dlna/profile.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dlnaprofiles.html',
|
||||
path: 'dlna/profiles',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dlna/profiles',
|
||||
view: 'dashboard/dlna/profiles.html'
|
||||
}
|
||||
}, {
|
||||
path: 'dlnasettings.html',
|
||||
path: 'dlna',
|
||||
pageProps: {
|
||||
controller: 'dashboard/dlna/settings',
|
||||
view: 'dashboard/dlna/settings.html'
|
||||
}
|
||||
}, {
|
||||
path: 'addplugin.html',
|
||||
path: 'plugins/add',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/add/index',
|
||||
view: 'dashboard/plugins/add/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'library.html',
|
||||
path: 'libraries',
|
||||
pageProps: {
|
||||
controller: 'dashboard/library',
|
||||
view: 'dashboard/library.html'
|
||||
}
|
||||
}, {
|
||||
path: 'librarydisplay.html',
|
||||
path: 'libraries/display',
|
||||
pageProps: {
|
||||
controller: 'dashboard/librarydisplay',
|
||||
view: 'dashboard/librarydisplay.html'
|
||||
}
|
||||
}, {
|
||||
path: 'edititemmetadata.html',
|
||||
pageProps: {
|
||||
controller: 'edititemmetadata',
|
||||
view: 'edititemmetadata.html'
|
||||
}
|
||||
}, {
|
||||
path: 'encodingsettings.html',
|
||||
path: 'playback/transcoding',
|
||||
pageProps: {
|
||||
controller: 'dashboard/encodingsettings',
|
||||
view: 'dashboard/encodingsettings.html'
|
||||
}
|
||||
}, {
|
||||
path: 'log.html',
|
||||
path: 'logs',
|
||||
pageProps: {
|
||||
controller: 'dashboard/logs',
|
||||
view: 'dashboard/logs.html'
|
||||
}
|
||||
}, {
|
||||
path: 'metadataimages.html',
|
||||
path: 'libraries/metadata',
|
||||
pageProps: {
|
||||
controller: 'dashboard/metadataImages',
|
||||
view: 'dashboard/metadataimages.html'
|
||||
}
|
||||
}, {
|
||||
path: 'metadatanfo.html',
|
||||
path: 'libraries/nfo',
|
||||
pageProps: {
|
||||
controller: 'dashboard/metadatanfo',
|
||||
view: 'dashboard/metadatanfo.html'
|
||||
}
|
||||
}, {
|
||||
path: 'playbackconfiguration.html',
|
||||
path: 'playback/resume',
|
||||
pageProps: {
|
||||
controller: 'dashboard/playback',
|
||||
view: 'dashboard/playback.html'
|
||||
}
|
||||
}, {
|
||||
path: 'availableplugins.html',
|
||||
path: 'plugins/catalog',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/available/index',
|
||||
view: 'dashboard/plugins/available/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'repositories.html',
|
||||
path: 'plugins/repositories',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/repositories/index',
|
||||
view: 'dashboard/plugins/repositories/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvguideprovider.html',
|
||||
path: 'livetv/guide',
|
||||
pageProps: {
|
||||
controller: 'livetvguideprovider',
|
||||
view: 'livetvguideprovider.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvsettings.html',
|
||||
path: 'recordings',
|
||||
pageProps: {
|
||||
controller: 'livetvsettings',
|
||||
view: 'livetvsettings.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvstatus.html',
|
||||
path: 'livetv',
|
||||
pageProps: {
|
||||
controller: 'livetvstatus',
|
||||
view: 'livetvstatus.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetvtuner.html',
|
||||
path: 'livetv/tuner',
|
||||
pageProps: {
|
||||
controller: 'livetvtuner',
|
||||
view: 'livetvtuner.html'
|
||||
}
|
||||
}, {
|
||||
path: 'installedplugins.html',
|
||||
path: 'plugins',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/installed/index',
|
||||
view: 'dashboard/plugins/installed/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'scheduledtask.html',
|
||||
path: 'tasks/edit',
|
||||
pageProps: {
|
||||
controller: 'dashboard/scheduledtasks/scheduledtask',
|
||||
view: 'dashboard/scheduledtasks/scheduledtask.html'
|
||||
}
|
||||
}, {
|
||||
path: 'scheduledtasks.html',
|
||||
path: 'tasks',
|
||||
pageProps: {
|
||||
controller: 'dashboard/scheduledtasks/scheduledtasks',
|
||||
view: 'dashboard/scheduledtasks/scheduledtasks.html'
|
||||
}
|
||||
}, {
|
||||
path: 'apikeys.html',
|
||||
path: 'keys',
|
||||
pageProps: {
|
||||
controller: 'dashboard/apikeys',
|
||||
view: 'dashboard/apikeys.html'
|
||||
}
|
||||
}, {
|
||||
path: 'streamingsettings.html',
|
||||
path: 'playback/streaming',
|
||||
pageProps: {
|
||||
view: 'dashboard/streaming.html',
|
||||
controller: 'dashboard/streaming'
|
40
src/apps/dashboard/routes/_redirects.ts
Normal file
40
src/apps/dashboard/routes/_redirects.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import type { Redirect } from 'components/router/Redirect';
|
||||
|
||||
export const REDIRECTS: Redirect[] = [
|
||||
{ from: 'addplugin.html', to: '/dashboard/plugins/add' },
|
||||
{ from: 'apikeys.html', to: '/dashboard/keys' },
|
||||
{ from: 'availableplugins.html', to: '/dashboard/plugins/catalog' },
|
||||
{ from: 'dashboard.html', to: '/dashboard' },
|
||||
{ from: 'dashboardgeneral.html', to: '/dashboard/settings' },
|
||||
{ from: 'device.html', to: '/dashboard/devices/edit' },
|
||||
{ from: 'devices.html', to: '/dashboard/devices' },
|
||||
{ from: 'dlnaprofile.html', to: '/dashboard/dlna/profiles/edit' },
|
||||
{ from: 'dlnaprofiles.html', to: '/dashboard/dlna/profiles' },
|
||||
{ from: 'dlnasettings.html', to: '/dashboard/dlna' },
|
||||
{ from: 'edititemmetadata.html', to: '/metadata' },
|
||||
{ from: 'encodingsettings.html', to: '/dashboard/playback/transcoding' },
|
||||
{ from: 'installedplugins.html', to: '/dashboard/plugins' },
|
||||
{ from: 'library.html', to: '/dashboard/libraries' },
|
||||
{ from: 'librarydisplay.html', to: '/dashboard/libraries/display' },
|
||||
{ from: 'livetvguideprovider.html', to: '/dashboard/livetv/guide' },
|
||||
{ from: 'livetvsettings.html', to: '/dashboard/recordings' },
|
||||
{ from: 'livetvstatus.html', to: '/dashboard/livetv' },
|
||||
{ from: 'livetvtuner.html', to: '/dashboard/livetv/tuner' },
|
||||
{ from: 'log.html', to: '/dashboard/logs' },
|
||||
{ from: 'metadataimages.html', to: '/dashboard/libraries/metadata' },
|
||||
{ from: 'metadatanfo.html', to: '/dashboard/libraries/nfo' },
|
||||
{ from: 'networking.html', to: '/dashboard/networking' },
|
||||
{ from: 'notificationsettings.html', to: '/dashboard/notifications' },
|
||||
{ from: 'playbackconfiguration.html', to: '/dashboard/playback/resume' },
|
||||
{ from: 'repositories.html', to: '/dashboard/plugins/repositories' },
|
||||
{ from: 'scheduledtask.html', to: '/dashboard/tasks/edit' },
|
||||
{ from: 'scheduledtasks.html', to: '/dashboard/tasks' },
|
||||
{ from: 'serveractivity.html', to: '/dashboard/activity' },
|
||||
{ from: 'streamingsettings.html', to: '/dashboard/playback/streaming' },
|
||||
{ from: 'useredit.html', to: '/dashboard/users/profile' },
|
||||
{ from: 'userlibraryaccess.html', to: '/dashboard/users/access' },
|
||||
{ from: 'usernew.html', to: '/dashboard/users/add' },
|
||||
{ from: 'userparentalcontrol.html', to: '/dashboard/users/parentalcontrol' },
|
||||
{ from: 'userpassword.html', to: '/dashboard/users/password' },
|
||||
{ from: 'userprofiles.html', to: '/dashboard/users' }
|
||||
];
|
|
@ -19,9 +19,9 @@ import { parseISO8601Date, toLocaleDateString, toLocaleTimeString } from 'script
|
|||
import globalize from 'scripts/globalize';
|
||||
import { toBoolean } from 'utils/string';
|
||||
|
||||
import LogLevelChip from '../../components/activityTable/LogLevelChip';
|
||||
import OverviewCell from '../../components/activityTable/OverviewCell';
|
||||
import GridActionsCellLink from '../../components/GridActionsCellLink';
|
||||
import LogLevelChip from '../components/activityTable/LogLevelChip';
|
||||
import OverviewCell from '../components/activityTable/OverviewCell';
|
||||
import GridActionsCellLink from '../components/dataGrid/GridActionsCellLink';
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 25;
|
||||
const VIEW_PARAM = 'useractivity';
|
||||
|
@ -68,7 +68,7 @@ const Activity = () => {
|
|||
sx={{ padding: 0 }}
|
||||
title={users[row.UserId]?.Name ?? undefined}
|
||||
component={Link}
|
||||
to={`/useredit.html?userId=${row.UserId}`}
|
||||
to={`/dashboard/users/profile?userId=${row.UserId}`}
|
||||
>
|
||||
<UserAvatar user={users[row.UserId]} />
|
||||
</IconButton>
|
|
@ -9,7 +9,7 @@ const PluginLink = () => (
|
|||
__html: `<a
|
||||
is='emby-linkbutton'
|
||||
class='button-link'
|
||||
href='#/addplugin.html?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173'
|
||||
href='#/dashboard/plugins/add?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173'
|
||||
>
|
||||
${globalize.translate('GetThePlugin')}
|
||||
</a>`
|
|
@ -140,7 +140,7 @@ const UserNew: FunctionComponent = () => {
|
|||
}
|
||||
|
||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
Dashboard.navigate('useredit.html?userId=' + user.Id)
|
||||
Dashboard.navigate('/dashboard/users/profile?userId=' + user.Id)
|
||||
.catch(err => {
|
||||
console.error('[usernew] failed to navigate to edit user page', err);
|
||||
});
|
|
@ -85,21 +85,21 @@ const UserProfiles: FunctionComponent = () => {
|
|||
callback: function (id: string) {
|
||||
switch (id) {
|
||||
case 'open':
|
||||
Dashboard.navigate('useredit.html?userId=' + userId)
|
||||
Dashboard.navigate('/dashboard/users/profile?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to user edit page', err);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'access':
|
||||
Dashboard.navigate('userlibraryaccess.html?userId=' + userId)
|
||||
Dashboard.navigate('/dashboard/users/access?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to user library page', err);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'parentalcontrol':
|
||||
Dashboard.navigate('userparentalcontrol.html?userId=' + userId)
|
||||
Dashboard.navigate('/dashboard/users/parentalcontrol?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to parental control page', err);
|
||||
});
|
||||
|
@ -146,7 +146,7 @@ const UserProfiles: FunctionComponent = () => {
|
|||
});
|
||||
|
||||
(page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
|
||||
Dashboard.navigate('usernew.html')
|
||||
Dashboard.navigate('/dashboard/users/add')
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to new user page', err);
|
||||
});
|
|
@ -32,7 +32,7 @@ const getCheckedElementDataIds = (elements: NodeListOf<Element>) => (
|
|||
);
|
||||
|
||||
function onSaveComplete() {
|
||||
Dashboard.navigate('userprofiles.html')
|
||||
Dashboard.navigate('/dashboard/users')
|
||||
.catch(err => {
|
||||
console.error('[useredit] failed to navigate to user profile', err);
|
||||
});
|
|
@ -1,14 +1,16 @@
|
|||
import React from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
||||
import { REDIRECTS } from 'apps/stable/routes/_redirects';
|
||||
import ConnectionRequired from 'components/ConnectionRequired';
|
||||
import ServerContentPage from 'components/ServerContentPage';
|
||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||
import { toRedirectRoute } from 'components/router/Redirect';
|
||||
|
||||
import AppLayout from './AppLayout';
|
||||
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
||||
import { ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
||||
|
||||
const ExperimentalApp = () => {
|
||||
return (
|
||||
|
@ -20,26 +22,25 @@ const ExperimentalApp = () => {
|
|||
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
|
||||
</Route>
|
||||
|
||||
{/* Admin routes */}
|
||||
<Route element={<ConnectionRequired isAdminRequired />}>
|
||||
{ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)}
|
||||
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
|
||||
|
||||
<Route path='configurationpage' element={
|
||||
<ServerContentPage view='/web/configurationpage' />
|
||||
} />
|
||||
</Route>
|
||||
|
||||
{/* Public routes */}
|
||||
<Route element={<ConnectionRequired isUserRequired={false} />}>
|
||||
<Route index element={<Navigate replace to='/home.html' />} />
|
||||
|
||||
{LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)}
|
||||
</Route>
|
||||
|
||||
{/* Redirects for old paths */}
|
||||
<Route path='serveractivity.html' element={<Navigate replace to='/dashboard/activity' />} />
|
||||
</Route>
|
||||
|
||||
{/* Redirects for old paths */}
|
||||
{REDIRECTS.map(toRedirectRoute)}
|
||||
|
||||
{/* Ignore dashboard routes */}
|
||||
{Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => (
|
||||
<Route
|
||||
key={key}
|
||||
path={`/${path}/*`}
|
||||
element={null}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
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 AppHeader from 'components/AppHeader';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
import AppBody from 'components/AppBody';
|
||||
import ElevationScroll from 'components/ElevationScroll';
|
||||
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||
|
||||
import AppToolbar from './components/AppToolbar';
|
||||
import AppDrawer, { DRAWER_WIDTH, isDrawerPath } from './components/drawers/AppDrawer';
|
||||
import ElevationScroll from './components/ElevationScroll';
|
||||
import theme from './theme';
|
||||
import AppDrawer, { isDrawerPath } from './components/drawers/AppDrawer';
|
||||
|
||||
import './AppOverrides.scss';
|
||||
|
||||
|
@ -29,6 +28,7 @@ const AppLayout = () => {
|
|||
const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned);
|
||||
const { user } = useApi();
|
||||
const location = useLocation();
|
||||
const theme = useTheme();
|
||||
|
||||
const isDrawerAvailable = isDrawerPath(location.pathname);
|
||||
const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user);
|
||||
|
@ -47,67 +47,54 @@ const AppLayout = () => {
|
|||
}, [ isDrawerActive, setIsDrawerActive ]);
|
||||
|
||||
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' }}>
|
||||
<ElevationScroll elevate={isDrawerOpen}>
|
||||
<AppBar
|
||||
position='fixed'
|
||||
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.drawer + 1 }}
|
||||
>
|
||||
<AppToolbar
|
||||
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
|
||||
})
|
||||
}}
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<ElevationScroll elevate={isDrawerOpen}>
|
||||
<AppBar
|
||||
position='fixed'
|
||||
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.drawer + 1 }}
|
||||
>
|
||||
<div className='mainAnimatedPages skinBody' />
|
||||
<div className='skinBody'>
|
||||
<Outlet />
|
||||
</div>
|
||||
</Box>
|
||||
<AppToolbar
|
||||
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>
|
||||
</ThemeProvider>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -10,11 +10,6 @@ $mui-bp-xl: 1536px;
|
|||
position: relative;
|
||||
}
|
||||
|
||||
// Fix dashboard pages layout to work with drawer
|
||||
.dashboardDocument .skinBody {
|
||||
position: unset;
|
||||
}
|
||||
|
||||
// Hide some items from the user "settings" page that are in the drawer
|
||||
#myPreferencesMenuPage {
|
||||
.lnkQuickConnectPreferences,
|
||||
|
@ -26,8 +21,7 @@ $mui-bp-xl: 1536px;
|
|||
|
||||
// Fix the padding of some pages
|
||||
.homePage.libraryPage, // Home page
|
||||
.libraryPage:not(.withTabs), // Tabless library pages
|
||||
.content-primary.content-primary { // Dashboard pages
|
||||
.libraryPage:not(.withTabs) { // Tabless library pages
|
||||
padding-top: 3.25rem !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,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 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 } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
import appIcon from 'assets/img/icon-transparent.png';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import AppToolbar from 'components/toolbar/AppToolbar';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import AppTabs from '../tabs/AppTabs';
|
||||
import { isDrawerPath } from '../drawers/AppDrawer';
|
||||
import UserMenuButton from './UserMenuButton';
|
||||
import RemotePlayButton from './RemotePlayButton';
|
||||
import SyncPlayButton from './SyncPlayButton';
|
||||
|
||||
|
@ -25,120 +17,40 @@ interface AppToolbarProps {
|
|||
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
||||
}
|
||||
|
||||
const onBackButtonClick = () => {
|
||||
appRouter.back()
|
||||
.catch(err => {
|
||||
console.error('[AppToolbar] error calling appRouter.back', err);
|
||||
});
|
||||
};
|
||||
|
||||
const AppToolbar: FC<AppToolbarProps> = ({
|
||||
const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
|
||||
isDrawerOpen,
|
||||
onDrawerButtonClick
|
||||
}) => {
|
||||
const { user } = useApi();
|
||||
const isUserLoggedIn = Boolean(user);
|
||||
const location = useLocation();
|
||||
|
||||
const isDrawerAvailable = isDrawerPath(location.pathname);
|
||||
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>
|
||||
|
||||
<AppTabs isDrawerOpen={isDrawerOpen} />
|
||||
|
||||
{isUserLoggedIn && (
|
||||
<AppToolbar
|
||||
buttons={
|
||||
<>
|
||||
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'flex-end' }}>
|
||||
<SyncPlayButton />
|
||||
<RemotePlayButton />
|
||||
<SyncPlayButton />
|
||||
<RemotePlayButton />
|
||||
|
||||
<Tooltip title={globalize.translate('Search')}>
|
||||
<IconButton
|
||||
size='large'
|
||||
aria-label={globalize.translate('Search')}
|
||||
color='inherit'
|
||||
component={Link}
|
||||
to='/search.html'
|
||||
>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flexGrow: 0 }}>
|
||||
<UserMenuButton />
|
||||
</Box>
|
||||
<Tooltip title={globalize.translate('Search')}>
|
||||
<IconButton
|
||||
size='large'
|
||||
aria-label={globalize.translate('Search')}
|
||||
color='inherit'
|
||||
component={Link}
|
||||
to='/search.html'
|
||||
>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</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 { useApi } from 'hooks/useApi';
|
||||
import { useSyncPlayGroups } from 'hooks/useSyncPlayGroups';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { PluginType } from 'types/plugin';
|
||||
import Events from 'utils/events';
|
||||
|
@ -47,7 +48,6 @@ const SyncPlayMenu: FC<SyncPlayMenuProps> = ({
|
|||
}) => {
|
||||
const [ syncPlay, setSyncPlay ] = useState<SyncPlayInstance>();
|
||||
const { __legacyApiClient__, api, user } = useApi();
|
||||
const [ groups, setGroups ] = useState<GroupInfoDto[]>([]);
|
||||
const [ currentGroup, setCurrentGroup ] = useState<GroupInfoDto>();
|
||||
const isSyncPlayEnabled = Boolean(currentGroup);
|
||||
|
||||
|
@ -55,18 +55,7 @@ const SyncPlayMenu: FC<SyncPlayMenuProps> = ({
|
|||
setSyncPlay(pluginManager.firstOfType(PluginType.SyncPlay)?.instance);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
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 { data: groups } = useSyncPlayGroups();
|
||||
|
||||
const onGroupAddClick = useCallback(() => {
|
||||
if (api && user) {
|
||||
|
@ -224,7 +213,7 @@ const SyncPlayMenu: FC<SyncPlayMenuProps> = ({
|
|||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
} else if (groups.length === 0 && user?.Policy?.SyncPlayAccess !== SyncPlayUserAccessType.CreateAndJoinGroups) {
|
||||
} else if (!groups?.length && user?.Policy?.SyncPlayAccess !== SyncPlayUserAccessType.CreateAndJoinGroups) {
|
||||
menuItems.push(
|
||||
<MenuItem key='sync-play-unavailable' disabled>
|
||||
<ListItemIcon>
|
||||
|
@ -234,7 +223,7 @@ const SyncPlayMenu: FC<SyncPlayMenuProps> = ({
|
|||
</MenuItem>
|
||||
);
|
||||
} else {
|
||||
if (groups.length > 0) {
|
||||
if (groups && groups.length > 0) {
|
||||
groups.forEach(group => {
|
||||
menuItems.push(
|
||||
<MenuItem
|
||||
|
|
|
@ -1,21 +1,15 @@
|
|||
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 { LEGACY_ADMIN_ROUTES, LEGACY_USER_ROUTES } from '../../routes/legacyRoutes';
|
||||
import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';
|
||||
|
||||
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 ResponsiveDrawer, { ResponsiveDrawerProps } from './ResponsiveDrawer';
|
||||
|
||||
export const DRAWER_WIDTH = 240;
|
||||
|
||||
const DRAWERLESS_ROUTES = [
|
||||
'edititemmetadata.html', // metadata manager
|
||||
'video' // video player
|
||||
];
|
||||
|
||||
|
@ -24,63 +18,29 @@ const MAIN_DRAWER_ROUTES = [
|
|||
...LEGACY_USER_ROUTES
|
||||
].filter(route => !DRAWERLESS_ROUTES.includes(route.path));
|
||||
|
||||
const ADMIN_DRAWER_ROUTES = [
|
||||
...ASYNC_ADMIN_ROUTES,
|
||||
...LEGACY_ADMIN_ROUTES,
|
||||
{ path: '/configurationpage' } // Plugin configuration page
|
||||
].filter(route => !DRAWERLESS_ROUTES.includes(route.path));
|
||||
|
||||
/** Utility function to check if a path has a drawer. */
|
||||
export const isDrawerPath = (path: string) => (
|
||||
MAIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path)
|
||||
|| ADMIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path)
|
||||
);
|
||||
|
||||
const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
||||
open = false,
|
||||
onClose,
|
||||
onOpen
|
||||
}) => (
|
||||
<Routes>
|
||||
{
|
||||
MAIN_DRAWER_ROUTES.map(route => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={
|
||||
<ResponsiveDrawer
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
<MainDrawerContent />
|
||||
</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>
|
||||
);
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const hasSecondaryToolBar = isTabPath(location.pathname);
|
||||
|
||||
return (
|
||||
<ResponsiveDrawer
|
||||
hasSecondaryToolBar={hasSecondaryToolBar}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
<MainDrawerContent />
|
||||
</ResponsiveDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppDrawer;
|
||||
|
|
|
@ -17,12 +17,12 @@ import ListSubheader from '@mui/material/ListSubheader';
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { useWebConfig } from 'hooks/useWebConfig';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
|
||||
import ListItemLink from './ListItemLink';
|
||||
import LibraryIcon from '../LibraryIcon';
|
||||
|
||||
const MainDrawerContent = () => {
|
||||
|
@ -150,7 +150,7 @@ const MainDrawerContent = () => {
|
|||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard.html'>
|
||||
<ListItemLink to='/dashboard'>
|
||||
<ListItemIcon>
|
||||
<Dashboard />
|
||||
</ListItemIcon>
|
||||
|
@ -158,7 +158,7 @@ const MainDrawerContent = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/edititemmetadata.html'>
|
||||
<ListItemLink to='/metadata'>
|
||||
<ListItemIcon>
|
||||
<Edit />
|
||||
</ListItemIcon>
|
||||
|
|
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')}
|
||||
sx={{ ml: 2 }}
|
||||
aria-describedby={id}
|
||||
className='paper-icon-button-light btnShuffle autoSize'
|
||||
className='paper-icon-button-light btnSort autoSize'
|
||||
onClick={handleClick}
|
||||
>
|
||||
<SortByAlphaIcon />
|
||||
|
|
|
@ -100,7 +100,7 @@ const ViewSettingsButton: FC<ViewSettingsButtonProps> = ({
|
|||
title={globalize.translate('ButtonSelectView')}
|
||||
sx={{ ml: 2 }}
|
||||
aria-describedby={id}
|
||||
className='paper-icon-button-light btnShuffle autoSize'
|
||||
className='paper-icon-button-light btnSelectView autoSize'
|
||||
onClick={handleClick}
|
||||
>
|
||||
<ViewComfyIcon />
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import { AsyncRoute, AsyncRouteType } from 'components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'dashboard/activity', page: 'dashboard/activity', type: AsyncRouteType.Experimental },
|
||||
{ path: 'notificationsettings.html', page: 'dashboard/notifications' },
|
||||
{ path: 'usernew.html', page: 'user/usernew' },
|
||||
{ path: 'userprofiles.html', page: 'user/userprofiles' },
|
||||
{ path: 'useredit.html', page: 'user/useredit' },
|
||||
{ path: 'userlibraryaccess.html', page: 'user/userlibraryaccess' },
|
||||
{ path: 'userparentalcontrol.html', page: 'user/userparentalcontrol' },
|
||||
{ path: 'userpassword.html', page: 'user/userpassword' }
|
||||
];
|
|
@ -1,2 +1 @@
|
|||
export * from './admin';
|
||||
export * from './user';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { AsyncRoute, AsyncRouteType } from '../../../../components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_USER_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'quickconnect', page: 'quickConnect' },
|
||||
{ path: 'search.html', page: 'search' },
|
||||
{ path: 'userprofile.html', page: 'user/userprofile' },
|
||||
{ path: 'home.html', page: 'home', type: AsyncRouteType.Experimental },
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
export * from './admin';
|
||||
export * from './public';
|
||||
export * from './user';
|
||||
|
|
|
@ -49,12 +49,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
|
|||
controller: 'user/home/index',
|
||||
view: 'user/home/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'mypreferencesquickconnect.html',
|
||||
pageProps: {
|
||||
controller: 'user/quickConnect/index',
|
||||
view: 'user/quickConnect/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'mypreferencesplayback.html',
|
||||
pageProps: {
|
||||
|
|
|
@ -1,26 +1,21 @@
|
|||
import React from 'react';
|
||||
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import AppHeader from 'components/AppHeader';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
import ServerContentPage from 'components/ServerContentPage';
|
||||
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
||||
import AppBody from 'components/AppBody';
|
||||
import ConnectionRequired from 'components/ConnectionRequired';
|
||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||
import { toRedirectRoute } from 'components/router/Redirect';
|
||||
|
||||
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
||||
import { ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
||||
import { REDIRECTS } from './routes/_redirects';
|
||||
|
||||
const Layout = () => (
|
||||
<>
|
||||
<Backdrop />
|
||||
<AppHeader />
|
||||
|
||||
<div className='mainAnimatedPages skinBody' />
|
||||
<div className='skinBody'>
|
||||
<Outlet />
|
||||
</div>
|
||||
</>
|
||||
<AppBody>
|
||||
<Outlet />
|
||||
</AppBody>
|
||||
);
|
||||
|
||||
const StableApp = () => (
|
||||
|
@ -32,16 +27,6 @@ const StableApp = () => (
|
|||
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
|
||||
</Route>
|
||||
|
||||
{/* Admin routes */}
|
||||
<Route path='/' element={<ConnectionRequired isAdminRequired />}>
|
||||
{ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)}
|
||||
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
|
||||
|
||||
<Route path='configurationpage' element={
|
||||
<ServerContentPage view='/web/configurationpage' />
|
||||
} />
|
||||
</Route>
|
||||
|
||||
{/* Public routes */}
|
||||
<Route path='/' element={<ConnectionRequired isUserRequired={false} />}>
|
||||
<Route index element={<Navigate replace to='/home.html' />} />
|
||||
|
@ -51,10 +36,19 @@ const StableApp = () => (
|
|||
|
||||
{/* Suppress warnings for unhandled routes */}
|
||||
<Route path='*' element={null} />
|
||||
|
||||
{/* Redirects for old paths */}
|
||||
<Route path='/serveractivity.html' element={<Navigate replace to='/dashboard/activity' />} />
|
||||
</Route>
|
||||
|
||||
{/* Redirects for old paths */}
|
||||
{REDIRECTS.map(toRedirectRoute)}
|
||||
|
||||
{/* Ignore dashboard routes */}
|
||||
{Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => (
|
||||
<Route
|
||||
key={key}
|
||||
path={`/${path}/*`}
|
||||
element={null}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
);
|
||||
|
||||
|
|
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';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_USER_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'quickconnect', page: 'quickConnect' },
|
||||
{ path: 'search.html', page: 'search' },
|
||||
{ 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 './user';
|
||||
|
|
|
@ -49,12 +49,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
|
|||
controller: 'user/home/index',
|
||||
view: 'user/home/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'mypreferencesquickconnect.html',
|
||||
pageProps: {
|
||||
controller: 'user/quickConnect/index',
|
||||
view: 'user/quickConnect/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'mypreferencesplayback.html',
|
||||
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(() => {
|
||||
// Initialize the UI components after first render
|
||||
import('../scripts/libraryMenu');
|
||||
}, []);
|
||||
|
||||
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-scrollContainer scrollContainer focuscontainer-y' />
|
||||
</div>
|
||||
<div className='skinHeader focuscontainer-x' />
|
||||
<div className='mainDrawerHandle' />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -5,14 +5,13 @@ import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
|||
import Toolbar from '@mui/material/Toolbar';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import browser from 'scripts/browser';
|
||||
|
||||
import { DRAWER_WIDTH } from './AppDrawer';
|
||||
import { isTabPath } from '../tabs/tabRoutes';
|
||||
export const DRAWER_WIDTH = 240;
|
||||
|
||||
export interface ResponsiveDrawerProps {
|
||||
hasSecondaryToolBar?: boolean
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onOpen: () => void
|
||||
|
@ -20,18 +19,17 @@ export interface ResponsiveDrawerProps {
|
|||
|
||||
const ResponsiveDrawer: FC<ResponsiveDrawerProps> = ({
|
||||
children,
|
||||
hasSecondaryToolBar = false,
|
||||
open = false,
|
||||
onClose,
|
||||
onOpen
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const isSmallScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
|
||||
const isLargeScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('lg'));
|
||||
const isTallToolbar = isTabPath(location.pathname) && !isLargeScreen;
|
||||
|
||||
const getToolbarStyles = useCallback((theme: Theme) => ({
|
||||
marginBottom: isTallToolbar ? theme.spacing(6) : 0
|
||||
}), [ isTallToolbar ]);
|
||||
marginBottom: (hasSecondaryToolBar && !isLargeScreen) ? theme.spacing(6) : 0
|
||||
}), [ hasSecondaryToolBar, isLargeScreen ]);
|
||||
|
||||
return ( isSmallScreen ? (
|
||||
/* DESKTOP DRAWER */
|
|
@ -5,24 +5,30 @@
|
|||
*/
|
||||
|
||||
import escapeHtml from 'escape-html';
|
||||
import datetime from '../../scripts/datetime';
|
||||
import imageLoader from '../images/imageLoader';
|
||||
import itemHelper from '../itemHelper';
|
||||
|
||||
import cardBuilderUtils from './cardBuilderUtils';
|
||||
import browser from 'scripts/browser';
|
||||
import datetime from 'scripts/datetime';
|
||||
import dom from 'scripts/dom';
|
||||
import globalize from 'scripts/globalize';
|
||||
import imageHelper from 'scripts/imagehelper';
|
||||
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
|
||||
import { randomInt } from 'utils/number';
|
||||
|
||||
import focusManager from '../focusManager';
|
||||
import imageLoader from '../images/imageLoader';
|
||||
import indicators from '../indicators/indicators';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import itemHelper from '../itemHelper';
|
||||
import layoutManager from '../layoutManager';
|
||||
import dom from '../../scripts/dom';
|
||||
import browser from '../../scripts/browser';
|
||||
import { playbackManager } from '../playback/playbackmanager';
|
||||
import itemShortcuts from '../shortcuts';
|
||||
import imageHelper from '../../scripts/imagehelper';
|
||||
import { randomInt } from '../../utils/number.ts';
|
||||
import './card.scss';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../guide/programs.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import itemShortcuts from '../shortcuts';
|
||||
|
||||
import 'elements/emby-button/paper-icon-button-light';
|
||||
|
||||
import './card.scss';
|
||||
import '../guide/programs.scss';
|
||||
|
||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
|
||||
|
@ -41,217 +47,6 @@ export function getCardsHtml(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.
|
||||
* @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.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -301,16 +96,16 @@ function setCardData(items, options) {
|
|||
options.shape = 'banner';
|
||||
options.coverImage = true;
|
||||
} else if (primaryImageAspectRatio >= 1.33) {
|
||||
options.shape = requestedShape === 'autooverflow' ? 'overflowBackdrop' : 'backdrop';
|
||||
options.shape = getBackdropShape(requestedShape === 'autooverflow');
|
||||
} else if (primaryImageAspectRatio > 0.71) {
|
||||
options.shape = requestedShape === 'autooverflow' ? 'overflowSquare' : 'square';
|
||||
options.shape = getSquareShape(requestedShape === 'autooverflow');
|
||||
} else {
|
||||
options.shape = requestedShape === 'autooverflow' ? 'overflowPortrait' : 'portrait';
|
||||
options.shape = getPortraitShape(requestedShape === 'autooverflow');
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.shape) {
|
||||
options.shape = options.defaultShape || (requestedShape === 'autooverflow' ? 'overflowSquare' : 'square');
|
||||
options.shape = options.defaultShape || getSquareShape(requestedShape === 'autooverflow');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -318,7 +113,7 @@ function setCardData(items, options) {
|
|||
options.preferThumb = options.shape === 'backdrop' || options.shape === 'overflowBackdrop';
|
||||
}
|
||||
|
||||
options.uiAspect = getDesiredAspect(options.shape);
|
||||
options.uiAspect = cardBuilderUtils.getDesiredAspect(options.shape);
|
||||
options.primaryImageAspectRatio = primaryImageAspectRatio;
|
||||
|
||||
if (!options.width && options.widths) {
|
||||
|
@ -460,30 +255,6 @@ function buildCardsHtmlInternal(items, options) {
|
|||
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
|
||||
* @property {string} imgUrl - Image URL.
|
||||
|
@ -509,7 +280,7 @@ function getCardImageUrl(item, apiClient, options, shape) {
|
|||
let imgUrl = null;
|
||||
let imgTag = null;
|
||||
let coverImage = false;
|
||||
const uiAspect = getDesiredAspect(shape);
|
||||
const uiAspect = cardBuilderUtils.getDesiredAspect(shape);
|
||||
let imgType = null;
|
||||
let itemId = null;
|
||||
|
||||
|
|
173
src/components/cardbuilder/cardBuilderUtils.js
Normal file
173
src/components/cardbuilder/cardBuilderUtils.js
Normal file
|
@ -0,0 +1,173 @@
|
|||
const ASPECT_RATIOS = {
|
||||
portrait: (2 / 3),
|
||||
backdrop: (16 / 9),
|
||||
square: 1,
|
||||
banner: (1000 / 185)
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the aspect ratio for a card given its shape.
|
||||
* @param {string} shape - Shape for which to get the aspect ratio.
|
||||
* @returns {null|number} Ratio of the shape.
|
||||
*/
|
||||
function getDesiredAspect(shape) {
|
||||
if (!shape) {
|
||||
return null;
|
||||
}
|
||||
|
||||
shape = shape.toLowerCase();
|
||||
if (shape.indexOf('portrait') !== -1) {
|
||||
return ASPECT_RATIOS.portrait;
|
||||
}
|
||||
if (shape.indexOf('backdrop') !== -1) {
|
||||
return ASPECT_RATIOS.backdrop;
|
||||
}
|
||||
if (shape.indexOf('square') !== -1) {
|
||||
return ASPECT_RATIOS.square;
|
||||
}
|
||||
if (shape.indexOf('banner') !== -1) {
|
||||
return ASPECT_RATIOS.banner;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the number of posters per row.
|
||||
* @param {string} shape - Shape of the cards.
|
||||
* @param {number} screenWidth - Width of the screen.
|
||||
* @param {boolean} isOrientationLandscape - Flag for the orientation of the screen.
|
||||
* @param {boolean} isTV - Flag to denote if posters are rendered on a television screen.
|
||||
* @returns {number} Number of cards per row for an itemsContainer.
|
||||
*/
|
||||
function getPostersPerRow(shape, screenWidth, isOrientationLandscape, isTV) {
|
||||
switch (shape) {
|
||||
case 'portrait': return postersPerRowPortrait(screenWidth, isTV);
|
||||
case 'square': return postersPerRowSquare(screenWidth, isTV);
|
||||
case 'banner': return postersPerRowBanner(screenWidth);
|
||||
case 'backdrop': return postersPerRowBackdrop(screenWidth, isTV);
|
||||
case 'smallBackdrop': return postersPerRowSmallBackdrop(screenWidth);
|
||||
case 'overflowSmallBackdrop': return postersPerRowOverflowSmallBackdrop(screenWidth, isOrientationLandscape, isTV);
|
||||
case 'overflowPortrait': return postersPerRowOverflowPortrait(screenWidth, isOrientationLandscape, isTV);
|
||||
case 'overflowSquare': return postersPerRowOverflowSquare(screenWidth, isOrientationLandscape, isTV);
|
||||
case 'overflowBackdrop': return postersPerRowOverflowBackdrop(screenWidth, isOrientationLandscape, isTV);
|
||||
default: return 4;
|
||||
}
|
||||
}
|
||||
|
||||
const postersPerRowPortrait = (screenWidth, isTV) => {
|
||||
switch (true) {
|
||||
case isTV: return 100 / 16.66666667;
|
||||
case screenWidth >= 2200: return 10;
|
||||
case screenWidth >= 1920: return 100 / 11.1111111111;
|
||||
case screenWidth >= 1600: return 8;
|
||||
case screenWidth >= 1400: return 100 / 14.28571428571;
|
||||
case screenWidth >= 1200: return 100 / 16.66666667;
|
||||
case screenWidth >= 800: return 5;
|
||||
case screenWidth >= 700: return 4;
|
||||
case screenWidth >= 500: return 100 / 33.33333333;
|
||||
default: return 100 / 33.33333333;
|
||||
}
|
||||
};
|
||||
|
||||
const postersPerRowSquare = (screenWidth, isTV) => {
|
||||
switch (true) {
|
||||
case isTV: return 100 / 16.66666667;
|
||||
case screenWidth >= 2200: return 10;
|
||||
case screenWidth >= 1920: return 100 / 11.1111111111;
|
||||
case screenWidth >= 1600: return 8;
|
||||
case screenWidth >= 1400: return 100 / 14.28571428571;
|
||||
case screenWidth >= 1200: return 100 / 16.66666667;
|
||||
case screenWidth >= 800: return 5;
|
||||
case screenWidth >= 700: return 4;
|
||||
case screenWidth >= 500: return 100 / 33.33333333;
|
||||
default: return 2;
|
||||
}
|
||||
};
|
||||
|
||||
const postersPerRowBanner = (screenWidth) => {
|
||||
switch (true) {
|
||||
case screenWidth >= 2200: return 4;
|
||||
case screenWidth >= 1200: return 100 / 33.33333333;
|
||||
case screenWidth >= 800: return 2;
|
||||
default: return 1;
|
||||
}
|
||||
};
|
||||
|
||||
const postersPerRowBackdrop = (screenWidth, isTV) => {
|
||||
switch (true) {
|
||||
case isTV: return 4;
|
||||
case screenWidth >= 2500: return 6;
|
||||
case screenWidth >= 1600: return 5;
|
||||
case screenWidth >= 1200: return 4;
|
||||
case screenWidth >= 770: return 3;
|
||||
case screenWidth >= 420: return 2;
|
||||
default: return 1;
|
||||
}
|
||||
};
|
||||
|
||||
function postersPerRowSmallBackdrop(screenWidth) {
|
||||
switch (true) {
|
||||
case screenWidth >= 1600: return 8;
|
||||
case screenWidth >= 1400: return 100 / 14.2857142857;
|
||||
case screenWidth >= 1200: return 100 / 16.66666667;
|
||||
case screenWidth >= 1000: return 5;
|
||||
case screenWidth >= 800: return 4;
|
||||
case screenWidth >= 500: return 100 / 33.33333333;
|
||||
default: return 2;
|
||||
}
|
||||
}
|
||||
|
||||
const postersPerRowOverflowSmallBackdrop = (screenWidth, isLandscape, isTV) => {
|
||||
switch (true) {
|
||||
case isTV: return 100 / 18.9;
|
||||
case isLandscape && screenWidth >= 800: return 100 / 15.5;
|
||||
case isLandscape: return 100 / 23.3;
|
||||
case screenWidth >= 540: return 100 / 30;
|
||||
default: return 100 / 72;
|
||||
}
|
||||
};
|
||||
|
||||
const postersPerRowOverflowPortrait = (screenWidth, isLandscape, isTV) => {
|
||||
switch (true) {
|
||||
case isTV: return 100 / 15.5;
|
||||
case isLandscape && screenWidth >= 1700: return 100 / 11.6;
|
||||
case isLandscape: return 100 / 15.5;
|
||||
case screenWidth >= 1400: return 100 / 15;
|
||||
case screenWidth >= 1200: return 100 / 18;
|
||||
case screenWidth >= 760: return 100 / 23;
|
||||
case screenWidth >= 400: return 100 / 31.5;
|
||||
default: return 100 / 42;
|
||||
}
|
||||
};
|
||||
|
||||
const postersPerRowOverflowSquare = (screenWidth, isLandscape, isTV) => {
|
||||
switch (true) {
|
||||
case isTV: return 100 / 15.5;
|
||||
case isLandscape && screenWidth >= 1700: return 100 / 11.6;
|
||||
case isLandscape: return 100 / 15.5;
|
||||
case screenWidth >= 1400: return 100 / 15;
|
||||
case screenWidth >= 1200: return 100 / 18;
|
||||
case screenWidth >= 760: return 100 / 23;
|
||||
case screenWidth >= 540: return 100 / 31.5;
|
||||
default: return 100 / 42;
|
||||
}
|
||||
};
|
||||
|
||||
const postersPerRowOverflowBackdrop = (screenWidth, isLandscape, isTV) => {
|
||||
switch (true) {
|
||||
case isTV: return 100 / 23.3;
|
||||
case isLandscape && screenWidth >= 1700: return 100 / 18.5;
|
||||
case isLandscape: return 100 / 23.3;
|
||||
case screenWidth >= 1800: return 100 / 23.5;
|
||||
case screenWidth >= 1400: return 100 / 30;
|
||||
case screenWidth >= 760: return 100 / 40;
|
||||
case screenWidth >= 640: return 100 / 56;
|
||||
default: return 100 / 72;
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
getDesiredAspect,
|
||||
getPostersPerRow
|
||||
};
|
417
src/components/cardbuilder/cardBuilderUtils.test.js
Normal file
417
src/components/cardbuilder/cardBuilderUtils.test.js
Normal file
|
@ -0,0 +1,417 @@
|
|||
import { describe, expect, test } from 'vitest';
|
||||
import cardBuilderUtils from './cardBuilderUtils';
|
||||
|
||||
describe('getDesiredAspect', () => {
|
||||
test('"portrait" (case insensitive)', () => {
|
||||
expect(cardBuilderUtils.getDesiredAspect('portrait')).toEqual((2 / 3));
|
||||
expect(cardBuilderUtils.getDesiredAspect('PorTRaIt')).toEqual((2 / 3));
|
||||
});
|
||||
|
||||
test('"backdrop" (case insensitive)', () => {
|
||||
expect(cardBuilderUtils.getDesiredAspect('backdrop')).toEqual((16 / 9));
|
||||
expect(cardBuilderUtils.getDesiredAspect('BaCkDroP')).toEqual((16 / 9));
|
||||
});
|
||||
|
||||
test('"square" (case insensitive)', () => {
|
||||
expect(cardBuilderUtils.getDesiredAspect('square')).toEqual(1);
|
||||
expect(cardBuilderUtils.getDesiredAspect('sQuArE')).toEqual(1);
|
||||
});
|
||||
|
||||
test('"banner" (case insensitive)', () => {
|
||||
expect(cardBuilderUtils.getDesiredAspect('banner')).toEqual((1000 / 185));
|
||||
expect(cardBuilderUtils.getDesiredAspect('BaNnEr')).toEqual((1000 / 185));
|
||||
});
|
||||
|
||||
test('invalid shape', () => {
|
||||
expect(cardBuilderUtils.getDesiredAspect('invalid')).toBeNull();
|
||||
});
|
||||
|
||||
test('shape is not provided', () => {
|
||||
expect(cardBuilderUtils.getDesiredAspect('')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPostersPerRow', () => {
|
||||
test('resolves to default of 4 posters per row if shape is not provided', () => {
|
||||
expect(cardBuilderUtils.getPostersPerRow('', 0, false, false)).toEqual(4);
|
||||
});
|
||||
|
||||
describe('portrait', () => {
|
||||
const postersPerRowForPortrait = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('portrait', screenWidth, false, isTV));
|
||||
|
||||
test('television', () => {
|
||||
expect(postersPerRowForPortrait(0, true)).toEqual(100 / 16.66666667);
|
||||
});
|
||||
|
||||
test('screen width less than 500px', () => {
|
||||
expect(postersPerRowForPortrait(100, false)).toEqual(100 / 33.33333333);
|
||||
expect(postersPerRowForPortrait(499, false)).toEqual(100 / 33.33333333);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 500px', () => {
|
||||
expect(postersPerRowForPortrait(500, false)).toEqual(100 / 33.33333333);
|
||||
expect(postersPerRowForPortrait(501, false)).toEqual(100 / 33.33333333);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 700px', () => {
|
||||
expect(postersPerRowForPortrait(700, false)).toEqual(4);
|
||||
expect(postersPerRowForPortrait(701, false)).toEqual(4);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 800px', () => {
|
||||
expect(postersPerRowForPortrait(800, false)).toEqual(5);
|
||||
expect(postersPerRowForPortrait(801, false)).toEqual(5);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1200px', () => {
|
||||
expect(postersPerRowForPortrait(1200, false)).toEqual(100 / 16.66666667);
|
||||
expect(postersPerRowForPortrait(1201, false)).toEqual(100 / 16.66666667);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1400px', () => {
|
||||
expect(postersPerRowForPortrait(1400, false)).toEqual( 100 / 14.28571428571);
|
||||
expect(postersPerRowForPortrait(1401, false)).toEqual( 100 / 14.28571428571);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1600px', () => {
|
||||
expect(postersPerRowForPortrait(1600, false)).toEqual( 8);
|
||||
expect(postersPerRowForPortrait(1601, false)).toEqual( 8);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1920px', () => {
|
||||
expect(postersPerRowForPortrait(1920, false)).toEqual( 100 / 11.1111111111);
|
||||
expect(postersPerRowForPortrait(1921, false)).toEqual( 100 / 11.1111111111);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 2200px', () => {
|
||||
expect(postersPerRowForPortrait(2200, false)).toEqual( 10);
|
||||
expect(postersPerRowForPortrait(2201, false)).toEqual( 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('square', () => {
|
||||
const postersPerRowForSquare = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('square', screenWidth, false, isTV));
|
||||
|
||||
test('television', () => {
|
||||
expect(postersPerRowForSquare(0, true)).toEqual(100 / 16.66666667);
|
||||
});
|
||||
|
||||
test('screen width less than 500px', () => {
|
||||
expect(postersPerRowForSquare(100, false)).toEqual(2);
|
||||
expect(postersPerRowForSquare(499, false)).toEqual(2);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 500px', () => {
|
||||
expect(postersPerRowForSquare(500, false)).toEqual(100 / 33.33333333);
|
||||
expect(postersPerRowForSquare(501, false)).toEqual(100 / 33.33333333);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 700px', () => {
|
||||
expect(postersPerRowForSquare(700, false)).toEqual(4);
|
||||
expect(postersPerRowForSquare(701, false)).toEqual(4);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 800px', () => {
|
||||
expect(postersPerRowForSquare(800, false)).toEqual(5);
|
||||
expect(postersPerRowForSquare(801, false)).toEqual(5);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1200px', () => {
|
||||
expect(postersPerRowForSquare(1200, false)).toEqual(100 / 16.66666667);
|
||||
expect(postersPerRowForSquare(1201, false)).toEqual(100 / 16.66666667);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1400px', () => {
|
||||
expect(postersPerRowForSquare(1400, false)).toEqual( 100 / 14.28571428571);
|
||||
expect(postersPerRowForSquare(1401, false)).toEqual( 100 / 14.28571428571);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1600px', () => {
|
||||
expect(postersPerRowForSquare(1600, false)).toEqual(8);
|
||||
expect(postersPerRowForSquare(1601, false)).toEqual(8);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1920px', () => {
|
||||
expect(postersPerRowForSquare(1920, false)).toEqual(100 / 11.1111111111);
|
||||
expect(postersPerRowForSquare(1921, false)).toEqual(100 / 11.1111111111);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 2200px', () => {
|
||||
expect(postersPerRowForSquare(2200, false)).toEqual( 10);
|
||||
expect(postersPerRowForSquare(2201, false)).toEqual( 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('banner', () => {
|
||||
const postersPerRowForBanner = (screenWidth) => (cardBuilderUtils.getPostersPerRow('banner', screenWidth, false, false));
|
||||
|
||||
test('screen width less than 800px', () => {
|
||||
expect(postersPerRowForBanner(799)).toEqual(1);
|
||||
});
|
||||
|
||||
test('screen width greater than or equal to 800px', () => {
|
||||
expect(postersPerRowForBanner(800)).toEqual(2);
|
||||
expect(postersPerRowForBanner(801)).toEqual(2);
|
||||
});
|
||||
|
||||
test('screen width greater than or equal to 1200px', () => {
|
||||
expect(postersPerRowForBanner(1200)).toEqual(100 / 33.33333333);
|
||||
expect(postersPerRowForBanner(1201)).toEqual(100 / 33.33333333);
|
||||
});
|
||||
|
||||
test('screen width greater than or equal to 2200px', () => {
|
||||
expect(postersPerRowForBanner(2200)).toEqual(4);
|
||||
expect(postersPerRowForBanner(2201)).toEqual(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('backdrop', () => {
|
||||
const postersPerRowForBackdrop = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('backdrop', screenWidth, false, isTV));
|
||||
|
||||
test('television', () => {
|
||||
expect(postersPerRowForBackdrop(0, true)).toEqual(4);
|
||||
});
|
||||
|
||||
test('screen width less than 420px', () => {
|
||||
expect(postersPerRowForBackdrop(100, false)).toEqual(1);
|
||||
expect(postersPerRowForBackdrop(419, false)).toEqual(1);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 420px', () => {
|
||||
expect(postersPerRowForBackdrop(420, false)).toEqual(2);
|
||||
expect(postersPerRowForBackdrop(421, false)).toEqual(2);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 770px', () => {
|
||||
expect(postersPerRowForBackdrop(770, false)).toEqual(3);
|
||||
expect(postersPerRowForBackdrop(771, false)).toEqual(3);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1200px', () => {
|
||||
expect(postersPerRowForBackdrop(1200, false)).toEqual(4);
|
||||
expect(postersPerRowForBackdrop(1201, false)).toEqual(4);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1600px', () => {
|
||||
expect(postersPerRowForBackdrop(1600, false)).toEqual(5);
|
||||
expect(postersPerRowForBackdrop(1601, false)).toEqual(5);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 2500px', () => {
|
||||
expect(postersPerRowForBackdrop(2500, false)).toEqual(6);
|
||||
expect(postersPerRowForBackdrop(2501, false)).toEqual(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('small backdrop', () => {
|
||||
const postersPerRowForSmallBackdrop = (screenWidth) => (cardBuilderUtils.getPostersPerRow('smallBackdrop', screenWidth, false, false));
|
||||
|
||||
test('screen width less than 500px', () => {
|
||||
expect(postersPerRowForSmallBackdrop(100)).toEqual(2);
|
||||
expect(postersPerRowForSmallBackdrop(499)).toEqual(2);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 500px', () => {
|
||||
expect(postersPerRowForSmallBackdrop(500)).toEqual(100 / 33.33333333);
|
||||
expect(postersPerRowForSmallBackdrop(501)).toEqual(100 / 33.33333333);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 800px', () => {
|
||||
expect(postersPerRowForSmallBackdrop(800)).toEqual(4);
|
||||
expect(postersPerRowForSmallBackdrop(801)).toEqual(4);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1000px', () => {
|
||||
expect(postersPerRowForSmallBackdrop(1000)).toEqual(5);
|
||||
expect(postersPerRowForSmallBackdrop(1001)).toEqual(5);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1200px', () => {
|
||||
expect(postersPerRowForSmallBackdrop(1200)).toEqual(100 / 16.66666667);
|
||||
expect(postersPerRowForSmallBackdrop(1201)).toEqual(100 / 16.66666667);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1400px', () => {
|
||||
expect(postersPerRowForSmallBackdrop(1400)).toEqual(100 / 14.2857142857);
|
||||
expect(postersPerRowForSmallBackdrop(1401)).toEqual(100 / 14.2857142857);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1600px', () => {
|
||||
expect(postersPerRowForSmallBackdrop(1600)).toEqual(8);
|
||||
expect(postersPerRowForSmallBackdrop(1601)).toEqual(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe('overflow small backdrop', () => {
|
||||
const postersPerRowForOverflowSmallBackdrop = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowSmallBackdrop', screenWidth, isLandscape, isTV));
|
||||
|
||||
test('television', () => {
|
||||
expect(postersPerRowForOverflowSmallBackdrop(0, false, true)).toEqual( 100 / 18.9);
|
||||
});
|
||||
|
||||
describe('non-landscape', () => {
|
||||
test('screen width greater or equal to 540px', () => {
|
||||
expect(postersPerRowForOverflowSmallBackdrop(540, false)).toEqual(100 / 30);
|
||||
expect(postersPerRowForOverflowSmallBackdrop(541, false)).toEqual(100 / 30);
|
||||
});
|
||||
|
||||
test('screen width is less than 540px', () => {
|
||||
expect(postersPerRowForOverflowSmallBackdrop(539, false)).toEqual(100 / 72);
|
||||
expect(postersPerRowForOverflowSmallBackdrop(100, false)).toEqual(100 / 72);
|
||||
});
|
||||
});
|
||||
|
||||
describe('landscape', () => {
|
||||
test('screen width greater or equal to 800px', () => {
|
||||
expect(postersPerRowForOverflowSmallBackdrop(800, true)).toEqual(100 / 15.5);
|
||||
expect(postersPerRowForOverflowSmallBackdrop(801, true)).toEqual(100 / 15.5);
|
||||
});
|
||||
|
||||
test('screen width is less than 800px', () => {
|
||||
expect(postersPerRowForOverflowSmallBackdrop(799, true)).toEqual(100 / 23.3);
|
||||
expect(postersPerRowForOverflowSmallBackdrop(100, true)).toEqual(100 / 23.3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('overflow portrait', () => {
|
||||
const postersPerRowForOverflowPortrait = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowPortrait', screenWidth, isLandscape, isTV));
|
||||
|
||||
test('television', () => {
|
||||
expect(postersPerRowForOverflowPortrait(0, false, true)).toEqual( 100 / 15.5);
|
||||
});
|
||||
|
||||
describe('non-landscape', () => {
|
||||
test('screen width greater or equal to 1400px', () => {
|
||||
expect(postersPerRowForOverflowPortrait(1400, false)).toEqual(100 / 15);
|
||||
expect(postersPerRowForOverflowPortrait(1401, false)).toEqual(100 / 15);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1200px', () => {
|
||||
expect(postersPerRowForOverflowPortrait(1200, false)).toEqual(100 / 18);
|
||||
expect(postersPerRowForOverflowPortrait(1201, false)).toEqual(100 / 18);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 760px', () => {
|
||||
expect(postersPerRowForOverflowPortrait(760, false)).toEqual(100 / 23);
|
||||
expect(postersPerRowForOverflowPortrait(761, false)).toEqual(100 / 23);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 400px', () => {
|
||||
expect(postersPerRowForOverflowPortrait(400, false)).toEqual(100 / 31.5);
|
||||
expect(postersPerRowForOverflowPortrait(401, false)).toEqual(100 / 31.5);
|
||||
});
|
||||
|
||||
test('screen width is less than 400px', () => {
|
||||
expect(postersPerRowForOverflowPortrait(399, false)).toEqual(100 / 42);
|
||||
expect(postersPerRowForOverflowPortrait(100, false)).toEqual(100 / 42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('landscape', () => {
|
||||
test('screen width greater or equal to 1700px', () => {
|
||||
expect(postersPerRowForOverflowPortrait(1700, true)).toEqual(100 / 11.6);
|
||||
expect(postersPerRowForOverflowPortrait(1701, true)).toEqual(100 / 11.6);
|
||||
});
|
||||
|
||||
test('screen width is less than 1700px', () => {
|
||||
expect(postersPerRowForOverflowPortrait(1699, true)).toEqual(100 / 15.5);
|
||||
expect(postersPerRowForOverflowPortrait(100, true)).toEqual(100 / 15.5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('overflow square', () => {
|
||||
const postersPerRowForOverflowSquare = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowSquare', screenWidth, isLandscape, isTV));
|
||||
|
||||
test('television', () => {
|
||||
expect(postersPerRowForOverflowSquare(0, false, true)).toEqual( 100 / 15.5);
|
||||
});
|
||||
|
||||
describe('non-landscape', () => {
|
||||
test('screen width greater or equal to 1400px', () => {
|
||||
expect(postersPerRowForOverflowSquare(1400, false)).toEqual(100 / 15);
|
||||
expect(postersPerRowForOverflowSquare(1401, false)).toEqual(100 / 15);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1200px', () => {
|
||||
expect(postersPerRowForOverflowSquare(1200, false)).toEqual(100 / 18);
|
||||
expect(postersPerRowForOverflowSquare(1201, false)).toEqual(100 / 18);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 760px', () => {
|
||||
expect(postersPerRowForOverflowSquare(760, false)).toEqual(100 / 23);
|
||||
expect(postersPerRowForOverflowSquare(761, false)).toEqual(100 / 23);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 540px', () => {
|
||||
expect(postersPerRowForOverflowSquare(540, false)).toEqual(100 / 31.5);
|
||||
expect(postersPerRowForOverflowSquare(541, false)).toEqual(100 / 31.5);
|
||||
});
|
||||
|
||||
test('screen width is less than 540px', () => {
|
||||
expect(postersPerRowForOverflowSquare(539, false)).toEqual(100 / 42);
|
||||
expect(postersPerRowForOverflowSquare(100, false)).toEqual(100 / 42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('landscape', () => {
|
||||
test('screen width greater or equal to 1700px', () => {
|
||||
expect(postersPerRowForOverflowSquare(1700, true)).toEqual(100 / 11.6);
|
||||
expect(postersPerRowForOverflowSquare(1701, true)).toEqual(100 / 11.6);
|
||||
});
|
||||
|
||||
test('screen width is less than 1700px', () => {
|
||||
expect(postersPerRowForOverflowSquare(1699, true)).toEqual(100 / 15.5);
|
||||
expect(postersPerRowForOverflowSquare(100, true)).toEqual(100 / 15.5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('overflow backdrop', () => {
|
||||
const postersPerRowForOverflowBackdrop = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowBackdrop', screenWidth, isLandscape, isTV));
|
||||
|
||||
test('television', () => {
|
||||
expect(postersPerRowForOverflowBackdrop(0, false, true)).toEqual( 100 / 23.3);
|
||||
});
|
||||
|
||||
describe('non-landscape', () => {
|
||||
test('screen width greater or equal to 1800px', () => {
|
||||
expect(postersPerRowForOverflowBackdrop(1800, false)).toEqual(100 / 23.5);
|
||||
expect(postersPerRowForOverflowBackdrop(1801, false)).toEqual(100 / 23.5);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 1400px', () => {
|
||||
expect(postersPerRowForOverflowBackdrop(1400, false)).toEqual(100 / 30);
|
||||
expect(postersPerRowForOverflowBackdrop(1401, false)).toEqual(100 / 30);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 760px', () => {
|
||||
expect(postersPerRowForOverflowBackdrop(760, false)).toEqual(100 / 40);
|
||||
expect(postersPerRowForOverflowBackdrop(761, false)).toEqual(100 / 40);
|
||||
});
|
||||
|
||||
test('screen width greater or equal to 640px', () => {
|
||||
expect(postersPerRowForOverflowBackdrop(640, false)).toEqual(100 / 56);
|
||||
expect(postersPerRowForOverflowBackdrop(641, false)).toEqual(100 / 56);
|
||||
});
|
||||
|
||||
test('screen width is less than 640px', () => {
|
||||
expect(postersPerRowForOverflowBackdrop(639, false)).toEqual(100 / 72);
|
||||
expect(postersPerRowForOverflowBackdrop(100, false)).toEqual(100 / 72);
|
||||
});
|
||||
});
|
||||
|
||||
describe('landscape', () => {
|
||||
test('screen width greater or equal to 1700px', () => {
|
||||
expect(postersPerRowForOverflowBackdrop(1700, true)).toEqual(100 / 18.5);
|
||||
expect(postersPerRowForOverflowBackdrop(1701, true)).toEqual(100 / 18.5);
|
||||
});
|
||||
|
||||
test('screen width is less than 1700px', () => {
|
||||
expect(postersPerRowForOverflowBackdrop(1699, true)).toEqual(100 / 23.3);
|
||||
expect(postersPerRowForOverflowBackdrop(100, true)).toEqual(100 / 23.3);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -10,28 +10,28 @@ const createLinkElement = (activeTab: string) => ({
|
|||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'useredit' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('useredit.html', true);">
|
||||
onclick="Dashboard.navigate('/dashboard/users/profile', true);">
|
||||
${globalize.translate('Profile')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userlibraryaccess' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('userlibraryaccess.html', true);">
|
||||
onclick="Dashboard.navigate('/dashboard/users/access', true);">
|
||||
${globalize.translate('TabAccess')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userparentalcontrol' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('userparentalcontrol.html', true);">
|
||||
onclick="Dashboard.navigate('/dashboard/users/parentalcontrol', true);">
|
||||
${globalize.translate('TabParentalControl')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userpassword' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('userpassword.html', true);">
|
||||
onclick="Dashboard.navigate('/dashboard/users/password', true);">
|
||||
${globalize.translate('HeaderPassword')}
|
||||
</a>`
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl
|
|||
__html: `<a
|
||||
is="emby-linkbutton"
|
||||
class="cardContent"
|
||||
href="#/useredit.html?userId=${user.Id}"
|
||||
href="#/dashboard/users/profile?userId=${user.Id}"
|
||||
>
|
||||
${renderImgUrl}
|
||||
</a>`
|
||||
|
|
|
@ -36,7 +36,7 @@ function loadScreensavers(context, userSettings) {
|
|||
const selectScreensaver = context.querySelector('.selectScreensaver');
|
||||
const options = pluginManager.ofType(PluginType.Screensaver).map(plugin => {
|
||||
return {
|
||||
name: plugin.name,
|
||||
name: globalize.translate(plugin.name),
|
||||
value: plugin.id
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,50 +1,42 @@
|
|||
import loading from './loading/loading';
|
||||
import cardBuilder from './cardbuilder/cardBuilder';
|
||||
import dom from '../scripts/dom';
|
||||
import dom from 'scripts/dom';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
|
||||
import { getParameterByName } from 'utils/url';
|
||||
|
||||
import { appHost } from './apphost';
|
||||
import cardBuilder from './cardbuilder/cardBuilder';
|
||||
import imageLoader from './images/imageLoader';
|
||||
import globalize from '../scripts/globalize';
|
||||
import layoutManager from './layoutManager';
|
||||
import { getParameterByName } from '../utils/url.ts';
|
||||
import '../styles/scrollstyles.scss';
|
||||
import '../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import loading from './loading/loading';
|
||||
|
||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
||||
import 'styles/scrollstyles.scss';
|
||||
|
||||
function enableScrollX() {
|
||||
return !layoutManager.desktop;
|
||||
}
|
||||
|
||||
function getThumbShape() {
|
||||
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
|
||||
}
|
||||
|
||||
function getPosterShape() {
|
||||
return enableScrollX() ? 'overflowPortrait' : 'portrait';
|
||||
}
|
||||
|
||||
function getSquareShape() {
|
||||
return enableScrollX() ? 'overflowSquare' : 'square';
|
||||
}
|
||||
|
||||
function getSections() {
|
||||
return [{
|
||||
name: 'Movies',
|
||||
types: 'Movie',
|
||||
id: 'favoriteMovies',
|
||||
shape: getPosterShape(),
|
||||
shape: getPortraitShape(enableScrollX()),
|
||||
showTitle: false,
|
||||
overlayPlayButton: true
|
||||
}, {
|
||||
name: 'Shows',
|
||||
types: 'Series',
|
||||
id: 'favoriteShows',
|
||||
shape: getPosterShape(),
|
||||
shape: getPortraitShape(enableScrollX()),
|
||||
showTitle: false,
|
||||
overlayPlayButton: true
|
||||
}, {
|
||||
name: 'Episodes',
|
||||
types: 'Episode',
|
||||
id: 'favoriteEpisode',
|
||||
shape: getThumbShape(),
|
||||
shape: getBackdropShape(enableScrollX()),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
|
@ -55,7 +47,7 @@ function getSections() {
|
|||
name: 'Videos',
|
||||
types: 'Video,MusicVideo',
|
||||
id: 'favoriteVideos',
|
||||
shape: getThumbShape(),
|
||||
shape: getBackdropShape(enableScrollX()),
|
||||
preferThumb: true,
|
||||
showTitle: true,
|
||||
overlayPlayButton: true,
|
||||
|
@ -65,7 +57,7 @@ function getSections() {
|
|||
name: 'Artists',
|
||||
types: 'MusicArtist',
|
||||
id: 'favoriteArtists',
|
||||
shape: getSquareShape(),
|
||||
shape: getSquareShape(enableScrollX()),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
|
@ -77,7 +69,7 @@ function getSections() {
|
|||
name: 'Albums',
|
||||
types: 'MusicAlbum',
|
||||
id: 'favoriteAlbums',
|
||||
shape: getSquareShape(),
|
||||
shape: getSquareShape(enableScrollX()),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
|
@ -89,7 +81,7 @@ function getSections() {
|
|||
name: 'Songs',
|
||||
types: 'Audio',
|
||||
id: 'favoriteSongs',
|
||||
shape: getSquareShape(),
|
||||
shape: getSquareShape(enableScrollX()),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
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 layoutManager from '../layoutManager';
|
||||
import imageLoader from '../images/imageLoader';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import layoutManager from '../layoutManager';
|
||||
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 '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) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
|
@ -69,11 +74,14 @@ export function loadSections(elem, apiClient, user, userSettings) {
|
|||
promises.push(loadSection(elem, apiClient, user, userSettings, userViews, sections, i));
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(function () {
|
||||
return resume(elem, {
|
||||
refresh: true
|
||||
return Promise.all(promises)
|
||||
// Timeout for polyfilled CustomElements (webOS 1.2)
|
||||
.then(() => new Promise((resolve) => setTimeout(resolve, 0)))
|
||||
.then(() => {
|
||||
return resume(elem, {
|
||||
refresh: true
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
let noLibDescription;
|
||||
if (user.Policy?.IsAdministrator) {
|
||||
|
@ -91,7 +99,7 @@ export function loadSections(elem, apiClient, user, userSettings) {
|
|||
const createNowLink = elem.querySelector('#button-createLibrary');
|
||||
if (createNowLink) {
|
||||
createNowLink.addEventListener('click', function () {
|
||||
Dashboard.navigate('library.html');
|
||||
Dashboard.navigate('dashboard/libraries');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -166,18 +174,6 @@ function enableScrollX() {
|
|||
return true;
|
||||
}
|
||||
|
||||
function getSquareShape() {
|
||||
return enableScrollX() ? 'overflowSquare' : 'square';
|
||||
}
|
||||
|
||||
function getThumbShape() {
|
||||
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
|
||||
}
|
||||
|
||||
function getPortraitShape() {
|
||||
return enableScrollX() ? 'overflowPortrait' : 'portrait';
|
||||
}
|
||||
|
||||
function getLibraryButtonsHtml(items) {
|
||||
let html = '';
|
||||
|
||||
|
@ -241,11 +237,11 @@ function getLatestItemsHtmlFn(itemType, viewType) {
|
|||
const cardLayout = false;
|
||||
let shape;
|
||||
if (itemType === 'Channel' || viewType === 'movies' || viewType === 'books' || viewType === 'tvshows') {
|
||||
shape = getPortraitShape();
|
||||
shape = getPortraitShape(enableScrollX());
|
||||
} else if (viewType === 'music' || viewType === 'homevideos') {
|
||||
shape = getSquareShape();
|
||||
shape = getSquareShape(enableScrollX());
|
||||
} else {
|
||||
shape = getThumbShape();
|
||||
shape = getBackdropShape(enableScrollX());
|
||||
}
|
||||
|
||||
return cardBuilder.getCardsHtml({
|
||||
|
@ -342,7 +338,7 @@ export function loadLibraryTiles(elem, apiClient, user, userSettings, shape, use
|
|||
|
||||
html += cardBuilder.getCardsHtml({
|
||||
items: userViews,
|
||||
shape: getThumbShape(),
|
||||
shape: getBackdropShape(enableScrollX()),
|
||||
showTitle: true,
|
||||
centerText: true,
|
||||
overlayText: false,
|
||||
|
@ -420,7 +416,9 @@ function getItemsToResumeHtmlFn(useEpisodeImages, mediaType) {
|
|||
items: items,
|
||||
preferThumb: true,
|
||||
inheritThumb: !useEpisodeImages,
|
||||
shape: (mediaType === 'Book') ? getPortraitShape() : getThumbShape(),
|
||||
shape: (mediaType === 'Book') ?
|
||||
getPortraitShape(enableScrollX()) :
|
||||
getBackdropShape(enableScrollX()),
|
||||
overlayText: false,
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
|
@ -468,7 +466,7 @@ function getOnNowItemsHtml(items) {
|
|||
showChannelName: false,
|
||||
showAirDateTime: false,
|
||||
showAirEndTime: true,
|
||||
defaultShape: getThumbShape(),
|
||||
defaultShape: getBackdropShape(enableScrollX()),
|
||||
lines: 3,
|
||||
overlayPlayButton: true
|
||||
});
|
||||
|
@ -611,7 +609,7 @@ function getNextUpItemsHtmlFn(useEpisodeImages) {
|
|||
items: items,
|
||||
preferThumb: true,
|
||||
inheritThumb: !useEpisodeImages,
|
||||
shape: getThumbShape(),
|
||||
shape: getBackdropShape(enableScrollX()),
|
||||
overlayText: false,
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
|
|
|
@ -445,7 +445,7 @@ function executeCommand(item, id, options) {
|
|||
});
|
||||
break;
|
||||
case 'multiSelect':
|
||||
import('./multiSelect/multiSelect').then(({ startMultiSelect: startMultiSelect }) => {
|
||||
import('./multiSelect/multiSelect').then(({ startMultiSelect }) => {
|
||||
const card = dom.parentWithClass(options.positionTo, 'card');
|
||||
startMultiSelect(card);
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import loadable from '@loadable/component';
|
||||
import loadable, { LoadableComponent } from '@loadable/component';
|
||||
import React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
|
||||
|
@ -10,13 +10,18 @@ export enum AsyncRouteType {
|
|||
export interface AsyncRoute {
|
||||
/** The URL path for this route. */
|
||||
path: string
|
||||
/** The relative path to the page component in the routes directory. */
|
||||
page: string
|
||||
/** The route should use the page component from the experimental app. */
|
||||
/**
|
||||
* The relative path to the page component in the routes directory.
|
||||
* 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
|
||||
}
|
||||
|
||||
interface AsyncPageProps {
|
||||
export interface AsyncPageProps {
|
||||
/** The relative path to the page component in the routes directory. */
|
||||
page: string
|
||||
}
|
||||
|
@ -31,14 +36,19 @@ const StableAsyncPage = loadable(
|
|||
{ cacheKey: (props: AsyncPageProps) => props.page }
|
||||
);
|
||||
|
||||
export const toAsyncPageRoute = ({ path, page, type = AsyncRouteType.Stable }: AsyncRoute) => (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
element={(
|
||||
export const toAsyncPageRoute = ({ path, page, element, type = AsyncRouteType.Stable }: AsyncRoute) => {
|
||||
const Element = element
|
||||
|| (
|
||||
type === AsyncRouteType.Experimental ?
|
||||
<ExperimentalAsyncPage page={page} /> :
|
||||
<StableAsyncPage page={page} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
ExperimentalAsyncPage :
|
||||
StableAsyncPage
|
||||
);
|
||||
|
||||
return (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
element={<Element page={page ?? path} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
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') {
|
||||
return '#/dashboard.html';
|
||||
return '#/dashboard';
|
||||
}
|
||||
|
||||
if (item === 'recordedtv') {
|
||||
|
|
|
@ -46,15 +46,8 @@ function init(instance) {
|
|||
if (inputOffset) {
|
||||
inputOffset = inputOffset[0];
|
||||
inputOffset = parseFloat(inputOffset);
|
||||
inputOffset = Math.min(30, Math.max(-30, inputOffset));
|
||||
|
||||
// replace current text by considered offset
|
||||
this.textContent = inputOffset + 's';
|
||||
// set new offset
|
||||
playbackManager.setSubtitleOffset(inputOffset, player);
|
||||
// synchronize with slider value
|
||||
subtitleSyncSlider.updateOffset(
|
||||
getSliderValueFromOffset(inputOffset));
|
||||
subtitleSyncSlider.updateOffset(inputOffset);
|
||||
} else {
|
||||
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) {
|
||||
// default value is 0s = 0ms
|
||||
this.value = sliderValue === undefined ? 0 : sliderValue;
|
||||
|
||||
updateSubtitleOffset();
|
||||
};
|
||||
|
||||
subtitleSyncSlider.addEventListener('change', function () {
|
||||
// set new offset
|
||||
playbackManager.setSubtitleOffset(getOffsetFromSliderValue(this.value), player);
|
||||
// synchronize with textField value
|
||||
subtitleSyncTextField.updateOffset(
|
||||
getOffsetFromSliderValue(this.value));
|
||||
});
|
||||
subtitleSyncSlider.addEventListener('change', () => updateSubtitleOffset());
|
||||
|
||||
subtitleSyncSlider.getBubbleHtml = function (value) {
|
||||
const newOffset = getOffsetFromPercentage(value);
|
||||
subtitleSyncSlider.getBubbleHtml = function (_, value) {
|
||||
return '<h1 class="sliderBubbleText">'
|
||||
+ (newOffset > 0 ? '+' : '') + parseFloat(newOffset) + 's'
|
||||
+ (value > 0 ? '+' : '') + parseFloat(value) + 's'
|
||||
+ '</h1>';
|
||||
};
|
||||
|
||||
|
@ -107,25 +103,6 @@ function init(instance) {
|
|||
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 {
|
||||
constructor(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>
|
||||
<div class="subtitleSyncTextField" contenteditable="true" spellcheck="false">0s</div>
|
||||
<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>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
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 ServerConnections from '../ServerConnections';
|
||||
import dom from '../../scripts/dom';
|
||||
|
@ -13,6 +15,7 @@ import '../../elements/emby-button/emby-button';
|
|||
import '../../elements/emby-select/emby-select';
|
||||
import '../formdialog.scss';
|
||||
import './style.scss';
|
||||
import { readFileAsBase64 } from 'utils/file';
|
||||
|
||||
let currentItemId;
|
||||
let currentServerId;
|
||||
|
@ -75,7 +78,7 @@ function setFiles(page, files) {
|
|||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
async function onSubmit(e) {
|
||||
const file = currentFile;
|
||||
|
||||
if (!isValidSubtitleFile(file)) {
|
||||
|
@ -89,8 +92,17 @@ function onSubmit(e) {
|
|||
const dlg = dom.parentWithClass(this, 'dialog');
|
||||
const language = dlg.querySelector('#selectLanguage').value;
|
||||
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 = '';
|
||||
loading.hide();
|
||||
hasChanges = true;
|
||||
|
|
|
@ -31,6 +31,10 @@
|
|||
<input type="checkbox" is="emby-checkbox" id="chkIsForced" />
|
||||
<span>${LabelIsForced}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkIsHearingImpaired" />
|
||||
<span>${LabelIsHearingImpaired}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="selectContainer flex-grow">
|
||||
<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
|
||||
key='admin-dashboard-link'
|
||||
component={Link}
|
||||
to='/dashboard.html'
|
||||
to='/dashboard'
|
||||
onClick={onMenuClose}
|
||||
>
|
||||
|
||||
|
@ -127,7 +127,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
|
|||
<MenuItem
|
||||
key='admin-metadata-link'
|
||||
component={Link}
|
||||
to='/edititemmetadata.html'
|
||||
to='/metadata'
|
||||
onClick={onMenuClose}
|
||||
>
|
||||
<ListItemIcon>
|
||||
|
@ -140,7 +140,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
|
|||
<Divider />
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to='/mypreferencesquickconnect.html'
|
||||
to='/quickconnect'
|
||||
onClick={onMenuClose}
|
||||
>
|
||||
<ListItemIcon>
|
|
@ -2,11 +2,11 @@ import IconButton from '@mui/material/IconButton';
|
|||
import Tooltip from '@mui/material/Tooltip';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import UserAvatar from 'components/UserAvatar';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import AppUserMenu, { ID } from './menus/AppUserMenu';
|
||||
import UserAvatar from 'components/UserAvatar';
|
||||
import AppUserMenu, { ID } from './AppUserMenu';
|
||||
|
||||
const UserMenuButton = () => {
|
||||
const { user } = useApi();
|
|
@ -3,11 +3,7 @@ import './viewManager/viewContainer.scss';
|
|||
import Dashboard from '../utils/dashboard';
|
||||
|
||||
const getMainAnimatedPages = () => {
|
||||
if (!mainAnimatedPages) {
|
||||
mainAnimatedPages = document.querySelector('.mainAnimatedPages');
|
||||
}
|
||||
|
||||
return mainAnimatedPages;
|
||||
return document.querySelector('.mainAnimatedPages');
|
||||
};
|
||||
|
||||
function setControllerClass(view, options) {
|
||||
|
@ -61,7 +57,9 @@ export function loadView(options) {
|
|||
|
||||
view.classList.add('mainAnimatedPage');
|
||||
|
||||
if (!getMainAnimatedPages()) {
|
||||
const mainAnimatedPages = getMainAnimatedPages();
|
||||
|
||||
if (!mainAnimatedPages) {
|
||||
console.warn('[viewContainer] main animated pages element is not present');
|
||||
return;
|
||||
}
|
||||
|
@ -187,6 +185,7 @@ export function setOnBeforeChange(fn) {
|
|||
}
|
||||
|
||||
export function tryRestoreView(options) {
|
||||
console.debug('[viewContainer] tryRestoreView', options);
|
||||
const url = options.url;
|
||||
const index = currentUrls.indexOf(url);
|
||||
|
||||
|
@ -232,14 +231,15 @@ function triggerDestroy(view) {
|
|||
}
|
||||
|
||||
export function reset() {
|
||||
console.debug('[viewContainer] resetting view cache');
|
||||
allPages = [];
|
||||
currentUrls = [];
|
||||
const mainAnimatedPages = getMainAnimatedPages();
|
||||
if (mainAnimatedPages) mainAnimatedPages.innerHTML = '';
|
||||
selectedPageIndex = -1;
|
||||
}
|
||||
|
||||
let onBeforeChange;
|
||||
let mainAnimatedPages;
|
||||
let allPages = [];
|
||||
let currentUrls = [];
|
||||
const pageContainerCount = 3;
|
||||
|
@ -248,8 +248,8 @@ reset();
|
|||
getMainAnimatedPages()?.classList.remove('hide');
|
||||
|
||||
export default {
|
||||
loadView: loadView,
|
||||
tryRestoreView: tryRestoreView,
|
||||
reset: reset,
|
||||
setOnBeforeChange: setOnBeforeChange
|
||||
loadView,
|
||||
tryRestoreView,
|
||||
reset,
|
||||
setOnBeforeChange
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="dashboardSections" style="padding-top:.5em;">
|
||||
<div class="dashboardColumn dashboardColumn-2-60 dashboardColumn-3-46">
|
||||
<div class="dashboardSection">
|
||||
<a is="emby-linkbutton" href="#/dashboardgeneral.html" class="button-flat sectionTitleTextButton">
|
||||
<a is="emby-linkbutton" href="#/dashboard/settings" class="button-flat sectionTitleTextButton">
|
||||
<h3>${TabServer}</h3>
|
||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
|
||||
<div class="dashboardSection">
|
||||
<a is="emby-linkbutton" href="#/devices.html" class="button-flat sectionTitleTextButton">
|
||||
<a is="emby-linkbutton" href="#/dashboard/devices" class="button-flat sectionTitleTextButton">
|
||||
<h3>${HeaderActiveDevices}</h3>
|
||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
@ -70,7 +70,7 @@
|
|||
</div>
|
||||
|
||||
<div class="dashboardSection">
|
||||
<a is="emby-linkbutton" href="#/dashboardgeneral.html" class="button-flat sectionTitleTextButton">
|
||||
<a is="emby-linkbutton" href="#/dashboard/settings" class="button-flat sectionTitleTextButton">
|
||||
<h3>${HeaderPaths}</h3>
|
||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
|
|
@ -73,7 +73,7 @@ function showDeviceMenu(view, btn, deviceId) {
|
|||
callback: function (id) {
|
||||
switch (id) {
|
||||
case 'open':
|
||||
Dashboard.navigate('device.html?id=' + deviceId);
|
||||
Dashboard.navigate('dashboard/devices/edit?id=' + deviceId);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
|
@ -94,7 +94,7 @@ function load(page, devices) {
|
|||
deviceHtml += '<div class="cardBox visualCardBox">';
|
||||
deviceHtml += '<div class="cardScalable">';
|
||||
deviceHtml += '<div class="cardPadder cardPadder-backdrop"></div>';
|
||||
deviceHtml += `<a is="emby-linkbutton" href="#!/device.html?id=${escapeHtml(device.Id)}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`;
|
||||
deviceHtml += `<a is="emby-linkbutton" href="#/dashboard/devices/edit?id=${escapeHtml(device.Id)}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`;
|
||||
// audit note: getDeviceIcon returns static text
|
||||
const iconUrl = imageHelper.getDeviceIcon(device);
|
||||
|
||||
|
|
|
@ -264,7 +264,7 @@
|
|||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
<button is="emby-button" type="button" class="button-cancel raised block" onclick="Dashboard.navigate('dlnaprofiles.html');">
|
||||
<button is="emby-button" type="button" class="button-cancel raised block" onclick="Dashboard.navigate('dashboard/dlna/profiles');">
|
||||
<span>${ButtonCancel}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -639,7 +639,7 @@ function saveProfile(page, profile) {
|
|||
data: JSON.stringify(profile),
|
||||
contentType: 'application/json'
|
||||
}).then(function () {
|
||||
Dashboard.navigate('dlnaprofiles.html');
|
||||
Dashboard.navigate('dashboard/dlna/profiles');
|
||||
}, Dashboard.processErrorResponse);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${HeaderCustomDlnaProfiles}</h2>
|
||||
<a is="emby-linkbutton" href="#/dlnaprofile.html" class="fab submit" style="margin:0 0 0 1em">
|
||||
<a is="emby-linkbutton" href="#/dashboard/dlna/profiles/edit" class="fab submit" style="margin:0 0 0 1em">
|
||||
<span class="material-icons add" aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
|
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