From cad0f1672e14a79427bdc64ee2f76c9787a2fdee Mon Sep 17 00:00:00 2001 From: Venson Date: Thu, 28 Sep 2023 21:24:59 +0300 Subject: [PATCH 01/70] Added github container definition --- .devcontainer/devcontainer.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..78b93eaf3c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "Node.js & TypeScript", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/typescript-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": [80] + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "yarn install", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} From 4265e5075ea74d4647ca03f328a581b583f29e23 Mon Sep 17 00:00:00 2001 From: Venson Date: Thu, 28 Sep 2023 21:36:24 +0300 Subject: [PATCH 02/70] Changed used image --- .devcontainer/devcontainer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 78b93eaf3c..666be2b07d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,15 +1,15 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node { - "name": "Node.js & TypeScript", + "name": "Node.js", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye", + "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": [80] + // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "yarn install", From 8bf82b5192be8f72cda9abfd9e8ecea393edecd6 Mon Sep 17 00:00:00 2001 From: Venson Date: Thu, 28 Sep 2023 22:11:46 +0300 Subject: [PATCH 03/70] Test vmn install 20 --- .devcontainer/devcontainer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 666be2b07d..8a6442296b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,7 @@ { "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" + "image": "mcr.microsoft.com/devcontainers/javascript-node:1-20-bullseye", // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, @@ -12,7 +12,7 @@ // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "yarn install", + "postCreateCommand": "nvm install 20" // Configure tool-specific properties. // "customizations": {}, From 46ab95df31bea7d1a44e2036cb13e2d97c82b283 Mon Sep 17 00:00:00 2001 From: Venson Date: Thu, 28 Sep 2023 22:15:51 +0300 Subject: [PATCH 04/70] I hate this i hate this i hate this --- .devcontainer/devcontainer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8a6442296b..4f66115614 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,7 +12,8 @@ // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "nvm install 20" + //https://github.com/microsoft/vscode-dev-containers/issues/559 + "postCreateCommand": "source $NVM_DIR/nvm.sh && nvm install 20" // Configure tool-specific properties. // "customizations": {}, From c0d14715fb07d430bca57ed880b6163ba2f391d7 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 20 Sep 2023 00:02:26 -0400 Subject: [PATCH 05/70] Migrate dashboard to separate app --- src/RootApp.tsx | 3 + src/apps/dashboard/App.tsx | 47 ++++++++++++++ src/apps/dashboard/AppLayout.tsx | 30 +++++++++ src/apps/dashboard/routes/_asyncRoutes.ts | 12 ++++ .../routes/_legacyRoutes.ts} | 62 +++++++++---------- src/apps/dashboard/routes/_redirects.ts | 51 +++++++++++++++ src/apps/experimental/App.tsx | 20 +++--- .../components/drawers/AppDrawer.tsx | 8 +-- .../experimental/routes/asyncRoutes/admin.ts | 12 ---- .../experimental/routes/asyncRoutes/index.ts | 1 - .../experimental/routes/legacyRoutes/index.ts | 1 - src/apps/stable/App.tsx | 20 +++--- src/apps/stable/routes/_redirects.ts | 3 +- src/apps/stable/routes/asyncRoutes/index.ts | 1 - src/apps/stable/routes/legacyRoutes/index.ts | 1 - 15 files changed, 190 insertions(+), 82 deletions(-) create mode 100644 src/apps/dashboard/App.tsx create mode 100644 src/apps/dashboard/AppLayout.tsx create mode 100644 src/apps/dashboard/routes/_asyncRoutes.ts rename src/apps/{experimental/routes/legacyRoutes/admin.ts => dashboard/routes/_legacyRoutes.ts} (76%) create mode 100644 src/apps/dashboard/routes/_redirects.ts delete mode 100644 src/apps/experimental/routes/asyncRoutes/admin.ts diff --git a/src/RootApp.tsx b/src/RootApp.tsx index 0d66c15ad2..956e57b1bb 100644 --- a/src/RootApp.tsx +++ b/src/RootApp.tsx @@ -12,6 +12,7 @@ import { ApiProvider } from 'hooks/useApi'; import { WebConfigProvider } from 'hooks/useWebConfig'; import theme from 'themes/theme'; +const DashboardApp = loadable(() => import('./apps/dashboard/App')); const ExperimentalApp = loadable(() => import('./apps/experimental/App')); const StableApp = loadable(() => import('./apps/stable/App')); @@ -31,6 +32,8 @@ const RootAppLayout = () => { : } + + ); }; diff --git a/src/apps/dashboard/App.tsx b/src/apps/dashboard/App.tsx new file mode 100644 index 0000000000..a5f09d97a8 --- /dev/null +++ b/src/apps/dashboard/App.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { Route, Routes } from 'react-router-dom'; + +import ConnectionRequired from 'components/ConnectionRequired'; +import { toViewManagerPageRoute } from 'components/router/LegacyRoute'; +import { toAsyncPageRoute } from 'components/router/AsyncRoute'; +import { toRedirectRoute } from 'components/router/Redirect'; +import ServerContentPage from 'components/ServerContentPage'; + +import { REDIRECTS } from './routes/_redirects'; +import { ASYNC_ADMIN_ROUTES } from './routes/_asyncRoutes'; +import { LEGACY_ADMIN_ROUTES } from './routes/_legacyRoutes'; +import AppLayout from './AppLayout'; + +const DashboardApp = () => ( + + }> + }> + + {ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)} + {LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)} + + + {/* TODO: Should the metadata manager be a separate app? */} + {toViewManagerPageRoute({ + path: 'metadata', + pageProps: { + controller: 'edititemmetadata', + view: 'edititemmetadata.html' + } + })} + + + } /> + + {/* Suppress warnings for unhandled routes */} + + + + + {/* Redirects for old paths */} + {REDIRECTS.map(toRedirectRoute)} + +); + +export default DashboardApp; diff --git a/src/apps/dashboard/AppLayout.tsx b/src/apps/dashboard/AppLayout.tsx new file mode 100644 index 0000000000..1e427d3c6e --- /dev/null +++ b/src/apps/dashboard/AppLayout.tsx @@ -0,0 +1,30 @@ +import { ThemeProvider } from '@mui/material/styles'; +import React from 'react'; +import { Outlet } from 'react-router-dom'; + +import AppHeader from 'components/AppHeader'; +import Backdrop from 'components/Backdrop'; +import theme from 'themes/theme'; + +const AppLayout = () => { + return ( + + + +
+ {/* + * 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. + */} + +
+ +
+
+ +
+ + ); +}; + +export default AppLayout; diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts new file mode 100644 index 0000000000..4dc364beb7 --- /dev/null +++ b/src/apps/dashboard/routes/_asyncRoutes.ts @@ -0,0 +1,12 @@ +import type { AsyncRoute } from 'components/router/AsyncRoute'; + +export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ + { path: 'activity', page: 'dashboard/activity' }, + { path: 'notifications', page: 'dashboard/notifications' }, + { path: 'users/new', page: 'user/usernew' }, + { path: 'users', page: 'user/userprofiles' }, + { path: 'users/profile', page: 'user/useredit' }, + { path: 'users/access', page: 'user/userlibraryaccess' }, + { path: 'users/parentalcontrol', page: 'user/userparentalcontrol' }, + { path: 'users/password', page: 'user/userpassword' } +]; diff --git a/src/apps/experimental/routes/legacyRoutes/admin.ts b/src/apps/dashboard/routes/_legacyRoutes.ts similarity index 76% rename from src/apps/experimental/routes/legacyRoutes/admin.ts rename to src/apps/dashboard/routes/_legacyRoutes.ts index 35a3976445..efdd543a42 100644 --- a/src/apps/experimental/routes/legacyRoutes/admin.ts +++ b/src/apps/dashboard/routes/_legacyRoutes.ts @@ -1,170 +1,164 @@ -import { LegacyRoute } from '../../../../components/router/LegacyRoute'; +import type { LegacyRoute } from 'components/router/LegacyRoute'; export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ { - path: 'dashboard.html', + path: '/dashboard', pageProps: { controller: 'dashboard/dashboard', view: 'dashboard/dashboard.html' } }, { - path: 'dashboardgeneral.html', + path: 'settings', pageProps: { controller: 'dashboard/general', view: 'dashboard/general.html' } }, { - path: 'networking.html', + path: 'networking', pageProps: { controller: 'dashboard/networking', view: 'dashboard/networking.html' } }, { - path: 'devices.html', + path: 'devices', pageProps: { controller: 'dashboard/devices/devices', view: 'dashboard/devices/devices.html' } }, { - path: 'device.html', + path: 'devices/edit', pageProps: { controller: 'dashboard/devices/device', view: 'dashboard/devices/device.html' } }, { - path: 'dlnaprofile.html', + path: 'dlna/profiles/edit', pageProps: { controller: 'dashboard/dlna/profile', view: 'dashboard/dlna/profile.html' } }, { - path: 'dlnaprofiles.html', + path: 'dlna/profiles', pageProps: { controller: 'dashboard/dlna/profiles', view: 'dashboard/dlna/profiles.html' } }, { - path: 'dlnasettings.html', + path: 'dlna', pageProps: { controller: 'dashboard/dlna/settings', view: 'dashboard/dlna/settings.html' } }, { - path: 'addplugin.html', + path: 'plugins/add', pageProps: { controller: 'dashboard/plugins/add/index', view: 'dashboard/plugins/add/index.html' } }, { - path: 'library.html', + path: 'libraries', pageProps: { controller: 'dashboard/library', view: 'dashboard/library.html' } }, { - path: 'librarydisplay.html', + path: 'libraries/display', pageProps: { controller: 'dashboard/librarydisplay', view: 'dashboard/librarydisplay.html' } }, { - path: 'edititemmetadata.html', - pageProps: { - controller: 'edititemmetadata', - view: 'edititemmetadata.html' - } - }, { - path: 'encodingsettings.html', + path: 'playback/transcoding', pageProps: { controller: 'dashboard/encodingsettings', view: 'dashboard/encodingsettings.html' } }, { - path: 'log.html', + path: 'logs', pageProps: { controller: 'dashboard/logs', view: 'dashboard/logs.html' } }, { - path: 'metadataimages.html', + path: 'libraries/metadata', pageProps: { controller: 'dashboard/metadataImages', view: 'dashboard/metadataimages.html' } }, { - path: 'metadatanfo.html', + path: 'libraries/nfo', pageProps: { controller: 'dashboard/metadatanfo', view: 'dashboard/metadatanfo.html' } }, { - path: 'playbackconfiguration.html', + path: 'playback/resume', pageProps: { controller: 'dashboard/playback', view: 'dashboard/playback.html' } }, { - path: 'availableplugins.html', + path: 'plugins/catalog', pageProps: { controller: 'dashboard/plugins/available/index', view: 'dashboard/plugins/available/index.html' } }, { - path: 'repositories.html', + path: 'plugins/repositories', pageProps: { controller: 'dashboard/plugins/repositories/index', view: 'dashboard/plugins/repositories/index.html' } }, { - path: 'livetvguideprovider.html', + path: 'livetv/guide', pageProps: { controller: 'livetvguideprovider', view: 'livetvguideprovider.html' } }, { - path: 'livetvsettings.html', + path: 'recordings', pageProps: { controller: 'livetvsettings', view: 'livetvsettings.html' } }, { - path: 'livetvstatus.html', + path: 'livetv', pageProps: { controller: 'livetvstatus', view: 'livetvstatus.html' } }, { - path: 'livetvtuner.html', + path: 'livetv/tuner', pageProps: { controller: 'livetvtuner', view: 'livetvtuner.html' } }, { - path: 'installedplugins.html', + path: 'plugins', pageProps: { controller: 'dashboard/plugins/installed/index', view: 'dashboard/plugins/installed/index.html' } }, { - path: 'scheduledtask.html', + path: 'tasks/edit', pageProps: { controller: 'dashboard/scheduledtasks/scheduledtask', view: 'dashboard/scheduledtasks/scheduledtask.html' } }, { - path: 'scheduledtasks.html', + path: 'tasks', pageProps: { controller: 'dashboard/scheduledtasks/scheduledtasks', view: 'dashboard/scheduledtasks/scheduledtasks.html' } }, { - path: 'apikeys.html', + path: 'keys', pageProps: { controller: 'dashboard/apikeys', view: 'dashboard/apikeys.html' } }, { - path: 'streamingsettings.html', + path: 'playback/streaming', pageProps: { view: 'dashboard/streaming.html', controller: 'dashboard/streaming' diff --git a/src/apps/dashboard/routes/_redirects.ts b/src/apps/dashboard/routes/_redirects.ts new file mode 100644 index 0000000000..a2786fe533 --- /dev/null +++ b/src/apps/dashboard/routes/_redirects.ts @@ -0,0 +1,51 @@ +import type { Redirect } from 'components/router/Redirect'; + +export const REDIRECTS: Redirect[] = [ + // FIXME: URL params are not included in redirects + { 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' }, + // FIXME: URL params are not included in redirects + { from: 'device.html', to: '/dashboard/devices/edit' }, + { from: 'devices.html', to: '/dashboard/devices' }, + // FIXME: URL params are not included in redirects + { 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' }, + // FIXME: URL params are not included in redirects + { from: 'livetvguideprovider.html', to: '/dashboard/livetv/guide' }, + { from: 'livetvsettings.html', to: '/dashboard/recordings' }, + { from: 'livetvstatus.html', to: '/dashboard/livetv' }, + // FIXME: URL params are not included in redirects + { 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: 'quickConnect.html', to: '/dashboard/quickconnect' }, + { from: 'repositories.html', to: '/dashboard/plugins/repositories' }, + // FIXME: URL params are not included in redirects + { 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: 'usernew.html', to: '/dashboard/users/add' }, + { from: 'userprofiles.html', to: '/dashboard/users' }, + // FIXME: URL params are not included in redirects + { from: 'useredit.html', to: '/dashboard/users/profile' }, + // FIXME: URL params are not included in redirects + { from: 'userlibraryaccess.html', to: '/dashboard/users/access' }, + // FIXME: URL params are not included in redirects + { from: 'userparentalcontrol.html', to: '/dashboard/users/parentalcontrol' }, + // FIXME: URL params are not included in redirects + { from: 'userpassword.html', to: '/dashboard/users/password' } +]; diff --git a/src/apps/experimental/App.tsx b/src/apps/experimental/App.tsx index 44c6d24b2d..a4f67e7957 100644 --- a/src/apps/experimental/App.tsx +++ b/src/apps/experimental/App.tsx @@ -3,14 +3,13 @@ import { Navigate, Route, Routes } from 'react-router-dom'; import { REDIRECTS } from 'apps/stable/routes/_redirects'; import ConnectionRequired from 'components/ConnectionRequired'; -import ServerContentPage from 'components/ServerContentPage'; import { toAsyncPageRoute } from 'components/router/AsyncRoute'; import { toViewManagerPageRoute } from 'components/router/LegacyRoute'; import { toRedirectRoute } from 'components/router/Redirect'; import AppLayout from './AppLayout'; -import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes'; -import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; +import { ASYNC_USER_ROUTES } from './routes/asyncRoutes'; +import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; const ExperimentalApp = () => { return ( @@ -22,16 +21,6 @@ const ExperimentalApp = () => { {LEGACY_USER_ROUTES.map(toViewManagerPageRoute)} - {/* Admin routes */} - }> - {ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)} - {LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)} - - - } /> - - {/* Public routes */} }> } /> @@ -42,6 +31,11 @@ const ExperimentalApp = () => { {/* Redirects for old paths */} {REDIRECTS.map(toRedirectRoute)} + + {/* Ignore dashboard routes */} + + + ); }; diff --git a/src/apps/experimental/components/drawers/AppDrawer.tsx b/src/apps/experimental/components/drawers/AppDrawer.tsx index c414e6ba78..06d8a49175 100644 --- a/src/apps/experimental/components/drawers/AppDrawer.tsx +++ b/src/apps/experimental/components/drawers/AppDrawer.tsx @@ -3,8 +3,8 @@ import { Route, Routes, useLocation } from 'react-router-dom'; import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer'; -import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from '../../routes/asyncRoutes'; -import { LEGACY_ADMIN_ROUTES, LEGACY_USER_ROUTES } from '../../routes/legacyRoutes'; +import { ASYNC_USER_ROUTES } from '../../routes/asyncRoutes'; +import { LEGACY_USER_ROUTES } from '../../routes/legacyRoutes'; import AdvancedDrawerSection from './dashboard/AdvancedDrawerSection'; import DevicesDrawerSection from './dashboard/DevicesDrawerSection'; @@ -27,8 +27,8 @@ const MAIN_DRAWER_ROUTES = [ ].filter(route => !DRAWERLESS_ROUTES.includes(route.path)); const ADMIN_DRAWER_ROUTES = [ - ...ASYNC_ADMIN_ROUTES, - ...LEGACY_ADMIN_ROUTES, + // ...ASYNC_ADMIN_ROUTES, + // ...LEGACY_ADMIN_ROUTES, { path: '/configurationpage' } // Plugin configuration page ].filter(route => !DRAWERLESS_ROUTES.includes(route.path)); diff --git a/src/apps/experimental/routes/asyncRoutes/admin.ts b/src/apps/experimental/routes/asyncRoutes/admin.ts deleted file mode 100644 index 7e8c0eca16..0000000000 --- a/src/apps/experimental/routes/asyncRoutes/admin.ts +++ /dev/null @@ -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' } -]; diff --git a/src/apps/experimental/routes/asyncRoutes/index.ts b/src/apps/experimental/routes/asyncRoutes/index.ts index 9dd4fb3c99..e5abc85650 100644 --- a/src/apps/experimental/routes/asyncRoutes/index.ts +++ b/src/apps/experimental/routes/asyncRoutes/index.ts @@ -1,2 +1 @@ -export * from './admin'; export * from './user'; diff --git a/src/apps/experimental/routes/legacyRoutes/index.ts b/src/apps/experimental/routes/legacyRoutes/index.ts index 2931c568e8..bc46c94c54 100644 --- a/src/apps/experimental/routes/legacyRoutes/index.ts +++ b/src/apps/experimental/routes/legacyRoutes/index.ts @@ -1,3 +1,2 @@ -export * from './admin'; export * from './public'; export * from './user'; diff --git a/src/apps/stable/App.tsx b/src/apps/stable/App.tsx index 8285cbc9ec..de6cdddb90 100644 --- a/src/apps/stable/App.tsx +++ b/src/apps/stable/App.tsx @@ -2,13 +2,12 @@ import React from 'react'; import { Navigate, Outlet, Route, Routes } from 'react-router-dom'; import AppBody from 'components/AppBody'; -import ServerContentPage from 'components/ServerContentPage'; import ConnectionRequired from 'components/ConnectionRequired'; import { toAsyncPageRoute } from 'components/router/AsyncRoute'; import { toViewManagerPageRoute } from 'components/router/LegacyRoute'; -import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes'; -import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; +import { ASYNC_USER_ROUTES } from './routes/asyncRoutes'; +import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; import { REDIRECTS } from './routes/_redirects'; import { toRedirectRoute } from 'components/router/Redirect'; @@ -27,16 +26,6 @@ const StableApp = () => ( {LEGACY_USER_ROUTES.map(toViewManagerPageRoute)} - {/* Admin routes */} - }> - {ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)} - {LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)} - - - } /> - - {/* Public routes */} }> } /> @@ -44,6 +33,11 @@ const StableApp = () => ( {LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)} + {/* Ignore dashboard routes */} + + + + {/* Suppress warnings for unhandled routes */} diff --git a/src/apps/stable/routes/_redirects.ts b/src/apps/stable/routes/_redirects.ts index fb24865d84..d48c48d995 100644 --- a/src/apps/stable/routes/_redirects.ts +++ b/src/apps/stable/routes/_redirects.ts @@ -1,6 +1,5 @@ import type { Redirect } from 'components/router/Redirect'; export const REDIRECTS: Redirect[] = [ - { from: 'mypreferencesquickconnect.html', to: '/quickconnect' }, - { from: 'serveractivity.html', to: '/dashboard/activity' } + { from: 'mypreferencesquickconnect.html', to: '/quickconnect' } ]; diff --git a/src/apps/stable/routes/asyncRoutes/index.ts b/src/apps/stable/routes/asyncRoutes/index.ts index 9dd4fb3c99..e5abc85650 100644 --- a/src/apps/stable/routes/asyncRoutes/index.ts +++ b/src/apps/stable/routes/asyncRoutes/index.ts @@ -1,2 +1 @@ -export * from './admin'; export * from './user'; diff --git a/src/apps/stable/routes/legacyRoutes/index.ts b/src/apps/stable/routes/legacyRoutes/index.ts index 2931c568e8..bc46c94c54 100644 --- a/src/apps/stable/routes/legacyRoutes/index.ts +++ b/src/apps/stable/routes/legacyRoutes/index.ts @@ -1,3 +1,2 @@ -export * from './admin'; export * from './public'; export * from './user'; From bd1ae96b62f7ba1b50f1a2951d1975d076c9e411 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 20 Sep 2023 16:25:11 -0400 Subject: [PATCH 06/70] Move routes to dashboard app --- src/apps/dashboard/App.tsx | 17 +- .../components/activityTable/LogLevelChip.tsx | 0 .../components/activityTable/OverviewCell.tsx | 0 .../dataGrid}/GridActionsCellLink.tsx | 0 src/apps/dashboard/routes/_asyncRoutes.ts | 16 +- src/apps/dashboard/routes/_redirects.ts | 16 +- .../routes}/activity.tsx | 6 +- .../routes}/notifications.tsx | 0 .../routes/users/access.tsx} | 0 .../routes/users/index.tsx} | 0 .../routes/users/new.tsx} | 0 .../routes/users/parentalcontrol.tsx} | 0 .../routes/users/password.tsx} | 0 .../routes/users/profile.tsx} | 0 .../components/drawers/AppDrawer.tsx | 2 - src/apps/stable/routes/asyncRoutes/admin.ts | 11 -- src/apps/stable/routes/legacyRoutes/admin.ts | 179 ------------------ 17 files changed, 29 insertions(+), 218 deletions(-) rename src/apps/{experimental => dashboard}/components/activityTable/LogLevelChip.tsx (100%) rename src/apps/{experimental => dashboard}/components/activityTable/OverviewCell.tsx (100%) rename src/apps/{experimental/components => dashboard/components/dataGrid}/GridActionsCellLink.tsx (100%) rename src/apps/{experimental/routes/dashboard => dashboard/routes}/activity.tsx (97%) rename src/apps/{stable/routes/dashboard => dashboard/routes}/notifications.tsx (100%) rename src/apps/{stable/routes/user/userlibraryaccess.tsx => dashboard/routes/users/access.tsx} (100%) rename src/apps/{stable/routes/user/userprofiles.tsx => dashboard/routes/users/index.tsx} (100%) rename src/apps/{stable/routes/user/usernew.tsx => dashboard/routes/users/new.tsx} (100%) rename src/apps/{stable/routes/user/userparentalcontrol.tsx => dashboard/routes/users/parentalcontrol.tsx} (100%) rename src/apps/{stable/routes/user/userpassword.tsx => dashboard/routes/users/password.tsx} (100%) rename src/apps/{stable/routes/user/useredit.tsx => dashboard/routes/users/profile.tsx} (100%) delete mode 100644 src/apps/stable/routes/asyncRoutes/admin.ts delete mode 100644 src/apps/stable/routes/legacyRoutes/admin.ts diff --git a/src/apps/dashboard/App.tsx b/src/apps/dashboard/App.tsx index a5f09d97a8..232c8e77cb 100644 --- a/src/apps/dashboard/App.tsx +++ b/src/apps/dashboard/App.tsx @@ -1,9 +1,10 @@ +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 { toAsyncPageRoute } from 'components/router/AsyncRoute'; +import { AsyncPageProps, AsyncRoute, toAsyncPageRoute } from 'components/router/AsyncRoute'; import { toRedirectRoute } from 'components/router/Redirect'; import ServerContentPage from 'components/ServerContentPage'; @@ -12,12 +13,24 @@ import { ASYNC_ADMIN_ROUTES } from './routes/_asyncRoutes'; import { LEGACY_ADMIN_ROUTES } from './routes/_legacyRoutes'; import AppLayout from './AppLayout'; +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 + }) +); + const DashboardApp = () => ( }> }> - {ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)} + {ASYNC_ADMIN_ROUTES.map(toDashboardAsyncPageRoute)} {LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)} diff --git a/src/apps/experimental/components/activityTable/LogLevelChip.tsx b/src/apps/dashboard/components/activityTable/LogLevelChip.tsx similarity index 100% rename from src/apps/experimental/components/activityTable/LogLevelChip.tsx rename to src/apps/dashboard/components/activityTable/LogLevelChip.tsx diff --git a/src/apps/experimental/components/activityTable/OverviewCell.tsx b/src/apps/dashboard/components/activityTable/OverviewCell.tsx similarity index 100% rename from src/apps/experimental/components/activityTable/OverviewCell.tsx rename to src/apps/dashboard/components/activityTable/OverviewCell.tsx diff --git a/src/apps/experimental/components/GridActionsCellLink.tsx b/src/apps/dashboard/components/dataGrid/GridActionsCellLink.tsx similarity index 100% rename from src/apps/experimental/components/GridActionsCellLink.tsx rename to src/apps/dashboard/components/dataGrid/GridActionsCellLink.tsx diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts index 4dc364beb7..8cae668770 100644 --- a/src/apps/dashboard/routes/_asyncRoutes.ts +++ b/src/apps/dashboard/routes/_asyncRoutes.ts @@ -1,12 +1,12 @@ import type { AsyncRoute } from 'components/router/AsyncRoute'; export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ - { path: 'activity', page: 'dashboard/activity' }, - { path: 'notifications', page: 'dashboard/notifications' }, - { path: 'users/new', page: 'user/usernew' }, - { path: 'users', page: 'user/userprofiles' }, - { path: 'users/profile', page: 'user/useredit' }, - { path: 'users/access', page: 'user/userlibraryaccess' }, - { path: 'users/parentalcontrol', page: 'user/userparentalcontrol' }, - { path: 'users/password', page: 'user/userpassword' } + { path: 'activity' }, + { path: 'notifications' }, + { path: 'users/new' }, + { path: 'users' }, + { path: 'users/profile' }, + { path: 'users/access' }, + { path: 'users/parentalcontrol' }, + { path: 'users/password' } ]; diff --git a/src/apps/dashboard/routes/_redirects.ts b/src/apps/dashboard/routes/_redirects.ts index a2786fe533..2191a3fafb 100644 --- a/src/apps/dashboard/routes/_redirects.ts +++ b/src/apps/dashboard/routes/_redirects.ts @@ -1,16 +1,13 @@ import type { Redirect } from 'components/router/Redirect'; export const REDIRECTS: Redirect[] = [ - // FIXME: URL params are not included in redirects { 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' }, - // FIXME: URL params are not included in redirects { from: 'device.html', to: '/dashboard/devices/edit' }, { from: 'devices.html', to: '/dashboard/devices' }, - // FIXME: URL params are not included in redirects { from: 'dlnaprofile.html', to: '/dashboard/dlna/profiles/edit' }, { from: 'dlnaprofiles.html', to: '/dashboard/dlna/profiles' }, { from: 'dlnasettings.html', to: '/dashboard/dlna' }, @@ -19,11 +16,9 @@ export const REDIRECTS: Redirect[] = [ { from: 'installedplugins.html', to: '/dashboard/plugins' }, { from: 'library.html', to: '/dashboard/libraries' }, { from: 'librarydisplay.html', to: '/dashboard/libraries/display' }, - // FIXME: URL params are not included in redirects { from: 'livetvguideprovider.html', to: '/dashboard/livetv/guide' }, { from: 'livetvsettings.html', to: '/dashboard/recordings' }, { from: 'livetvstatus.html', to: '/dashboard/livetv' }, - // FIXME: URL params are not included in redirects { from: 'livetvtuner.html', to: '/dashboard/livetv/tuner' }, { from: 'log.html', to: '/dashboard/logs' }, { from: 'metadataimages.html', to: '/dashboard/libraries/metadata' }, @@ -33,19 +28,14 @@ export const REDIRECTS: Redirect[] = [ { from: 'playbackconfiguration.html', to: '/dashboard/playback/resume' }, { from: 'quickConnect.html', to: '/dashboard/quickconnect' }, { from: 'repositories.html', to: '/dashboard/plugins/repositories' }, - // FIXME: URL params are not included in redirects { 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: 'usernew.html', to: '/dashboard/users/add' }, - { from: 'userprofiles.html', to: '/dashboard/users' }, - // FIXME: URL params are not included in redirects { from: 'useredit.html', to: '/dashboard/users/profile' }, - // FIXME: URL params are not included in redirects { from: 'userlibraryaccess.html', to: '/dashboard/users/access' }, - // FIXME: URL params are not included in redirects + { from: 'usernew.html', to: '/dashboard/users/add' }, { from: 'userparentalcontrol.html', to: '/dashboard/users/parentalcontrol' }, - // FIXME: URL params are not included in redirects - { from: 'userpassword.html', to: '/dashboard/users/password' } + { from: 'userpassword.html', to: '/dashboard/users/password' }, + { from: 'userprofiles.html', to: '/dashboard/users' } ]; diff --git a/src/apps/experimental/routes/dashboard/activity.tsx b/src/apps/dashboard/routes/activity.tsx similarity index 97% rename from src/apps/experimental/routes/dashboard/activity.tsx rename to src/apps/dashboard/routes/activity.tsx index f007e104d3..125140ca09 100644 --- a/src/apps/experimental/routes/dashboard/activity.tsx +++ b/src/apps/dashboard/routes/activity.tsx @@ -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'; diff --git a/src/apps/stable/routes/dashboard/notifications.tsx b/src/apps/dashboard/routes/notifications.tsx similarity index 100% rename from src/apps/stable/routes/dashboard/notifications.tsx rename to src/apps/dashboard/routes/notifications.tsx diff --git a/src/apps/stable/routes/user/userlibraryaccess.tsx b/src/apps/dashboard/routes/users/access.tsx similarity index 100% rename from src/apps/stable/routes/user/userlibraryaccess.tsx rename to src/apps/dashboard/routes/users/access.tsx diff --git a/src/apps/stable/routes/user/userprofiles.tsx b/src/apps/dashboard/routes/users/index.tsx similarity index 100% rename from src/apps/stable/routes/user/userprofiles.tsx rename to src/apps/dashboard/routes/users/index.tsx diff --git a/src/apps/stable/routes/user/usernew.tsx b/src/apps/dashboard/routes/users/new.tsx similarity index 100% rename from src/apps/stable/routes/user/usernew.tsx rename to src/apps/dashboard/routes/users/new.tsx diff --git a/src/apps/stable/routes/user/userparentalcontrol.tsx b/src/apps/dashboard/routes/users/parentalcontrol.tsx similarity index 100% rename from src/apps/stable/routes/user/userparentalcontrol.tsx rename to src/apps/dashboard/routes/users/parentalcontrol.tsx diff --git a/src/apps/stable/routes/user/userpassword.tsx b/src/apps/dashboard/routes/users/password.tsx similarity index 100% rename from src/apps/stable/routes/user/userpassword.tsx rename to src/apps/dashboard/routes/users/password.tsx diff --git a/src/apps/stable/routes/user/useredit.tsx b/src/apps/dashboard/routes/users/profile.tsx similarity index 100% rename from src/apps/stable/routes/user/useredit.tsx rename to src/apps/dashboard/routes/users/profile.tsx diff --git a/src/apps/experimental/components/drawers/AppDrawer.tsx b/src/apps/experimental/components/drawers/AppDrawer.tsx index 06d8a49175..7e4abf33ca 100644 --- a/src/apps/experimental/components/drawers/AppDrawer.tsx +++ b/src/apps/experimental/components/drawers/AppDrawer.tsx @@ -27,8 +27,6 @@ const MAIN_DRAWER_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)); diff --git a/src/apps/stable/routes/asyncRoutes/admin.ts b/src/apps/stable/routes/asyncRoutes/admin.ts deleted file mode 100644 index 72bcc6f32b..0000000000 --- a/src/apps/stable/routes/asyncRoutes/admin.ts +++ /dev/null @@ -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' } -]; diff --git a/src/apps/stable/routes/legacyRoutes/admin.ts b/src/apps/stable/routes/legacyRoutes/admin.ts deleted file mode 100644 index fd430feeba..0000000000 --- a/src/apps/stable/routes/legacyRoutes/admin.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { LegacyRoute } from '../../../../components/router/LegacyRoute'; - -export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ - { - path: 'dashboard.html', - pageProps: { - controller: 'dashboard/dashboard', - view: 'dashboard/dashboard.html' - } - }, { - path: 'dashboardgeneral.html', - pageProps: { - controller: 'dashboard/general', - view: 'dashboard/general.html' - } - }, { - path: 'networking.html', - pageProps: { - controller: 'dashboard/networking', - view: 'dashboard/networking.html' - } - }, { - path: 'devices.html', - pageProps: { - controller: 'dashboard/devices/devices', - view: 'dashboard/devices/devices.html' - } - }, { - path: 'device.html', - pageProps: { - controller: 'dashboard/devices/device', - view: 'dashboard/devices/device.html' - } - }, { - path: 'dlnaprofile.html', - pageProps: { - controller: 'dashboard/dlna/profile', - view: 'dashboard/dlna/profile.html' - } - }, { - path: 'dlnaprofiles.html', - pageProps: { - controller: 'dashboard/dlna/profiles', - view: 'dashboard/dlna/profiles.html' - } - }, { - path: 'dlnasettings.html', - pageProps: { - controller: 'dashboard/dlna/settings', - view: 'dashboard/dlna/settings.html' - } - }, { - path: 'addplugin.html', - pageProps: { - controller: 'dashboard/plugins/add/index', - view: 'dashboard/plugins/add/index.html' - } - }, { - path: 'library.html', - pageProps: { - controller: 'dashboard/library', - view: 'dashboard/library.html' - } - }, { - path: 'librarydisplay.html', - pageProps: { - controller: 'dashboard/librarydisplay', - view: 'dashboard/librarydisplay.html' - } - }, { - path: 'edititemmetadata.html', - pageProps: { - controller: 'edititemmetadata', - view: 'edititemmetadata.html' - } - }, { - path: 'encodingsettings.html', - pageProps: { - controller: 'dashboard/encodingsettings', - view: 'dashboard/encodingsettings.html' - } - }, { - path: 'log.html', - pageProps: { - controller: 'dashboard/logs', - view: 'dashboard/logs.html' - } - }, { - path: 'metadataimages.html', - pageProps: { - controller: 'dashboard/metadataImages', - view: 'dashboard/metadataimages.html' - } - }, { - path: 'metadatanfo.html', - pageProps: { - controller: 'dashboard/metadatanfo', - view: 'dashboard/metadatanfo.html' - } - }, { - path: 'playbackconfiguration.html', - pageProps: { - controller: 'dashboard/playback', - view: 'dashboard/playback.html' - } - }, { - path: 'availableplugins.html', - pageProps: { - controller: 'dashboard/plugins/available/index', - view: 'dashboard/plugins/available/index.html' - } - }, { - path: 'repositories.html', - pageProps: { - controller: 'dashboard/plugins/repositories/index', - view: 'dashboard/plugins/repositories/index.html' - } - }, { - path: 'livetvguideprovider.html', - pageProps: { - controller: 'livetvguideprovider', - view: 'livetvguideprovider.html' - } - }, { - path: 'livetvsettings.html', - pageProps: { - controller: 'livetvsettings', - view: 'livetvsettings.html' - } - }, { - path: 'livetvstatus.html', - pageProps: { - controller: 'livetvstatus', - view: 'livetvstatus.html' - } - }, { - path: 'livetvtuner.html', - pageProps: { - controller: 'livetvtuner', - view: 'livetvtuner.html' - } - }, { - path: 'installedplugins.html', - pageProps: { - controller: 'dashboard/plugins/installed/index', - view: 'dashboard/plugins/installed/index.html' - } - }, { - path: 'scheduledtask.html', - pageProps: { - controller: 'dashboard/scheduledtasks/scheduledtask', - view: 'dashboard/scheduledtasks/scheduledtask.html' - } - }, { - path: 'scheduledtasks.html', - pageProps: { - controller: 'dashboard/scheduledtasks/scheduledtasks', - view: 'dashboard/scheduledtasks/scheduledtasks.html' - } - }, { - path: 'dashboard/activity', - pageProps: { - controller: 'dashboard/serveractivity', - view: 'dashboard/serveractivity.html' - } - }, { - path: 'apikeys.html', - pageProps: { - controller: 'dashboard/apikeys', - view: 'dashboard/apikeys.html' - } - }, { - path: 'streamingsettings.html', - pageProps: { - view: 'dashboard/streaming.html', - controller: 'dashboard/streaming' - } - } -]; From 6101e04ca870822dd8276800a7932b3abec2a7fc Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 20 Sep 2023 17:34:48 -0400 Subject: [PATCH 07/70] Update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bcb0c53311..2516be0a25 100644 --- a/README.md +++ b/README.md @@ -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 From d5e703287a3408be5922927a54efa363fe3346bc Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Thu, 21 Sep 2023 16:40:08 -0400 Subject: [PATCH 08/70] Move shared components to common layout --- src/RootApp.tsx | 8 +++++++- src/apps/dashboard/App.tsx | 12 +++++++++--- src/apps/dashboard/AppLayout.tsx | 26 ++++++-------------------- src/apps/experimental/App.tsx | 11 ++++++++--- src/apps/stable/App.tsx | 13 +++++++++---- 5 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/RootApp.tsx b/src/RootApp.tsx index 956e57b1bb..26f12eb39b 100644 --- a/src/RootApp.tsx +++ b/src/RootApp.tsx @@ -11,6 +11,8 @@ import { HistoryRouter } from 'components/router/HistoryRouter'; import { ApiProvider } from 'hooks/useApi'; import { WebConfigProvider } from 'hooks/useWebConfig'; import theme from 'themes/theme'; +import { useLocation } from 'react-router-dom'; +import { DASHBOARD_APP_PATHS } from './apps/dashboard/App'; const DashboardApp = loadable(() => import('./apps/dashboard/App')); const ExperimentalApp = loadable(() => import('./apps/experimental/App')); @@ -22,10 +24,14 @@ 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 ( <> - + { isExperimentalLayout ? diff --git a/src/apps/dashboard/App.tsx b/src/apps/dashboard/App.tsx index 232c8e77cb..2127a807f7 100644 --- a/src/apps/dashboard/App.tsx +++ b/src/apps/dashboard/App.tsx @@ -25,25 +25,31 @@ const toDashboardAsyncPageRoute = (route: AsyncRoute) => ( }) ); +export const DASHBOARD_APP_PATHS = { + Dashboard: 'dashboard', + MetadataManager: 'metadata', + PluginConfig: 'configurationpage' +}; + const DashboardApp = () => ( }> }> - + {ASYNC_ADMIN_ROUTES.map(toDashboardAsyncPageRoute)} {LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)} {/* TODO: Should the metadata manager be a separate app? */} {toViewManagerPageRoute({ - path: 'metadata', + path: DASHBOARD_APP_PATHS.MetadataManager, pageProps: { controller: 'edititemmetadata', view: 'edititemmetadata.html' } })} - } /> diff --git a/src/apps/dashboard/AppLayout.tsx b/src/apps/dashboard/AppLayout.tsx index 1e427d3c6e..19fe09e1ba 100644 --- a/src/apps/dashboard/AppLayout.tsx +++ b/src/apps/dashboard/AppLayout.tsx @@ -1,29 +1,15 @@ -import { ThemeProvider } from '@mui/material/styles'; import React from 'react'; import { Outlet } from 'react-router-dom'; -import AppHeader from 'components/AppHeader'; -import Backdrop from 'components/Backdrop'; -import theme from 'themes/theme'; +import AppBody from 'components/AppBody'; + +import '../experimental/AppOverrides.scss'; const AppLayout = () => { return ( - - - -
- {/* - * 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. - */} - -
- -
-
- -
- + + + ); }; diff --git a/src/apps/experimental/App.tsx b/src/apps/experimental/App.tsx index a4f67e7957..d804352f94 100644 --- a/src/apps/experimental/App.tsx +++ b/src/apps/experimental/App.tsx @@ -10,6 +10,7 @@ import { toRedirectRoute } from 'components/router/Redirect'; import AppLayout from './AppLayout'; import { ASYNC_USER_ROUTES } from './routes/asyncRoutes'; import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; +import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App'; const ExperimentalApp = () => { return ( @@ -33,9 +34,13 @@ const ExperimentalApp = () => { {REDIRECTS.map(toRedirectRoute)} {/* Ignore dashboard routes */} - - - + {Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => ( + + ))} ); }; diff --git a/src/apps/stable/App.tsx b/src/apps/stable/App.tsx index de6cdddb90..f9103eb6ea 100644 --- a/src/apps/stable/App.tsx +++ b/src/apps/stable/App.tsx @@ -5,11 +5,12 @@ 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_USER_ROUTES } from './routes/asyncRoutes'; import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; import { REDIRECTS } from './routes/_redirects'; -import { toRedirectRoute } from 'components/router/Redirect'; +import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App'; const Layout = () => ( @@ -34,9 +35,13 @@ const StableApp = () => ( {/* Ignore dashboard routes */} - - - + {Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => ( + + ))} {/* Suppress warnings for unhandled routes */} From f28542fbfb332ff266aabdd3fb7416170b29b8ef Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Sun, 24 Sep 2023 02:42:29 -0400 Subject: [PATCH 09/70] Fix app body not being unloaded when leaving dashboard --- src/apps/dashboard/App.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/apps/dashboard/App.tsx b/src/apps/dashboard/App.tsx index 2127a807f7..db1776509a 100644 --- a/src/apps/dashboard/App.tsx +++ b/src/apps/dashboard/App.tsx @@ -52,10 +52,10 @@ const DashboardApp = () => ( } /> - - {/* Suppress warnings for unhandled routes */} - + + {/* Suppress warnings for unhandled routes */} + {/* Redirects for old paths */} From 73aa0f1962e867caccc91e849b1e5f0973aa2337 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Sun, 24 Sep 2023 03:40:16 -0400 Subject: [PATCH 10/70] Fix app body not being unloaded when leaving dashboard --- src/apps/stable/App.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/apps/stable/App.tsx b/src/apps/stable/App.tsx index f9103eb6ea..b0219312a0 100644 --- a/src/apps/stable/App.tsx +++ b/src/apps/stable/App.tsx @@ -34,21 +34,21 @@ const StableApp = () => ( {LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)} - {/* Ignore dashboard routes */} - {Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => ( - - ))} - {/* Suppress warnings for unhandled routes */} {/* Redirects for old paths */} {REDIRECTS.map(toRedirectRoute)} + + {/* Ignore dashboard routes */} + {Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => ( + + ))} ); From b5dcdbf4b44e507b678bcbc84bd61a5170c29ae4 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Mon, 25 Sep 2023 00:00:36 -0400 Subject: [PATCH 11/70] Update dashboard paths --- src/apps/dashboard/routes/_redirects.ts | 1 - src/apps/dashboard/routes/activity.tsx | 2 +- src/apps/dashboard/routes/notifications.tsx | 2 +- src/apps/dashboard/routes/users/index.tsx | 8 ++-- src/apps/dashboard/routes/users/new.tsx | 2 +- src/apps/dashboard/routes/users/profile.tsx | 2 +- .../components/drawers/AppDrawer.tsx | 2 +- .../components/drawers/MainDrawerContent.tsx | 4 +- .../dashboard/AdvancedDrawerSection.tsx | 26 ++++++------- .../dashboard/DevicesDrawerSection.tsx | 12 +++--- .../drawers/dashboard/LiveTvDrawerSection.tsx | 4 +- .../drawers/dashboard/ServerDrawerSection.tsx | 38 +++++++++---------- .../dashboard/users/SectionTabs.tsx | 8 ++-- .../dashboard/users/UserCardBox.tsx | 2 +- src/components/homesections/homesections.js | 2 +- src/components/router/appRouter.js | 2 +- src/components/toolbar/AppUserMenu.tsx | 4 +- src/controllers/dashboard/dashboard.html | 6 +-- src/controllers/dashboard/devices/devices.js | 4 +- src/controllers/dashboard/dlna/profile.html | 2 +- src/controllers/dashboard/dlna/profile.js | 2 +- src/controllers/dashboard/dlna/profiles.html | 2 +- src/controllers/dashboard/dlna/profiles.js | 6 +-- src/controllers/dashboard/dlna/settings.js | 4 +- src/controllers/dashboard/encodingsettings.js | 6 +-- src/controllers/dashboard/library.js | 8 ++-- src/controllers/dashboard/librarydisplay.js | 8 ++-- src/controllers/dashboard/metadataImages.js | 8 ++-- src/controllers/dashboard/metadatanfo.js | 8 ++-- src/controllers/dashboard/playback.js | 6 +-- .../dashboard/plugins/available/index.js | 8 ++-- .../dashboard/plugins/installed/index.js | 8 ++-- .../dashboard/plugins/repositories/index.js | 6 +-- .../scheduledtasks/scheduledtasks.js | 4 +- src/controllers/dashboard/streaming.js | 6 +-- src/controllers/livetvguideprovider.js | 2 +- src/controllers/livetvstatus.js | 10 ++--- src/controllers/livetvtuner.js | 2 +- src/controllers/user/menu/index.html | 4 +- src/scripts/libraryMenu.js | 34 ++++++++--------- 40 files changed, 137 insertions(+), 138 deletions(-) diff --git a/src/apps/dashboard/routes/_redirects.ts b/src/apps/dashboard/routes/_redirects.ts index 2191a3fafb..94211c79c2 100644 --- a/src/apps/dashboard/routes/_redirects.ts +++ b/src/apps/dashboard/routes/_redirects.ts @@ -26,7 +26,6 @@ export const REDIRECTS: Redirect[] = [ { from: 'networking.html', to: '/dashboard/networking' }, { from: 'notificationsettings.html', to: '/dashboard/notifications' }, { from: 'playbackconfiguration.html', to: '/dashboard/playback/resume' }, - { from: 'quickConnect.html', to: '/dashboard/quickconnect' }, { from: 'repositories.html', to: '/dashboard/plugins/repositories' }, { from: 'scheduledtask.html', to: '/dashboard/tasks/edit' }, { from: 'scheduledtasks.html', to: '/dashboard/tasks' }, diff --git a/src/apps/dashboard/routes/activity.tsx b/src/apps/dashboard/routes/activity.tsx index 125140ca09..fa3a9135bd 100644 --- a/src/apps/dashboard/routes/activity.tsx +++ b/src/apps/dashboard/routes/activity.tsx @@ -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}`} > diff --git a/src/apps/dashboard/routes/notifications.tsx b/src/apps/dashboard/routes/notifications.tsx index ca874d1333..6f673c753f 100644 --- a/src/apps/dashboard/routes/notifications.tsx +++ b/src/apps/dashboard/routes/notifications.tsx @@ -9,7 +9,7 @@ const PluginLink = () => ( __html: ` ${globalize.translate('GetThePlugin')} ` diff --git a/src/apps/dashboard/routes/users/index.tsx b/src/apps/dashboard/routes/users/index.tsx index dc8a6e86fd..6789a00ce8 100644 --- a/src/apps/dashboard/routes/users/index.tsx +++ b/src/apps/dashboard/routes/users/index.tsx @@ -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); }); diff --git a/src/apps/dashboard/routes/users/new.tsx b/src/apps/dashboard/routes/users/new.tsx index 22758500ea..116895e947 100644 --- a/src/apps/dashboard/routes/users/new.tsx +++ b/src/apps/dashboard/routes/users/new.tsx @@ -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); }); diff --git a/src/apps/dashboard/routes/users/profile.tsx b/src/apps/dashboard/routes/users/profile.tsx index c4acdfaaee..05d3b72cdf 100644 --- a/src/apps/dashboard/routes/users/profile.tsx +++ b/src/apps/dashboard/routes/users/profile.tsx @@ -32,7 +32,7 @@ const getCheckedElementDataIds = (elements: NodeListOf) => ( ); function onSaveComplete() { - Dashboard.navigate('userprofiles.html') + Dashboard.navigate('/dashboard/users') .catch(err => { console.error('[useredit] failed to navigate to user profile', err); }); diff --git a/src/apps/experimental/components/drawers/AppDrawer.tsx b/src/apps/experimental/components/drawers/AppDrawer.tsx index 7e4abf33ca..dc48221b8c 100644 --- a/src/apps/experimental/components/drawers/AppDrawer.tsx +++ b/src/apps/experimental/components/drawers/AppDrawer.tsx @@ -17,7 +17,7 @@ import { isTabPath } from '../tabs/tabRoutes'; export const DRAWER_WIDTH = 240; const DRAWERLESS_ROUTES = [ - 'edititemmetadata.html', // metadata manager + 'metadata', // metadata manager 'video' // video player ]; diff --git a/src/apps/experimental/components/drawers/MainDrawerContent.tsx b/src/apps/experimental/components/drawers/MainDrawerContent.tsx index 4d2a74b8a9..351076a025 100644 --- a/src/apps/experimental/components/drawers/MainDrawerContent.tsx +++ b/src/apps/experimental/components/drawers/MainDrawerContent.tsx @@ -150,7 +150,7 @@ const MainDrawerContent = () => { } > - + @@ -158,7 +158,7 @@ const MainDrawerContent = () => { - + diff --git a/src/apps/experimental/components/drawers/dashboard/AdvancedDrawerSection.tsx b/src/apps/experimental/components/drawers/dashboard/AdvancedDrawerSection.tsx index 5a74c68632..97993a76cc 100644 --- a/src/apps/experimental/components/drawers/dashboard/AdvancedDrawerSection.tsx +++ b/src/apps/experimental/components/drawers/dashboard/AdvancedDrawerSection.tsx @@ -19,10 +19,10 @@ import ListItemLink from 'components/ListItemLink'; import globalize from 'scripts/globalize'; const PLUGIN_PATHS = [ - '/installedplugins.html', - '/availableplugins.html', - '/repositories.html', - '/addplugin.html', + '/dashboard/plugins', + '/dashboard/plugins/catalog', + '/dashboard/plugins/repositories', + '/dashboard/plugins/add', '/configurationpage' ]; @@ -41,7 +41,7 @@ const AdvancedDrawerSection = () => { } > - + @@ -49,7 +49,7 @@ const AdvancedDrawerSection = () => { - + @@ -57,7 +57,7 @@ const AdvancedDrawerSection = () => { - +
@@ -65,7 +65,7 @@ const AdvancedDrawerSection = () => { - + @@ -73,7 +73,7 @@ const AdvancedDrawerSection = () => { - + @@ -83,19 +83,19 @@ const AdvancedDrawerSection = () => { - + - + - + - + diff --git a/src/apps/experimental/components/drawers/dashboard/DevicesDrawerSection.tsx b/src/apps/experimental/components/drawers/dashboard/DevicesDrawerSection.tsx index fe3ec09217..6cc7ab79fc 100644 --- a/src/apps/experimental/components/drawers/dashboard/DevicesDrawerSection.tsx +++ b/src/apps/experimental/components/drawers/dashboard/DevicesDrawerSection.tsx @@ -12,8 +12,8 @@ import ListItemLink from 'components/ListItemLink'; import globalize from 'scripts/globalize'; const DLNA_PATHS = [ - '/dlnasettings.html', - '/dlnaprofiles.html' + '/dashboard/dlna', + '/dashboard/dlna/profiles' ]; const DevicesDrawerSection = () => { @@ -31,7 +31,7 @@ const DevicesDrawerSection = () => { } > - + @@ -47,7 +47,7 @@ const DevicesDrawerSection = () => { - + @@ -57,10 +57,10 @@ const DevicesDrawerSection = () => { - + - + diff --git a/src/apps/experimental/components/drawers/dashboard/LiveTvDrawerSection.tsx b/src/apps/experimental/components/drawers/dashboard/LiveTvDrawerSection.tsx index e3d20e154a..35ea15ce0d 100644 --- a/src/apps/experimental/components/drawers/dashboard/LiveTvDrawerSection.tsx +++ b/src/apps/experimental/components/drawers/dashboard/LiveTvDrawerSection.tsx @@ -20,7 +20,7 @@ const LiveTvDrawerSection = () => { } > - + @@ -28,7 +28,7 @@ const LiveTvDrawerSection = () => { - + diff --git a/src/apps/experimental/components/drawers/dashboard/ServerDrawerSection.tsx b/src/apps/experimental/components/drawers/dashboard/ServerDrawerSection.tsx index 2ed6b73f86..01e26ace84 100644 --- a/src/apps/experimental/components/drawers/dashboard/ServerDrawerSection.tsx +++ b/src/apps/experimental/components/drawers/dashboard/ServerDrawerSection.tsx @@ -12,16 +12,16 @@ import ListItemLink from 'components/ListItemLink'; import globalize from 'scripts/globalize'; const LIBRARY_PATHS = [ - '/library.html', - '/librarydisplay.html', - '/metadataimages.html', - '/metadatanfo.html' + '/dashboard/libraries', + '/dashboard/libraries/display', + '/dashboard/libraries/metadata', + '/dashboard/libraries/nfo' ]; const PLAYBACK_PATHS = [ - '/encodingsettings.html', - '/playbackconfiguration.html', - '/streamingsettings.html' + '/dashboard/playback/transcoding', + '/dashboard/playback/resume', + '/dashboard/playback/streaming' ]; const ServerDrawerSection = () => { @@ -40,7 +40,7 @@ const ServerDrawerSection = () => { } > - + @@ -48,7 +48,7 @@ const ServerDrawerSection = () => { - + @@ -56,7 +56,7 @@ const ServerDrawerSection = () => { - + @@ -64,7 +64,7 @@ const ServerDrawerSection = () => { - + @@ -74,22 +74,22 @@ const ServerDrawerSection = () => { - + - + - + - + - + @@ -99,13 +99,13 @@ const ServerDrawerSection = () => { - + - + - + diff --git a/src/components/dashboard/users/SectionTabs.tsx b/src/components/dashboard/users/SectionTabs.tsx index 0fad3469df..1befb5912b 100644 --- a/src/components/dashboard/users/SectionTabs.tsx +++ b/src/components/dashboard/users/SectionTabs.tsx @@ -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')} + onclick="Dashboard.navigate('/dashboard/users/access', true);"> ${globalize.translate('TabAccess')} + onclick="Dashboard.navigate('/dashboard/users/parentalcontrol', true);"> ${globalize.translate('TabParentalControl')} + onclick="Dashboard.navigate('/dashboard/users/password', true);"> ${globalize.translate('HeaderPassword')} ` }); diff --git a/src/components/dashboard/users/UserCardBox.tsx b/src/components/dashboard/users/UserCardBox.tsx index b535f7a16d..f0fbdf96a7 100644 --- a/src/components/dashboard/users/UserCardBox.tsx +++ b/src/components/dashboard/users/UserCardBox.tsx @@ -11,7 +11,7 @@ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl __html: ` ${renderImgUrl} ` diff --git a/src/components/homesections/homesections.js b/src/components/homesections/homesections.js index 212d976a8f..6f911cdadc 100644 --- a/src/components/homesections/homesections.js +++ b/src/components/homesections/homesections.js @@ -94,7 +94,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'); }); } } diff --git a/src/components/router/appRouter.js b/src/components/router/appRouter.js index 253fdee922..c56d7fc6be 100644 --- a/src/components/router/appRouter.js +++ b/src/components/router/appRouter.js @@ -527,7 +527,7 @@ class AppRouter { } if (item === 'manageserver') { - return '#/dashboard.html'; + return '#/dashboard'; } if (item === 'recordedtv') { diff --git a/src/components/toolbar/AppUserMenu.tsx b/src/components/toolbar/AppUserMenu.tsx index 7119a4f504..634d02cf88 100644 --- a/src/components/toolbar/AppUserMenu.tsx +++ b/src/components/toolbar/AppUserMenu.tsx @@ -115,7 +115,7 @@ const AppUserMenu: FC = ({ @@ -127,7 +127,7 @@ const AppUserMenu: FC = ({ diff --git a/src/controllers/dashboard/dashboard.html b/src/controllers/dashboard/dashboard.html index a76cf11971..aed6bb206f 100644 --- a/src/controllers/dashboard/dashboard.html +++ b/src/controllers/dashboard/dashboard.html @@ -3,7 +3,7 @@
- +

${TabServer}

@@ -33,7 +33,7 @@
- +

${HeaderActiveDevices}

@@ -70,7 +70,7 @@
- +

${HeaderPaths}

diff --git a/src/controllers/dashboard/devices/devices.js b/src/controllers/dashboard/devices/devices.js index 1c5ede9531..c62571a1bc 100644 --- a/src/controllers/dashboard/devices/devices.js +++ b/src/controllers/dashboard/devices/devices.js @@ -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 += '
'; deviceHtml += ' diff --git a/src/controllers/dashboard/dlna/profile.js b/src/controllers/dashboard/dlna/profile.js index 0a88f4214e..0f92a3200c 100644 --- a/src/controllers/dashboard/dlna/profile.js +++ b/src/controllers/dashboard/dlna/profile.js @@ -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); } diff --git a/src/controllers/dashboard/dlna/profiles.html b/src/controllers/dashboard/dlna/profiles.html index 6eb60d1c34..f1696632c9 100644 --- a/src/controllers/dashboard/dlna/profiles.html +++ b/src/controllers/dashboard/dlna/profiles.html @@ -8,7 +8,7 @@
diff --git a/src/controllers/dashboard/dlna/profiles.js b/src/controllers/dashboard/dlna/profiles.js index a7c8f045c4..f69a0c6bf0 100644 --- a/src/controllers/dashboard/dlna/profiles.js +++ b/src/controllers/dashboard/dlna/profiles.js @@ -40,7 +40,7 @@ function renderProfiles(page, element, profiles) { html += '
'; html += ''; html += ''; @@ -78,10 +78,10 @@ function deleteProfile(page, id) { function getTabs() { return [{ - href: '#/dlnasettings.html', + href: '#/dashboard/dlna', name: globalize.translate('Settings') }, { - href: '#/dlnaprofiles.html', + href: '#/dashboard/dlna/profiles', name: globalize.translate('TabProfiles') }]; } diff --git a/src/controllers/dashboard/dlna/settings.js b/src/controllers/dashboard/dlna/settings.js index fcc7d7d100..d12b6744af 100644 --- a/src/controllers/dashboard/dlna/settings.js +++ b/src/controllers/dashboard/dlna/settings.js @@ -37,10 +37,10 @@ function onSubmit() { function getTabs() { return [{ - href: '#/dlnasettings.html', + href: '#/dashboard/dlna', name: globalize.translate('Settings') }, { - href: '#/dlnaprofiles.html', + href: '#/dashboard/dlna/profiles', name: globalize.translate('TabProfiles') }]; } diff --git a/src/controllers/dashboard/encodingsettings.js b/src/controllers/dashboard/encodingsettings.js index 9c268c0866..24946cadb7 100644 --- a/src/controllers/dashboard/encodingsettings.js +++ b/src/controllers/dashboard/encodingsettings.js @@ -167,13 +167,13 @@ function setDecodingCodecsVisible(context, value) { function getTabs() { return [{ - href: '#/encodingsettings.html', + href: '#/dashboard/playback/transcoding', name: globalize.translate('Transcoding') }, { - href: '#/playbackconfiguration.html', + href: '#/dashboard/playback/resume', name: globalize.translate('ButtonResume') }, { - href: '#/streamingsettings.html', + href: '#/dashboard/playback/streaming', name: globalize.translate('TabStreaming') }]; } diff --git a/src/controllers/dashboard/library.js b/src/controllers/dashboard/library.js index 51ca3af712..62d9329969 100644 --- a/src/controllers/dashboard/library.js +++ b/src/controllers/dashboard/library.js @@ -360,16 +360,16 @@ function getVirtualFolderHtml(page, virtualFolder, index) { function getTabs() { return [{ - href: '#/library.html', + href: '#/dashboard/libraries', name: globalize.translate('HeaderLibraries') }, { - href: '#/librarydisplay.html', + href: '#/dashboard/libraries/display', name: globalize.translate('Display') }, { - href: '#/metadataimages.html', + href: '#/dashboard/libraries/metadata', name: globalize.translate('Metadata') }, { - href: '#/metadatanfo.html', + href: '#/dashboard/libraries/nfo', name: globalize.translate('TabNfoSettings') }]; } diff --git a/src/controllers/dashboard/librarydisplay.js b/src/controllers/dashboard/librarydisplay.js index b418984fb7..a38608277d 100644 --- a/src/controllers/dashboard/librarydisplay.js +++ b/src/controllers/dashboard/librarydisplay.js @@ -7,16 +7,16 @@ import Dashboard from '../../utils/dashboard'; function getTabs() { return [{ - href: '#/library.html', + href: '#/dashboard/libraries', name: globalize.translate('HeaderLibraries') }, { - href: '#/librarydisplay.html', + href: '#/dashboard/libraries/display', name: globalize.translate('Display') }, { - href: '#/metadataimages.html', + href: '#/dashboard/libraries/metadata', name: globalize.translate('Metadata') }, { - href: '#/metadatanfo.html', + href: '#/dashboard/libraries/nfo', name: globalize.translate('TabNfoSettings') }]; } diff --git a/src/controllers/dashboard/metadataImages.js b/src/controllers/dashboard/metadataImages.js index 53633d6915..779727dc7a 100644 --- a/src/controllers/dashboard/metadataImages.js +++ b/src/controllers/dashboard/metadataImages.js @@ -88,16 +88,16 @@ function onSubmit() { function getTabs() { return [{ - href: '#/library.html', + href: '#/dashboard/libraries', name: globalize.translate('HeaderLibraries') }, { - href: '#/librarydisplay.html', + href: '#/dashboard/libraries/display', name: globalize.translate('Display') }, { - href: '#/metadataimages.html', + href: '#/dashboard/libraries/metadata', name: globalize.translate('Metadata') }, { - href: '#/metadatanfo.html', + href: '#/dashboard/libraries/nfo', name: globalize.translate('TabNfoSettings') }]; } diff --git a/src/controllers/dashboard/metadatanfo.js b/src/controllers/dashboard/metadatanfo.js index 38e2ec71fe..1a68d4d63c 100644 --- a/src/controllers/dashboard/metadatanfo.js +++ b/src/controllers/dashboard/metadatanfo.js @@ -46,16 +46,16 @@ function showConfirmMessage() { function getTabs() { return [{ - href: '#/library.html', + href: '#/dashboard/libraries', name: globalize.translate('HeaderLibraries') }, { - href: '#/librarydisplay.html', + href: '#/dashboard/libraries/display', name: globalize.translate('Display') }, { - href: '#/metadataimages.html', + href: '#/dashboard/libraries/metadata', name: globalize.translate('Metadata') }, { - href: '#/metadatanfo.html', + href: '#/dashboard/libraries/nfo', name: globalize.translate('TabNfoSettings') }]; } diff --git a/src/controllers/dashboard/playback.js b/src/controllers/dashboard/playback.js index f24e77efd9..6b4985df34 100644 --- a/src/controllers/dashboard/playback.js +++ b/src/controllers/dashboard/playback.js @@ -31,13 +31,13 @@ function onSubmit() { function getTabs() { return [{ - href: '#/encodingsettings.html', + href: '#/dashboard/playback/transcoding', name: globalize.translate('Transcoding') }, { - href: '#/playbackconfiguration.html', + href: '#/dashboard/playback/resume', name: globalize.translate('ButtonResume') }, { - href: '#/streamingsettings.html', + href: '#/dashboard/playback/streaming', name: globalize.translate('TabStreaming') }]; } diff --git a/src/controllers/dashboard/plugins/available/index.js b/src/controllers/dashboard/plugins/available/index.js index 78368b2c14..b3445b5cb7 100644 --- a/src/controllers/dashboard/plugins/available/index.js +++ b/src/controllers/dashboard/plugins/available/index.js @@ -120,7 +120,7 @@ function onSearchBarType(searchBar) { function getPluginHtml(plugin, options, installedPlugins) { let html = ''; - let href = plugin.externalUrl ? plugin.externalUrl : '#/addplugin.html?name=' + encodeURIComponent(plugin.name) + '&guid=' + plugin.guid; + let href = plugin.externalUrl ? plugin.externalUrl : '#/dashboard/plugins/add?name=' + encodeURIComponent(plugin.name) + '&guid=' + plugin.guid; if (options.context) { href += '&context=' + options.context; @@ -161,13 +161,13 @@ function getPluginHtml(plugin, options, installedPlugins) { function getTabs() { return [{ - href: '#/installedplugins.html', + href: '#/dashboard/plugins', name: globalize.translate('TabMyPlugins') }, { - href: '#/availableplugins.html', + href: '#/dashboard/plugins/catalog', name: globalize.translate('TabCatalog') }, { - href: '#/repositories.html', + href: '#/dashboard/plugins/repositories', name: globalize.translate('TabRepositories') }]; } diff --git a/src/controllers/dashboard/plugins/installed/index.js b/src/controllers/dashboard/plugins/installed/index.js index f6442710fc..91ebcfdff1 100644 --- a/src/controllers/dashboard/plugins/installed/index.js +++ b/src/controllers/dashboard/plugins/installed/index.js @@ -130,7 +130,7 @@ function populateList(page, plugins, pluginConfigurationPages) { } else { html += '
'; html += '

' + globalize.translate('MessageNoPluginsInstalled') + '

'; - html += '

'; + html += '

'; html += globalize.translate('MessageBrowsePluginCatalog'); html += '

'; html += '
'; @@ -221,13 +221,13 @@ function reloadList(page) { function getTabs() { return [{ - href: '#/installedplugins.html', + href: '#/dashboard/plugins', name: globalize.translate('TabMyPlugins') }, { - href: '#/availableplugins.html', + href: '#/dashboard/plugins/catalog', name: globalize.translate('TabCatalog') }, { - href: '#/repositories.html', + href: '#/dashboard/plugins/repositories', name: globalize.translate('TabRepositories') }]; } diff --git a/src/controllers/dashboard/plugins/repositories/index.js b/src/controllers/dashboard/plugins/repositories/index.js index 3c83b6e394..55ff12a456 100644 --- a/src/controllers/dashboard/plugins/repositories/index.js +++ b/src/controllers/dashboard/plugins/repositories/index.js @@ -105,13 +105,13 @@ function getRepositoryElement(repository) { function getTabs() { return [{ - href: '#/installedplugins.html', + href: '#/dashboard/plugins', name: globalize.translate('TabMyPlugins') }, { - href: '#/availableplugins.html', + href: '#/dashboard/plugins/catalog', name: globalize.translate('TabCatalog') }, { - href: '#/repositories.html', + href: '#/dashboard/plugins/repositories', name: globalize.translate('TabRepositories') }]; } diff --git a/src/controllers/dashboard/scheduledtasks/scheduledtasks.js b/src/controllers/dashboard/scheduledtasks/scheduledtasks.js index f17bc5c935..53966e18d1 100644 --- a/src/controllers/dashboard/scheduledtasks/scheduledtasks.js +++ b/src/controllers/dashboard/scheduledtasks/scheduledtasks.js @@ -53,12 +53,12 @@ function populateList(page, tasks) { html += '
'; } html += '
'; - html += ""; + html += ""; html += ''; html += ''; html += '
'; const textAlignStyle = globalize.getIsRTL() ? 'right' : 'left'; - html += ""; + html += ""; html += "

" + task.Name + '

'; html += "
" + getTaskProgressHtml(task) + '
'; html += '
'; diff --git a/src/controllers/dashboard/streaming.js b/src/controllers/dashboard/streaming.js index ba9d767517..c02a5cdbde 100644 --- a/src/controllers/dashboard/streaming.js +++ b/src/controllers/dashboard/streaming.js @@ -22,13 +22,13 @@ function onSubmit() { function getTabs() { return [{ - href: '#/encodingsettings.html', + href: '#/dashboard/playback/transcoding', name: globalize.translate('Transcoding') }, { - href: '#/playbackconfiguration.html', + href: '#/dashboard/playback/resume', name: globalize.translate('ButtonResume') }, { - href: '#/streamingsettings.html', + href: '#/dashboard/playback/streaming', name: globalize.translate('TabStreaming') }]; } diff --git a/src/controllers/livetvguideprovider.js b/src/controllers/livetvguideprovider.js index 7a133945f2..e87f2ace33 100644 --- a/src/controllers/livetvguideprovider.js +++ b/src/controllers/livetvguideprovider.js @@ -5,7 +5,7 @@ import { getParameterByName } from '../utils/url.ts'; import Events from '../utils/events.ts'; function onListingsSubmitted() { - Dashboard.navigate('livetvstatus.html'); + Dashboard.navigate('dashboard/livetv'); } function init(page, type, providerId) { diff --git a/src/controllers/livetvstatus.js b/src/controllers/livetvstatus.js index 3c0e304939..8532e8ae2a 100644 --- a/src/controllers/livetvstatus.js +++ b/src/controllers/livetvstatus.js @@ -220,9 +220,9 @@ function getProviderName(providerId) { function getProviderConfigurationUrl(providerId) { switch (providerId.toLowerCase()) { case 'xmltv': - return '#/livetvguideprovider.html?type=xmltv'; + return '#/dashboard/livetv/guide?type=xmltv'; case 'schedulesdirect': - return '#/livetvguideprovider.html?type=schedulesdirect'; + return '#/dashboard/livetv/guide?type=schedulesdirect'; } } @@ -249,7 +249,7 @@ function addProvider(button) { } function addDevice() { - Dashboard.navigate('livetvtuner.html'); + Dashboard.navigate('dashboard/livetv/tuner'); } function showDeviceMenu(button, tunerDeviceId) { @@ -274,7 +274,7 @@ function showDeviceMenu(button, tunerDeviceId) { break; case 'edit': - Dashboard.navigate('livetvtuner.html?id=' + tunerDeviceId); + Dashboard.navigate('dashboard/livetv/tuner?id=' + tunerDeviceId); } }); }); @@ -290,7 +290,7 @@ function onDevicesListClick(e) { if (btnCardOptions) { showDeviceMenu(btnCardOptions, id); } else { - Dashboard.navigate('livetvtuner.html?id=' + id); + Dashboard.navigate('dashboard/livetv/tuner?id=' + id); } } } diff --git a/src/controllers/livetvtuner.js b/src/controllers/livetvtuner.js index 7f6ec20270..de73b608d5 100644 --- a/src/controllers/livetvtuner.js +++ b/src/controllers/livetvtuner.js @@ -96,7 +96,7 @@ function submitForm(page) { contentType: 'application/json' }).then(function () { Dashboard.processServerConfigurationUpdateResult(); - Dashboard.navigate('livetvstatus.html'); + Dashboard.navigate('dashboard/livetv'); }, function () { loading.hide(); Dashboard.alert({ diff --git a/src/controllers/user/menu/index.html b/src/controllers/user/menu/index.html index 1c83bd9d68..8fe6326fc2 100644 --- a/src/controllers/user/menu/index.html +++ b/src/controllers/user/menu/index.html @@ -77,7 +77,7 @@

${HeaderAdmin}

- +
@@ -85,7 +85,7 @@
- +
'; } @@ -429,28 +429,28 @@ function createToolsMenuList(pluginItems) { name: globalize.translate('TabServer') }, { name: globalize.translate('TabDashboard'), - href: '#/dashboard.html', + href: '#/dashboard', pageIds: ['dashboardPage'], icon: 'dashboard' }, { name: globalize.translate('General'), - href: '#/dashboardgeneral.html', + href: '#/dashboard/settings', pageIds: ['dashboardGeneralPage'], icon: 'settings' }, { name: globalize.translate('HeaderUsers'), - href: '#/userprofiles.html', + href: '#/dashboard/users', pageIds: ['userProfilesPage', 'newUserPage', 'editUserPage', 'userLibraryAccessPage', 'userParentalControlPage', 'userPasswordPage'], icon: 'people' }, { name: globalize.translate('HeaderLibraries'), - href: '#/library.html', + href: '#/dashboard/libraries', pageIds: ['mediaLibraryPage', 'librarySettingsPage', 'libraryDisplayPage', 'metadataImagesConfigurationPage', 'metadataNfoPage'], icon: 'folder' }, { name: globalize.translate('TitlePlayback'), icon: 'play_arrow', - href: '#/encodingsettings.html', + href: '#/dashboard/playback/transcoding', pageIds: ['encodingSettingsPage', 'playbackConfigurationPage', 'streamingSettingsPage'] }]; addPluginPagesToMainMenu(links, pluginItems, 'server'); @@ -460,7 +460,7 @@ function createToolsMenuList(pluginItems) { }); links.push({ name: globalize.translate('HeaderDevices'), - href: '#/devices.html', + href: '#/dashboard/devices', pageIds: ['devicesPage', 'devicePage'], icon: 'devices' }); @@ -472,7 +472,7 @@ function createToolsMenuList(pluginItems) { }); links.push({ name: globalize.translate('DLNA'), - href: '#/dlnasettings.html', + href: '#/dashboard/dlna', pageIds: ['dlnaSettingsPage', 'dlnaProfilesPage', 'dlnaProfilePage'], icon: 'input' }); @@ -482,13 +482,13 @@ function createToolsMenuList(pluginItems) { }); links.push({ name: globalize.translate('LiveTV'), - href: '#/livetvstatus.html', + href: '#/dashboard/livetv', pageIds: ['liveTvStatusPage', 'liveTvTunerPage'], icon: 'live_tv' }); links.push({ name: globalize.translate('HeaderDVR'), - href: '#/livetvsettings.html', + href: '#/dashboard/recordings', pageIds: ['liveTvSettingsPage'], icon: 'dvr' }); @@ -500,35 +500,35 @@ function createToolsMenuList(pluginItems) { links.push({ name: globalize.translate('TabNetworking'), icon: 'cloud', - href: '#/networking.html', + href: '#/dashboard/networking', pageIds: ['networkingPage'] }); links.push({ name: globalize.translate('HeaderApiKeys'), icon: 'vpn_key', - href: '#/apikeys.html', + href: '#/dashboard/keys', pageIds: ['apiKeysPage'] }); links.push({ name: globalize.translate('TabLogs'), - href: '#/log.html', + href: '#/dashboard/logs', pageIds: ['logPage'], icon: 'bug_report' }); links.push({ name: globalize.translate('Notifications'), icon: 'notifications', - href: '#/notificationsettings.html' + href: '#/dashboard/notifications' }); links.push({ name: globalize.translate('TabPlugins'), icon: 'shopping_cart', - href: '#/installedplugins.html', + href: '#/dashboard/plugins', pageIds: ['pluginsPage', 'pluginCatalogPage'] }); links.push({ name: globalize.translate('TabScheduledTasks'), - href: '#/scheduledtasks.html', + href: '#/dashboard/tasks', pageIds: ['scheduledTasksPage', 'scheduledTaskPage'], icon: 'schedule' }); From 06386a8eb6c16d2407eb865739e3c94d4f4d0205 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Mon, 25 Sep 2023 00:16:10 -0400 Subject: [PATCH 12/70] Remove legacy dashboard drawer --- src/scripts/libraryMenu.js | 252 +------------------------------------ 1 file changed, 1 insertion(+), 251 deletions(-) diff --git a/src/scripts/libraryMenu.js b/src/scripts/libraryMenu.js index 6a3e77f267..f90ece9d79 100644 --- a/src/scripts/libraryMenu.js +++ b/src/scripts/libraryMenu.js @@ -376,249 +376,6 @@ function refreshLibraryInfoInDrawer(user) { } } -function refreshDashboardInfoInDrawer(page, apiClient) { - currentDrawerType = 'admin'; - loadNavDrawer(); - - if (navDrawerScrollContainer.querySelector('.adminDrawerLogo')) { - updateDashboardMenuSelectedItem(page); - } else { - createDashboardMenu(page, apiClient); - } -} - -function isUrlInCurrentView(url) { - return window.location.href.toString().toLowerCase().indexOf(url.toLowerCase()) !== -1; -} - -function updateDashboardMenuSelectedItem(page) { - const links = navDrawerScrollContainer.querySelectorAll('.navMenuOption'); - const currentViewId = page.id; - - for (let i = 0, length = links.length; i < length; i++) { - let link = links[i]; - let selected = false; - let pageIds = link.getAttribute('data-pageids'); - - if (pageIds) { - pageIds = pageIds.split('|'); - selected = pageIds.indexOf(currentViewId) != -1; - } - - let pageUrls = link.getAttribute('data-pageurls'); - - if (pageUrls) { - pageUrls = pageUrls.split('|'); - selected = pageUrls.filter(isUrlInCurrentView).length > 0; - } - - if (selected) { - link.classList.add('navMenuOption-selected'); - let title = ''; - link = link.querySelector('.navMenuOptionText') || link; - title += (link.innerText || link.textContent).trim(); - LibraryMenu.setTitle(title); - } else { - link.classList.remove('navMenuOption-selected'); - } - } -} - -function createToolsMenuList(pluginItems) { - const links = [{ - name: globalize.translate('TabServer') - }, { - name: globalize.translate('TabDashboard'), - href: '#/dashboard', - pageIds: ['dashboardPage'], - icon: 'dashboard' - }, { - name: globalize.translate('General'), - href: '#/dashboard/settings', - pageIds: ['dashboardGeneralPage'], - icon: 'settings' - }, { - name: globalize.translate('HeaderUsers'), - href: '#/dashboard/users', - pageIds: ['userProfilesPage', 'newUserPage', 'editUserPage', 'userLibraryAccessPage', 'userParentalControlPage', 'userPasswordPage'], - icon: 'people' - }, { - name: globalize.translate('HeaderLibraries'), - href: '#/dashboard/libraries', - pageIds: ['mediaLibraryPage', 'librarySettingsPage', 'libraryDisplayPage', 'metadataImagesConfigurationPage', 'metadataNfoPage'], - icon: 'folder' - }, { - name: globalize.translate('TitlePlayback'), - icon: 'play_arrow', - href: '#/dashboard/playback/transcoding', - pageIds: ['encodingSettingsPage', 'playbackConfigurationPage', 'streamingSettingsPage'] - }]; - addPluginPagesToMainMenu(links, pluginItems, 'server'); - links.push({ - divider: true, - name: globalize.translate('HeaderDevices') - }); - links.push({ - name: globalize.translate('HeaderDevices'), - href: '#/dashboard/devices', - pageIds: ['devicesPage', 'devicePage'], - icon: 'devices' - }); - links.push({ - name: globalize.translate('HeaderActivity'), - href: '#/dashboard/activity', - pageIds: ['serverActivityPage'], - icon: 'assessment' - }); - links.push({ - name: globalize.translate('DLNA'), - href: '#/dashboard/dlna', - pageIds: ['dlnaSettingsPage', 'dlnaProfilesPage', 'dlnaProfilePage'], - icon: 'input' - }); - links.push({ - divider: true, - name: globalize.translate('LiveTV') - }); - links.push({ - name: globalize.translate('LiveTV'), - href: '#/dashboard/livetv', - pageIds: ['liveTvStatusPage', 'liveTvTunerPage'], - icon: 'live_tv' - }); - links.push({ - name: globalize.translate('HeaderDVR'), - href: '#/dashboard/recordings', - pageIds: ['liveTvSettingsPage'], - icon: 'dvr' - }); - addPluginPagesToMainMenu(links, pluginItems, 'livetv'); - links.push({ - divider: true, - name: globalize.translate('TabAdvanced') - }); - links.push({ - name: globalize.translate('TabNetworking'), - icon: 'cloud', - href: '#/dashboard/networking', - pageIds: ['networkingPage'] - }); - links.push({ - name: globalize.translate('HeaderApiKeys'), - icon: 'vpn_key', - href: '#/dashboard/keys', - pageIds: ['apiKeysPage'] - }); - links.push({ - name: globalize.translate('TabLogs'), - href: '#/dashboard/logs', - pageIds: ['logPage'], - icon: 'bug_report' - }); - links.push({ - name: globalize.translate('Notifications'), - icon: 'notifications', - href: '#/dashboard/notifications' - }); - links.push({ - name: globalize.translate('TabPlugins'), - icon: 'shopping_cart', - href: '#/dashboard/plugins', - pageIds: ['pluginsPage', 'pluginCatalogPage'] - }); - links.push({ - name: globalize.translate('TabScheduledTasks'), - href: '#/dashboard/tasks', - pageIds: ['scheduledTasksPage', 'scheduledTaskPage'], - icon: 'schedule' - }); - if (hasUnsortedPlugins(pluginItems)) { - links.push({ - divider: true, - name: globalize.translate('TabPlugins') - }); - addPluginPagesToMainMenu(links, pluginItems); - } - return links; -} - -function hasUnsortedPlugins(pluginItems) { - for (const pluginItem of pluginItems) { - if (pluginItem.EnableInMainMenu && pluginItem.MenuSection === undefined) { - return true; - } - } - return false; -} - -function addPluginPagesToMainMenu(links, pluginItems, section) { - for (const pluginItem of pluginItems) { - if (pluginItem.EnableInMainMenu && pluginItem.MenuSection === section) { - links.push({ - name: pluginItem.DisplayName, - icon: pluginItem.MenuIcon || 'folder', - href: Dashboard.getPluginUrl(pluginItem.Name), - pageUrls: [Dashboard.getPluginUrl(pluginItem.Name)] - }); - } - } -} - -function getToolsMenuLinks(apiClient) { - return apiClient.getJSON(apiClient.getUrl('web/configurationpages') + '?pageType=PluginConfiguration&EnableInMainMenu=true').then(createToolsMenuList, function () { - return createToolsMenuList([]); - }); -} - -function getToolsLinkHtml(item) { - let menuHtml = ''; - let pageIds = item.pageIds ? item.pageIds.join('|') : ''; - pageIds = pageIds ? ' data-pageids="' + pageIds + '"' : ''; - let pageUrls = item.pageUrls ? item.pageUrls.join('|') : ''; - pageUrls = pageUrls ? ' data-pageurls="' + pageUrls + '"' : ''; - menuHtml += ''; - - if (item.icon) { - menuHtml += ''; - } - - menuHtml += ''; - menuHtml += escapeHtml(item.name); - menuHtml += ''; - return menuHtml + ''; -} - -function getToolsMenuHtml(apiClient) { - return getToolsMenuLinks(apiClient).then(function (items) { - let menuHtml = ''; - menuHtml += '
'; - - for (const item of items) { - if (item.href) { - menuHtml += getToolsLinkHtml(item); - } else if (item.name) { - menuHtml += '

'; - menuHtml += escapeHtml(item.name); - menuHtml += '

'; - } - } - - return menuHtml + '
'; - }); -} - -function createDashboardMenu(page, apiClient) { - return getToolsMenuHtml(apiClient).then(function (toolsMenuHtml) { - let html = ''; - html += ''; - html += toolsMenuHtml; - navDrawerScrollContainer.innerHTML = html; - updateDashboardMenuSelectedItem(page); - }); -} - function onSidebarLinkClick() { const section = this.getElementsByClassName('sectionName')[0]; const text = section ? section.innerHTML : this.innerHTML; @@ -1026,15 +783,8 @@ pageClassOn('pageshow', 'page', function (e) { const isDashboardPage = page.classList.contains('type-interior'); const isHomePage = page.classList.contains('homePage'); const isLibraryPage = !isDashboardPage && page.classList.contains('libraryPage'); - const apiClient = getCurrentApiClient(); - if (isDashboardPage) { - if (mainDrawerButton) { - mainDrawerButton.classList.remove('hide'); - } - - refreshDashboardInfoInDrawer(page, apiClient); - } else { + if (!isDashboardPage) { if (mainDrawerButton) { if (enableLibraryNavDrawer || (isHomePage && enableLibraryNavDrawerHome)) { mainDrawerButton.classList.remove('hide'); From 1a934c79563e067d448248ee565f106e5eeda4da Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Mon, 25 Sep 2023 02:13:16 -0400 Subject: [PATCH 13/70] Refactor common navigation components --- src/apps/dashboard/AppLayout.tsx | 98 ++++++++++++++++++- .../dashboard/components/drawer/AppDrawer.tsx | 29 ++++++ .../sections}/AdvancedDrawerSection.tsx | 0 .../drawer/sections}/DevicesDrawerSection.tsx | 0 .../drawer/sections}/LiveTvDrawerSection.tsx | 0 .../drawer/sections}/PluginDrawerSection.tsx | 0 .../drawer/sections}/ServerDrawerSection.tsx | 0 .../components/drawers/AppDrawer.tsx | 86 +++------------- 8 files changed, 138 insertions(+), 75 deletions(-) create mode 100644 src/apps/dashboard/components/drawer/AppDrawer.tsx rename src/apps/{experimental/components/drawers/dashboard => dashboard/components/drawer/sections}/AdvancedDrawerSection.tsx (100%) rename src/apps/{experimental/components/drawers/dashboard => dashboard/components/drawer/sections}/DevicesDrawerSection.tsx (100%) rename src/apps/{experimental/components/drawers/dashboard => dashboard/components/drawer/sections}/LiveTvDrawerSection.tsx (100%) rename src/apps/{experimental/components/drawers/dashboard => dashboard/components/drawer/sections}/PluginDrawerSection.tsx (100%) rename src/apps/{experimental/components/drawers/dashboard => dashboard/components/drawer/sections}/ServerDrawerSection.tsx (100%) diff --git a/src/apps/dashboard/AppLayout.tsx b/src/apps/dashboard/AppLayout.tsx index 19fe09e1ba..7afe655a3d 100644 --- a/src/apps/dashboard/AppLayout.tsx +++ b/src/apps/dashboard/AppLayout.tsx @@ -1,15 +1,103 @@ -import React from 'react'; -import { Outlet } from 'react-router-dom'; +import AppBar from '@mui/material/AppBar'; +import Box from '@mui/material/Box'; +import { useTheme } from '@mui/material/styles'; +import React, { useCallback, useEffect, useState } from 'react'; +import { Outlet, useLocation } from 'react-router-dom'; 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 AppDrawer from './components/drawer/AppDrawer'; + +// FIXME: Remove main app override styles import '../experimental/AppOverrides.scss'; +import AppToolbar from 'components/toolbar/AppToolbar'; + +interface DashboardAppSettings { + isDrawerPinned: boolean +} + +const DEFAULT_APP_SETTINGS: DashboardAppSettings = { + isDrawerPinned: false +}; const AppLayout = () => { + const [ appSettings, setAppSettings ] = useLocalStorage('DashboardAppSettings', DEFAULT_APP_SETTINGS); + const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned); + const location = useLocation(); + const theme = useTheme(); + const { user } = useApi(); + + // FIXME: Use const for metadata editor + const isDrawerAvailable = !location.pathname.startsWith('/metadata'); + 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 ( - - - + + + muiTheme.zIndex.drawer + 1 }} + > + + + + + + + + + + + + ); }; diff --git a/src/apps/dashboard/components/drawer/AppDrawer.tsx b/src/apps/dashboard/components/drawer/AppDrawer.tsx new file mode 100644 index 0000000000..7b1e180123 --- /dev/null +++ b/src/apps/dashboard/components/drawer/AppDrawer.tsx @@ -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 = ({ + open = false, + onClose, + onOpen +}) => ( + + + + + + + +); + +export default AppDrawer; diff --git a/src/apps/experimental/components/drawers/dashboard/AdvancedDrawerSection.tsx b/src/apps/dashboard/components/drawer/sections/AdvancedDrawerSection.tsx similarity index 100% rename from src/apps/experimental/components/drawers/dashboard/AdvancedDrawerSection.tsx rename to src/apps/dashboard/components/drawer/sections/AdvancedDrawerSection.tsx diff --git a/src/apps/experimental/components/drawers/dashboard/DevicesDrawerSection.tsx b/src/apps/dashboard/components/drawer/sections/DevicesDrawerSection.tsx similarity index 100% rename from src/apps/experimental/components/drawers/dashboard/DevicesDrawerSection.tsx rename to src/apps/dashboard/components/drawer/sections/DevicesDrawerSection.tsx diff --git a/src/apps/experimental/components/drawers/dashboard/LiveTvDrawerSection.tsx b/src/apps/dashboard/components/drawer/sections/LiveTvDrawerSection.tsx similarity index 100% rename from src/apps/experimental/components/drawers/dashboard/LiveTvDrawerSection.tsx rename to src/apps/dashboard/components/drawer/sections/LiveTvDrawerSection.tsx diff --git a/src/apps/experimental/components/drawers/dashboard/PluginDrawerSection.tsx b/src/apps/dashboard/components/drawer/sections/PluginDrawerSection.tsx similarity index 100% rename from src/apps/experimental/components/drawers/dashboard/PluginDrawerSection.tsx rename to src/apps/dashboard/components/drawer/sections/PluginDrawerSection.tsx diff --git a/src/apps/experimental/components/drawers/dashboard/ServerDrawerSection.tsx b/src/apps/dashboard/components/drawer/sections/ServerDrawerSection.tsx similarity index 100% rename from src/apps/experimental/components/drawers/dashboard/ServerDrawerSection.tsx rename to src/apps/dashboard/components/drawer/sections/ServerDrawerSection.tsx diff --git a/src/apps/experimental/components/drawers/AppDrawer.tsx b/src/apps/experimental/components/drawers/AppDrawer.tsx index dc48221b8c..f03a591675 100644 --- a/src/apps/experimental/components/drawers/AppDrawer.tsx +++ b/src/apps/experimental/components/drawers/AppDrawer.tsx @@ -1,23 +1,15 @@ import React, { FC } from 'react'; -import { Route, Routes, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer'; import { ASYNC_USER_ROUTES } from '../../routes/asyncRoutes'; import { LEGACY_USER_ROUTES } from '../../routes/legacyRoutes'; -import AdvancedDrawerSection from './dashboard/AdvancedDrawerSection'; -import DevicesDrawerSection from './dashboard/DevicesDrawerSection'; -import LiveTvDrawerSection from './dashboard/LiveTvDrawerSection'; -import PluginDrawerSection from './dashboard/PluginDrawerSection'; -import ServerDrawerSection from './dashboard/ServerDrawerSection'; import MainDrawerContent from './MainDrawerContent'; import { isTabPath } from '../tabs/tabRoutes'; -export const DRAWER_WIDTH = 240; - const DRAWERLESS_ROUTES = [ - 'metadata', // metadata manager 'video' // video player ]; @@ -26,75 +18,29 @@ const MAIN_DRAWER_ROUTES = [ ...LEGACY_USER_ROUTES ].filter(route => !DRAWERLESS_ROUTES.includes(route.path)); -const ADMIN_DRAWER_ROUTES = [ - { path: '/configurationpage' } // Plugin configuration page -].filter(route => !DRAWERLESS_ROUTES.includes(route.path)); - /** Utility function to check if a path has a drawer. */ export const isDrawerPath = (path: string) => ( MAIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path) - || ADMIN_DRAWER_ROUTES.some(route => route.path === path || `/${route.path}` === path) ); -const Drawer: FC = ({ children, ...props }) => { - const location = useLocation(); - const hasSecondaryToolBar = isTabPath(location.pathname); - - return ( - - {children} - - ); -}; - const AppDrawer: FC = ({ open = false, onClose, onOpen -}) => ( - - { - MAIN_DRAWER_ROUTES.map(route => ( - - - - } - /> - )) - } - { - ADMIN_DRAWER_ROUTES.map(route => ( - - - - - - - - } - /> - )) - } - -); +}) => { + const location = useLocation(); + const hasSecondaryToolBar = isTabPath(location.pathname); + + return ( + + + + ); +}; export default AppDrawer; From 35457b5abb2f4d8342bdbda942fda383c1d468a9 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Mon, 25 Sep 2023 13:54:33 -0400 Subject: [PATCH 14/70] Extract style overrides for dashboard --- src/apps/dashboard/App.tsx | 2 +- src/apps/dashboard/AppLayout.tsx | 18 +++++++++++------- src/apps/dashboard/AppOverrides.scss | 22 ++++++++++++++++++++++ src/apps/experimental/AppOverrides.scss | 8 +------- 4 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 src/apps/dashboard/AppOverrides.scss diff --git a/src/apps/dashboard/App.tsx b/src/apps/dashboard/App.tsx index db1776509a..30e3eda94d 100644 --- a/src/apps/dashboard/App.tsx +++ b/src/apps/dashboard/App.tsx @@ -34,7 +34,7 @@ export const DASHBOARD_APP_PATHS = { const DashboardApp = () => ( }> - }> + }> {ASYNC_ADMIN_ROUTES.map(toDashboardAsyncPageRoute)} {LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)} diff --git a/src/apps/dashboard/AppLayout.tsx b/src/apps/dashboard/AppLayout.tsx index 7afe655a3d..ce74f4989b 100644 --- a/src/apps/dashboard/AppLayout.tsx +++ b/src/apps/dashboard/AppLayout.tsx @@ -1,10 +1,11 @@ import AppBar from '@mui/material/AppBar'; import Box from '@mui/material/Box'; import { useTheme } from '@mui/material/styles'; -import React, { useCallback, useEffect, useState } from 'react'; +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'; @@ -12,9 +13,11 @@ import { useLocalStorage } from 'hooks/useLocalStorage'; import AppDrawer from './components/drawer/AppDrawer'; -// FIXME: Remove main app override styles -import '../experimental/AppOverrides.scss'; -import AppToolbar from 'components/toolbar/AppToolbar'; +import './AppOverrides.scss'; + +interface AppLayoutProps { + drawerlessPaths: string[] +} interface DashboardAppSettings { isDrawerPinned: boolean @@ -24,15 +27,16 @@ const DEFAULT_APP_SETTINGS: DashboardAppSettings = { isDrawerPinned: false }; -const AppLayout = () => { +const AppLayout: FC = ({ + drawerlessPaths +}) => { const [ appSettings, setAppSettings ] = useLocalStorage('DashboardAppSettings', DEFAULT_APP_SETTINGS); const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned); const location = useLocation(); const theme = useTheme(); const { user } = useApi(); - // FIXME: Use const for metadata editor - const isDrawerAvailable = !location.pathname.startsWith('/metadata'); + const isDrawerAvailable = !drawerlessPaths.some(path => location.pathname.startsWith(`/${path}`)); const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user); useEffect(() => { diff --git a/src/apps/dashboard/AppOverrides.scss b/src/apps/dashboard/AppOverrides.scss new file mode 100644 index 0000000000..c8597ee576 --- /dev/null +++ b/src/apps/dashboard/AppOverrides.scss @@ -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; + } +} diff --git a/src/apps/experimental/AppOverrides.scss b/src/apps/experimental/AppOverrides.scss index c365a5b295..cece6608c1 100644 --- a/src/apps/experimental/AppOverrides.scss +++ b/src/apps/experimental/AppOverrides.scss @@ -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; } From b4b57f5e55ea732ae3e3763a2ffb243de24d3a61 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 26 Sep 2023 00:37:48 -0400 Subject: [PATCH 15/70] Update metadata editor comment --- src/apps/dashboard/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/dashboard/App.tsx b/src/apps/dashboard/App.tsx index 30e3eda94d..7c52157258 100644 --- a/src/apps/dashboard/App.tsx +++ b/src/apps/dashboard/App.tsx @@ -40,7 +40,7 @@ const DashboardApp = () => ( {LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)} - {/* TODO: Should the metadata manager be a separate app? */} + {/* NOTE: The metadata editor might deserve a dedicated app in the future */} {toViewManagerPageRoute({ path: DASHBOARD_APP_PATHS.MetadataManager, pageProps: { From 187cefdcb13392c9f29cdda819766a67a0b916b3 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Sat, 30 Sep 2023 01:17:37 -0400 Subject: [PATCH 16/70] Update import order --- src/RootApp.tsx | 6 +++--- src/apps/dashboard/App.tsx | 2 +- src/apps/experimental/App.tsx | 2 +- src/apps/experimental/components/drawers/AppDrawer.tsx | 2 +- src/apps/stable/App.tsx | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/RootApp.tsx b/src/RootApp.tsx index 26f12eb39b..cc10ca7baa 100644 --- a/src/RootApp.tsx +++ b/src/RootApp.tsx @@ -1,18 +1,18 @@ import loadable from '@loadable/component'; import { ThemeProvider } from '@mui/material/styles'; import { History } from '@remix-run/router'; -import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import React from 'react'; +import { useLocation } from 'react-router-dom'; +import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App'; import AppHeader from 'components/AppHeader'; import Backdrop from 'components/Backdrop'; import { HistoryRouter } from 'components/router/HistoryRouter'; import { ApiProvider } from 'hooks/useApi'; import { WebConfigProvider } from 'hooks/useWebConfig'; import theme from 'themes/theme'; -import { useLocation } from 'react-router-dom'; -import { DASHBOARD_APP_PATHS } from './apps/dashboard/App'; const DashboardApp = loadable(() => import('./apps/dashboard/App')); const ExperimentalApp = loadable(() => import('./apps/experimental/App')); diff --git a/src/apps/dashboard/App.tsx b/src/apps/dashboard/App.tsx index 7c52157258..c5ab679297 100644 --- a/src/apps/dashboard/App.tsx +++ b/src/apps/dashboard/App.tsx @@ -8,10 +8,10 @@ import { AsyncPageProps, AsyncRoute, toAsyncPageRoute } from 'components/router/ 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'; -import AppLayout from './AppLayout'; const DashboardAsyncPage = loadable( (props: { page: string }) => import(/* webpackChunkName: "[request]" */ `./routes/${props.page}`), diff --git a/src/apps/experimental/App.tsx b/src/apps/experimental/App.tsx index d804352f94..b17e9054ee 100644 --- a/src/apps/experimental/App.tsx +++ b/src/apps/experimental/App.tsx @@ -1,6 +1,7 @@ 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 { toAsyncPageRoute } from 'components/router/AsyncRoute'; @@ -10,7 +11,6 @@ import { toRedirectRoute } from 'components/router/Redirect'; import AppLayout from './AppLayout'; import { ASYNC_USER_ROUTES } from './routes/asyncRoutes'; import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; -import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App'; const ExperimentalApp = () => { return ( diff --git a/src/apps/experimental/components/drawers/AppDrawer.tsx b/src/apps/experimental/components/drawers/AppDrawer.tsx index f03a591675..21926d6c59 100644 --- a/src/apps/experimental/components/drawers/AppDrawer.tsx +++ b/src/apps/experimental/components/drawers/AppDrawer.tsx @@ -5,9 +5,9 @@ import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDr import { ASYNC_USER_ROUTES } from '../../routes/asyncRoutes'; import { LEGACY_USER_ROUTES } from '../../routes/legacyRoutes'; +import { isTabPath } from '../tabs/tabRoutes'; import MainDrawerContent from './MainDrawerContent'; -import { isTabPath } from '../tabs/tabRoutes'; const DRAWERLESS_ROUTES = [ 'video' // video player diff --git a/src/apps/stable/App.tsx b/src/apps/stable/App.tsx index b0219312a0..9b0adbab9c 100644 --- a/src/apps/stable/App.tsx +++ b/src/apps/stable/App.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Navigate, Outlet, Route, Routes } from 'react-router-dom'; +import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App'; import AppBody from 'components/AppBody'; import ConnectionRequired from 'components/ConnectionRequired'; import { toAsyncPageRoute } from 'components/router/AsyncRoute'; @@ -10,7 +11,6 @@ import { toRedirectRoute } from 'components/router/Redirect'; import { ASYNC_USER_ROUTES } from './routes/asyncRoutes'; import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; import { REDIRECTS } from './routes/_redirects'; -import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App'; const Layout = () => ( From 189904256d9eb3473a05c6106ae5b197f5124812 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Sat, 30 Sep 2023 01:18:03 -0400 Subject: [PATCH 17/70] Remove old activity dashboard page --- src/controllers/dashboard/serveractivity.html | 12 ------- src/controllers/dashboard/serveractivity.js | 32 ------------------- 2 files changed, 44 deletions(-) delete mode 100644 src/controllers/dashboard/serveractivity.html delete mode 100644 src/controllers/dashboard/serveractivity.js diff --git a/src/controllers/dashboard/serveractivity.html b/src/controllers/dashboard/serveractivity.html deleted file mode 100644 index 29cacd300e..0000000000 --- a/src/controllers/dashboard/serveractivity.html +++ /dev/null @@ -1,12 +0,0 @@ -
-
-
-
-

-
-
-
-
-
-
-
diff --git a/src/controllers/dashboard/serveractivity.js b/src/controllers/dashboard/serveractivity.js deleted file mode 100644 index 72e84a89d5..0000000000 --- a/src/controllers/dashboard/serveractivity.js +++ /dev/null @@ -1,32 +0,0 @@ -import ActivityLog from '../../components/activitylog'; -import globalize from '../../scripts/globalize'; -import { toBoolean } from '../../utils/string.ts'; - -export default function (view, params) { - let activityLog; - - if (toBoolean(params.useractivity, true)) { - view.querySelector('.activityItems').setAttribute('data-useractivity', 'true'); - view.querySelector('.sectionTitle').innerHTML = globalize.translate('HeaderActivity'); - } else { - view.querySelector('.activityItems').setAttribute('data-useractivity', 'false'); - view.querySelector('.sectionTitle').innerHTML = globalize.translate('Alerts'); - } - - view.addEventListener('viewshow', function () { - if (!activityLog) { - activityLog = new ActivityLog({ - serverId: ApiClient.serverId(), - element: view.querySelector('.activityItems') - }); - } - }); - view.addEventListener('viewdestroy', function () { - if (activityLog) { - activityLog.destroy(); - } - - activityLog = null; - }); -} - From 1e3fa5418c5fbeafd0e5d708149c7c27bbaa52a1 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Sun, 1 Oct 2023 02:49:36 -0400 Subject: [PATCH 18/70] Remove duplicate card shape functions --- src/components/cardbuilder/cardBuilder.js | 39 +++++----- src/components/favoriteitems.js | 44 +++++------ src/components/homesections/homesections.js | 51 ++++++------- src/controllers/favorites.js | 56 ++++++-------- src/controllers/itemDetails/index.js | 85 +++++++++------------ src/controllers/livetv/livetvrecordings.js | 18 +++-- src/controllers/livetv/livetvschedule.js | 24 +++--- src/controllers/livetv/livetvsuggested.js | 49 +++++------- src/controllers/movies/moviegenres.js | 35 ++++----- src/controllers/movies/moviesrecommended.js | 50 ++++++------ src/controllers/music/musicrecommended.js | 48 ++++++------ src/controllers/shows/tvgenres.js | 35 ++++----- src/controllers/shows/tvrecommended.js | 42 +++++----- src/controllers/shows/tvupcoming.js | 25 +++--- src/scripts/livetvcomponents.js | 12 ++- src/utils/card.ts | 20 +++++ 16 files changed, 294 insertions(+), 339 deletions(-) create mode 100644 src/utils/card.ts diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index 96d7edb06a..c2f5436b39 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -5,24 +5,29 @@ */ import escapeHtml from 'escape-html'; -import datetime from '../../scripts/datetime'; -import imageLoader from '../images/imageLoader'; -import itemHelper from '../itemHelper'; + +import browser from 'scripts/browser'; +import datetime from 'scripts/datetime'; +import dom from 'scripts/dom'; +import globalize from 'scripts/globalize'; +import imageHelper from 'scripts/imagehelper'; +import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card'; +import { randomInt } from 'utils/number'; + import focusManager from '../focusManager'; +import 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; @@ -301,16 +306,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'); } } diff --git a/src/components/favoriteitems.js b/src/components/favoriteitems.js index ac0f3c0de0..b26c25ede1 100644 --- a/src/components/favoriteitems.js +++ b/src/components/favoriteitems.js @@ -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, diff --git a/src/components/homesections/homesections.js b/src/components/homesections/homesections.js index 212d976a8f..6f7be53f6e 100644 --- a/src/components/homesections/homesections.js +++ b/src/components/homesections/homesections.js @@ -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: @@ -169,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 = ''; @@ -244,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({ @@ -345,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, @@ -423,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, @@ -471,7 +466,7 @@ function getOnNowItemsHtml(items) { showChannelName: false, showAirDateTime: false, showAirEndTime: true, - defaultShape: getThumbShape(), + defaultShape: getBackdropShape(enableScrollX()), lines: 3, overlayPlayButton: true }); @@ -614,7 +609,7 @@ function getNextUpItemsHtmlFn(useEpisodeImages) { items: items, preferThumb: true, inheritThumb: !useEpisodeImages, - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), overlayText: false, showTitle: true, showParentTitle: true, diff --git a/src/controllers/favorites.js b/src/controllers/favorites.js index f14727a764..50c53d90b7 100644 --- a/src/controllers/favorites.js +++ b/src/controllers/favorites.js @@ -1,35 +1,25 @@ -import { appRouter } from '../components/router/appRouter'; -import cardBuilder from '../components/cardbuilder/cardBuilder'; -import dom from '../scripts/dom'; -import globalize from '../scripts/globalize'; -import { appHost } from '../components/apphost'; -import layoutManager from '../components/layoutManager'; -import focusManager from '../components/focusManager'; -import '../elements/emby-itemscontainer/emby-itemscontainer'; -import '../elements/emby-scroller/emby-scroller'; -import ServerConnections from '../components/ServerConnections'; +import { appHost } from 'components/apphost'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import focusManager from 'components/focusManager'; +import layoutManager from 'components/layoutManager'; +import { appRouter } from 'components/router/appRouter'; +import ServerConnections from 'components/ServerConnections'; +import dom from 'scripts/dom'; +import globalize from 'scripts/globalize'; +import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card'; + +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import 'elements/emby-scroller/emby-scroller'; function enableScrollX() { return true; } -function getThumbShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; -} - -function getPosterShape() { - return enableScrollX() ? 'overflowPortrait' : 'portrait'; -} - -function getSquareShape() { - return enableScrollX() ? 'overflowSquare' : 'square'; -} - function getSections() { return [{ name: 'Movies', types: 'Movie', - shape: getPosterShape(), + shape: getPortraitShape(enableScrollX()), showTitle: true, showYear: true, overlayPlayButton: true, @@ -38,7 +28,7 @@ function getSections() { }, { name: 'Shows', types: 'Series', - shape: getPosterShape(), + shape: getPortraitShape(enableScrollX()), showTitle: true, showYear: true, overlayPlayButton: true, @@ -47,7 +37,7 @@ function getSections() { }, { name: 'Episodes', types: 'Episode', - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), preferThumb: false, showTitle: true, showParentTitle: true, @@ -57,7 +47,7 @@ function getSections() { }, { name: 'Videos', types: 'Video', - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), preferThumb: true, showTitle: true, overlayPlayButton: true, @@ -66,7 +56,7 @@ function getSections() { }, { name: 'Collections', types: 'BoxSet', - shape: getPosterShape(), + shape: getPortraitShape(enableScrollX()), showTitle: true, overlayPlayButton: true, overlayText: false, @@ -74,7 +64,7 @@ function getSections() { }, { name: 'Playlists', types: 'Playlist', - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), preferThumb: false, showTitle: true, overlayText: false, @@ -85,7 +75,7 @@ function getSections() { }, { name: 'People', types: 'Person', - shape: getPosterShape(), + shape: getPortraitShape(enableScrollX()), preferThumb: false, showTitle: true, overlayText: false, @@ -96,7 +86,7 @@ function getSections() { }, { name: 'Artists', types: 'MusicArtist', - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), preferThumb: false, showTitle: true, overlayText: false, @@ -107,7 +97,7 @@ function getSections() { }, { name: 'Albums', types: 'MusicAlbum', - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), preferThumb: false, showTitle: true, overlayText: false, @@ -118,7 +108,7 @@ function getSections() { }, { name: 'Songs', types: 'Audio', - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), preferThumb: false, showTitle: true, overlayText: false, @@ -130,7 +120,7 @@ function getSections() { }, { name: 'Books', types: 'Book', - shape: getPosterShape(), + shape: getPortraitShape(enableScrollX()), showTitle: true, showYear: true, overlayPlayButton: true, diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js index 8b000348e0..4efe69e781 100644 --- a/src/controllers/itemDetails/index.js +++ b/src/controllers/itemDetails/index.js @@ -4,39 +4,42 @@ import { marked } from 'marked'; import escapeHtml from 'escape-html'; import isEqual from 'lodash-es/isEqual'; -import { appHost } from '../../components/apphost'; -import loading from '../../components/loading/loading'; -import { appRouter } from '../../components/router/appRouter'; -import layoutManager from '../../components/layoutManager'; -import Events from '../../utils/events.ts'; -import * as userSettings from '../../scripts/settings/userSettings'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import datetime from '../../scripts/datetime'; -import mediaInfo from '../../components/mediainfo/mediainfo'; -import { clearBackdrop, setBackdrops } from '../../components/backdrop/backdrop'; -import listView from '../../components/listview/listview'; -import itemContextMenu from '../../components/itemContextMenu'; -import itemHelper from '../../components/itemHelper'; -import dom from '../../scripts/dom'; -import imageLoader from '../../components/images/imageLoader'; -import libraryMenu from '../../scripts/libraryMenu'; -import globalize from '../../scripts/globalize'; -import browser from '../../scripts/browser'; -import { playbackManager } from '../../components/playback/playbackmanager'; -import '../../styles/scrollstyles.scss'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import '../../elements/emby-checkbox/emby-checkbox'; -import '../../elements/emby-button/emby-button'; -import '../../elements/emby-playstatebutton/emby-playstatebutton'; -import '../../elements/emby-ratingbutton/emby-ratingbutton'; -import '../../elements/emby-scroller/emby-scroller'; -import '../../elements/emby-select/emby-select'; -import itemShortcuts from '../../components/shortcuts'; -import Dashboard from '../../utils/dashboard'; -import ServerConnections from '../../components/ServerConnections'; -import confirm from '../../components/confirm/confirm'; -import { download } from '../../scripts/fileDownloader'; -import { getItemBackdropImageUrl } from '../../utils/jellyfin-apiclient/backdropImage'; +import { appHost } from 'components/apphost'; +import { clearBackdrop, setBackdrops } from 'components/backdrop/backdrop'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import confirm from 'components/confirm/confirm'; +import imageLoader from 'components/images/imageLoader'; +import itemContextMenu from 'components/itemContextMenu'; +import itemHelper from 'components/itemHelper'; +import mediaInfo from 'components/mediainfo/mediainfo'; +import layoutManager from 'components/layoutManager'; +import listView from 'components/listview/listview'; +import loading from 'components/loading/loading'; +import { playbackManager } from 'components/playback/playbackmanager'; +import { appRouter } from 'components/router/appRouter'; +import itemShortcuts from 'components/shortcuts'; +import ServerConnections from 'components/ServerConnections'; +import browser from 'scripts/browser'; +import datetime from 'scripts/datetime'; +import dom from 'scripts/dom'; +import { download } from 'scripts/fileDownloader'; +import globalize from 'scripts/globalize'; +import libraryMenu from 'scripts/libraryMenu'; +import * as userSettings from 'scripts/settings/userSettings'; +import { getPortraitShape, getSquareShape } from 'utils/card'; +import Dashboard from 'utils/dashboard'; +import Events from 'utils/events'; +import { getItemBackdropImageUrl } from 'utils/jellyfin-apiclient/backdropImage'; + +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import 'elements/emby-checkbox/emby-checkbox'; +import 'elements/emby-button/emby-button'; +import 'elements/emby-playstatebutton/emby-playstatebutton'; +import 'elements/emby-ratingbutton/emby-ratingbutton'; +import 'elements/emby-scroller/emby-scroller'; +import 'elements/emby-select/emby-select'; + +import 'styles/scrollstyles.scss'; function autoFocus(container) { import('../../components/autoFocuser').then(({ default: autoFocuser }) => { @@ -1069,22 +1072,6 @@ function enableScrollX() { return browser.mobile && window.screen.availWidth <= 1000; } -function getPortraitShape(scrollX) { - if (scrollX == null) { - scrollX = enableScrollX(); - } - - return scrollX ? 'overflowPortrait' : 'portrait'; -} - -function getSquareShape(scrollX) { - if (scrollX == null) { - scrollX = enableScrollX(); - } - - return scrollX ? 'overflowSquare' : 'square'; -} - function renderMoreFromSeason(view, item, apiClient) { const section = view.querySelector('.moreFromSeasonSection'); diff --git a/src/controllers/livetv/livetvrecordings.js b/src/controllers/livetv/livetvrecordings.js index 73afca81d7..be62a110ae 100644 --- a/src/controllers/livetv/livetvrecordings.js +++ b/src/controllers/livetv/livetvrecordings.js @@ -1,10 +1,12 @@ -import loading from '../../components/loading/loading'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import imageLoader from '../../components/images/imageLoader'; -import '../../scripts/livetvcomponents'; -import '../../components/listview/listview.scss'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import Dashboard from '../../utils/dashboard'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import imageLoader from 'components/images/imageLoader'; +import loading from 'components/loading/loading'; +import { getBackdropShape } from 'utils/card'; +import Dashboard from 'utils/dashboard'; + +import 'scripts/livetvcomponents'; +import 'components/listview/listview.scss'; +import 'elements/emby-itemscontainer/emby-itemscontainer'; function renderRecordings(elem, recordings, cardOptions, scrollX) { if (!elem) { @@ -32,7 +34,7 @@ function renderRecordings(elem, recordings, cardOptions, scrollX) { recordingItems.innerHTML = cardBuilder.getCardsHtml(Object.assign({ items: recordings, shape: scrollX ? 'autooverflow' : 'auto', - defaultShape: scrollX ? 'overflowBackdrop' : 'backdrop', + defaultShape: getBackdropShape(scrollX), showTitle: true, showParentTitle: true, coverImage: true, diff --git a/src/controllers/livetv/livetvschedule.js b/src/controllers/livetv/livetvschedule.js index 605930e9fd..595daab7a6 100644 --- a/src/controllers/livetv/livetvschedule.js +++ b/src/controllers/livetv/livetvschedule.js @@ -1,11 +1,13 @@ -import layoutManager from '../../components/layoutManager'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import imageLoader from '../../components/images/imageLoader'; -import loading from '../../components/loading/loading'; -import '../../scripts/livetvcomponents'; -import '../../elements/emby-button/emby-button'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import Dashboard from '../../utils/dashboard'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import imageLoader from 'components/images/imageLoader'; +import layoutManager from 'components/layoutManager'; +import loading from 'components/loading/loading'; +import { getBackdropShape } from 'utils/card'; +import Dashboard from 'utils/dashboard'; + +import 'elements/emby-button/emby-button'; +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import 'scripts/livetvcomponents'; function enableScrollX() { return !layoutManager.desktop; @@ -50,15 +52,11 @@ function renderRecordings(elem, recordings, cardOptions) { imageLoader.lazyChildren(recordingItems); } -function getBackdropShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; -} - function renderActiveRecordings(context, promise) { promise.then(function (result) { renderRecordings(context.querySelector('#activeRecordings'), result.Items, { shape: enableScrollX() ? 'autooverflow' : 'auto', - defaultShape: getBackdropShape(), + defaultShape: getBackdropShape(enableScrollX()), showParentTitle: false, showParentTitleOrTitle: true, showTitle: true, diff --git a/src/controllers/livetv/livetvsuggested.js b/src/controllers/livetv/livetvsuggested.js index d8325f7a2b..b01e754290 100644 --- a/src/controllers/livetv/livetvsuggested.js +++ b/src/controllers/livetv/livetvsuggested.js @@ -1,36 +1,25 @@ -import layoutManager from '../../components/layoutManager'; -import * as userSettings from '../../scripts/settings/userSettings'; -import inputManager from '../../scripts/inputManager'; -import loading from '../../components/loading/loading'; -import globalize from '../../scripts/globalize'; -import * as mainTabsManager from '../../components/maintabsmanager'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import imageLoader from '../../components/images/imageLoader'; -import '../../styles/scrollstyles.scss'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import '../../elements/emby-tabs/emby-tabs'; -import '../../elements/emby-button/emby-button'; -import { LibraryTab } from '../../types/libraryTab.ts'; -import Dashboard from '../../utils/dashboard'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import imageLoader from 'components/images/imageLoader'; +import layoutManager from 'components/layoutManager'; +import loading from 'components/loading/loading'; +import * as mainTabsManager from 'components/maintabsmanager'; +import globalize from 'scripts/globalize'; +import inputManager from 'scripts/inputManager'; +import * as userSettings from 'scripts/settings/userSettings'; +import { LibraryTab } from 'types/libraryTab'; +import Dashboard from 'utils/dashboard'; +import { getBackdropShape, getPortraitShape } from 'utils/card'; + +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import 'elements/emby-tabs/emby-tabs'; +import 'elements/emby-button/emby-button'; + +import 'styles/scrollstyles.scss'; function enableScrollX() { return !layoutManager.desktop; } -function getBackdropShape() { - if (enableScrollX()) { - return 'overflowBackdrop'; - } - return 'backdrop'; -} - -function getPortraitShape() { - if (enableScrollX()) { - return 'overflowPortrait'; - } - return 'portrait'; -} - function getLimit() { if (enableScrollX()) { return 12; @@ -96,7 +85,7 @@ function reload(page, enableFullRender) { EnableImageTypes: 'Primary,Thumb' }).then(function (result) { renderItems(page, result.Items, 'upcomingTvMovieItems', null, { - shape: getPortraitShape(), + shape: getPortraitShape(enableScrollX()), preferThumb: null, showParentTitle: false }); @@ -147,7 +136,7 @@ function renderItems(page, items, sectionClass, overlayButton, cardOptions) { preferThumb: 'auto', inheritThumb: false, shape: enableScrollX() ? 'autooverflow' : 'auto', - defaultShape: getBackdropShape(), + defaultShape: getBackdropShape(enableScrollX()), showParentTitle: true, showTitle: true, centerText: true, diff --git a/src/controllers/movies/moviegenres.js b/src/controllers/movies/moviegenres.js index 3d866c6e9a..36c433c70d 100644 --- a/src/controllers/movies/moviegenres.js +++ b/src/controllers/movies/moviegenres.js @@ -1,12 +1,15 @@ import escapeHtml from 'escape-html'; -import layoutManager from '../../components/layoutManager'; -import loading from '../../components/loading/loading'; -import * as userSettings from '../../scripts/settings/userSettings'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver'; -import globalize from '../../scripts/globalize'; -import { appRouter } from '../../components/router/appRouter'; -import '../../elements/emby-button/emby-button'; + +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import layoutManager from 'components/layoutManager'; +import lazyLoader from 'components/lazyLoader/lazyLoaderIntersectionObserver'; +import loading from 'components/loading/loading'; +import { appRouter } from 'components/router/appRouter'; +import globalize from 'scripts/globalize'; +import * as userSettings from 'scripts/settings/userSettings'; +import { getBackdropShape, getPortraitShape } from 'utils/card'; + +import 'elements/emby-button/emby-button'; export default function (view, params, tabContent) { function getPageData() { @@ -49,14 +52,6 @@ export default function (view, params, tabContent) { return !layoutManager.desktop; } - function getThumbShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; - } - - function getPortraitShape() { - return enableScrollX() ? 'overflowPortrait' : 'portrait'; - } - const fillItemsContainer = (entry) => { const elem = entry.target; const id = elem.getAttribute('data-id'); @@ -85,7 +80,7 @@ export default function (view, params, tabContent) { if (viewStyle == 'Thumb') { cardBuilder.buildCards(result.Items, { itemsContainer: elem, - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), preferThumb: true, showTitle: true, scalable: true, @@ -96,7 +91,7 @@ export default function (view, params, tabContent) { } else if (viewStyle == 'ThumbCard') { cardBuilder.buildCards(result.Items, { itemsContainer: elem, - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), preferThumb: true, showTitle: true, scalable: true, @@ -107,7 +102,7 @@ export default function (view, params, tabContent) { } else if (viewStyle == 'PosterCard') { cardBuilder.buildCards(result.Items, { itemsContainer: elem, - shape: getPortraitShape(), + shape: getPortraitShape(enableScrollX()), showTitle: true, scalable: true, centerText: false, @@ -117,7 +112,7 @@ export default function (view, params, tabContent) { } else if (viewStyle == 'Poster') { cardBuilder.buildCards(result.Items, { itemsContainer: elem, - shape: getPortraitShape(), + shape: getPortraitShape(enableScrollX()), scalable: true, overlayMoreButton: true, allowBottomPadding: true, diff --git a/src/controllers/movies/moviesrecommended.js b/src/controllers/movies/moviesrecommended.js index 1dc60c30e1..ea593f805d 100644 --- a/src/controllers/movies/moviesrecommended.js +++ b/src/controllers/movies/moviesrecommended.js @@ -1,35 +1,29 @@ import escapeHtml from 'escape-html'; -import layoutManager from '../../components/layoutManager'; -import inputManager from '../../scripts/inputManager'; -import * as userSettings from '../../scripts/settings/userSettings'; -import libraryMenu from '../../scripts/libraryMenu'; -import * as mainTabsManager from '../../components/maintabsmanager'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import dom from '../../scripts/dom'; -import imageLoader from '../../components/images/imageLoader'; -import { playbackManager } from '../../components/playback/playbackmanager'; -import globalize from '../../scripts/globalize'; -import { LibraryTab } from '../../types/libraryTab.ts'; -import Dashboard from '../../utils/dashboard'; -import Events from '../../utils/events.ts'; -import '../../elements/emby-scroller/emby-scroller'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import '../../elements/emby-tabs/emby-tabs'; -import '../../elements/emby-button/emby-button'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import imageLoader from 'components/images/imageLoader'; +import layoutManager from 'components/layoutManager'; +import * as mainTabsManager from 'components/maintabsmanager'; +import { playbackManager } from 'components/playback/playbackmanager'; +import dom from 'scripts/dom'; +import globalize from 'scripts/globalize'; +import inputManager from 'scripts/inputManager'; +import libraryMenu from 'scripts/libraryMenu'; +import * as userSettings from 'scripts/settings/userSettings'; +import { LibraryTab } from 'types/libraryTab'; +import { getBackdropShape, getPortraitShape } from 'utils/card'; +import Dashboard from 'utils/dashboard'; +import Events from 'utils/events'; + +import 'elements/emby-scroller/emby-scroller'; +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import 'elements/emby-tabs/emby-tabs'; +import 'elements/emby-button/emby-button'; function enableScrollX() { return !layoutManager.desktop; } -function getPortraitShape() { - return enableScrollX() ? 'overflowPortrait' : 'portrait'; -} - -function getThumbShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; -} - function loadLatest(page, userId, parentId) { const options = { IncludeItemTypes: 'Movie', @@ -45,7 +39,7 @@ function loadLatest(page, userId, parentId) { const container = page.querySelector('#recentlyAddedItems'); cardBuilder.buildCards(items, { itemsContainer: container, - shape: getPortraitShape(), + shape: getPortraitShape(enableScrollX()), scalable: true, overlayPlayButton: true, allowBottomPadding: allowBottomPadding, @@ -87,7 +81,7 @@ function loadResume(page, userId, parentId) { cardBuilder.buildCards(result.Items, { itemsContainer: container, preferThumb: true, - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), scalable: true, overlayPlayButton: true, allowBottomPadding: allowBottomPadding, @@ -138,7 +132,7 @@ function getRecommendationHtml(recommendation) { } html += cardBuilder.getCardsHtml(recommendation.Items, { - shape: getPortraitShape(), + shape: getPortraitShape(enableScrollX()), scalable: true, overlayPlayButton: true, allowBottomPadding: allowBottomPadding, diff --git a/src/controllers/music/musicrecommended.js b/src/controllers/music/musicrecommended.js index b66ae0ff7c..6dc14b9b56 100644 --- a/src/controllers/music/musicrecommended.js +++ b/src/controllers/music/musicrecommended.js @@ -1,22 +1,24 @@ -import browser from '../../scripts/browser'; -import layoutManager from '../../components/layoutManager'; -import * as userSettings from '../../scripts/settings/userSettings'; -import inputManager from '../../scripts/inputManager'; -import loading from '../../components/loading/loading'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import dom from '../../scripts/dom'; -import imageLoader from '../../components/images/imageLoader'; -import libraryMenu from '../../scripts/libraryMenu'; -import * as mainTabsManager from '../../components/maintabsmanager'; -import globalize from '../../scripts/globalize'; -import { LibraryTab } from '../../types/libraryTab.ts'; -import Dashboard from '../../utils/dashboard'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import imageLoader from 'components/images/imageLoader'; +import layoutManager from 'components/layoutManager'; +import loading from 'components/loading/loading'; +import * as mainTabsManager from 'components/maintabsmanager'; +import browser from 'scripts/browser'; +import dom from 'scripts/dom'; +import globalize from 'scripts/globalize'; +import inputManager from 'scripts/inputManager'; +import libraryMenu from 'scripts/libraryMenu'; +import * as userSettings from 'scripts/settings/userSettings'; +import { LibraryTab } from 'types/libraryTab'; +import Dashboard from 'utils/dashboard'; +import { getSquareShape } from 'utils/card'; -import '../../styles/scrollstyles.scss'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import '../../elements/emby-tabs/emby-tabs'; -import '../../elements/emby-button/emby-button'; -import '../../styles/flexstyles.scss'; +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import 'elements/emby-tabs/emby-tabs'; +import 'elements/emby-button/emby-button'; + +import 'styles/flexstyles.scss'; +import 'styles/scrollstyles.scss'; function itemsPerRow() { const screenWidth = dom.getWindowSize().innerWidth; @@ -40,10 +42,6 @@ function enableScrollX() { return !layoutManager.desktop; } -function getSquareShape() { - return enableScrollX() ? 'overflowSquare' : 'square'; -} - function loadLatest(page, parentId) { loading.show(); const userId = ApiClient.getCurrentUserId(); @@ -62,7 +60,7 @@ function loadLatest(page, parentId) { items: items, showUnplayedIndicator: false, showLatestItemsPopup: false, - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), showTitle: true, showParentTitle: true, lazy: true, @@ -108,7 +106,7 @@ function loadRecentlyPlayed(page, parentId) { itemsContainer.innerHTML = cardBuilder.getCardsHtml({ items: result.Items, showUnplayedIndicator: false, - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), showTitle: true, showParentTitle: true, action: 'instantmix', @@ -150,7 +148,7 @@ function loadFrequentlyPlayed(page, parentId) { itemsContainer.innerHTML = cardBuilder.getCardsHtml({ items: result.Items, showUnplayedIndicator: false, - shape: getSquareShape(), + shape: getSquareShape(enableScrollX()), showTitle: true, showParentTitle: true, action: 'instantmix', diff --git a/src/controllers/shows/tvgenres.js b/src/controllers/shows/tvgenres.js index 061089af86..3a45be7d5a 100644 --- a/src/controllers/shows/tvgenres.js +++ b/src/controllers/shows/tvgenres.js @@ -1,12 +1,15 @@ import escapeHtml from 'escape-html'; -import layoutManager from '../../components/layoutManager'; -import loading from '../../components/loading/loading'; -import * as userSettings from '../../scripts/settings/userSettings'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver'; -import globalize from '../../scripts/globalize'; -import { appRouter } from '../../components/router/appRouter'; -import '../../elements/emby-button/emby-button'; + +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import lazyLoader from 'components/lazyLoader/lazyLoaderIntersectionObserver'; +import layoutManager from 'components/layoutManager'; +import loading from 'components/loading/loading'; +import { appRouter } from 'components/router/appRouter'; +import globalize from 'scripts/globalize'; +import * as userSettings from 'scripts/settings/userSettings'; +import { getBackdropShape, getPortraitShape } from 'utils/card'; + +import 'elements/emby-button/emby-button'; export default function (view, params, tabContent) { function getPageData() { @@ -49,14 +52,6 @@ export default function (view, params, tabContent) { return !layoutManager.desktop; } - function getThumbShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; - } - - function getPortraitShape() { - return enableScrollX() ? 'overflowPortrait' : 'portrait'; - } - function fillItemsContainer(entry) { const elem = entry.target; const id = elem.getAttribute('data-id'); @@ -85,7 +80,7 @@ export default function (view, params, tabContent) { if (viewStyle == 'Thumb') { cardBuilder.buildCards(result.Items, { itemsContainer: elem, - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), preferThumb: true, showTitle: true, scalable: true, @@ -96,7 +91,7 @@ export default function (view, params, tabContent) { } else if (viewStyle == 'ThumbCard') { cardBuilder.buildCards(result.Items, { itemsContainer: elem, - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), preferThumb: true, showTitle: true, scalable: true, @@ -107,7 +102,7 @@ export default function (view, params, tabContent) { } else if (viewStyle == 'PosterCard') { cardBuilder.buildCards(result.Items, { itemsContainer: elem, - shape: getPortraitShape(), + shape: getPortraitShape(enableScrollX()), showTitle: true, scalable: true, centerText: false, @@ -117,7 +112,7 @@ export default function (view, params, tabContent) { } else if (viewStyle == 'Poster') { cardBuilder.buildCards(result.Items, { itemsContainer: elem, - shape: getPortraitShape(), + shape: getPortraitShape(enableScrollX()), scalable: true, showTitle: true, centerText: true, diff --git a/src/controllers/shows/tvrecommended.js b/src/controllers/shows/tvrecommended.js index d3673bf6ae..982420bde7 100644 --- a/src/controllers/shows/tvrecommended.js +++ b/src/controllers/shows/tvrecommended.js @@ -1,21 +1,23 @@ +import autoFocuser from 'components/autoFocuser'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import layoutManager from 'components/layoutManager'; +import loading from 'components/loading/loading'; +import * as mainTabsManager from 'components/maintabsmanager'; +import { playbackManager } from 'components/playback/playbackmanager'; +import dom from 'scripts/dom'; +import globalize from 'scripts/globalize'; +import inputManager from 'scripts/inputManager'; +import libraryMenu from 'scripts/libraryMenu'; +import * as userSettings from 'scripts/settings/userSettings'; +import { LibraryTab } from 'types/libraryTab'; +import { getBackdropShape } from 'utils/card'; +import Dashboard from 'utils/dashboard'; +import Events from 'utils/events'; -import inputManager from '../../scripts/inputManager'; -import libraryMenu from '../../scripts/libraryMenu'; -import layoutManager from '../../components/layoutManager'; -import loading from '../../components/loading/loading'; -import dom from '../../scripts/dom'; -import * as userSettings from '../../scripts/settings/userSettings'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import { playbackManager } from '../../components/playback/playbackmanager'; -import * as mainTabsManager from '../../components/maintabsmanager'; -import globalize from '../../scripts/globalize'; -import '../../styles/scrollstyles.scss'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; -import '../../elements/emby-button/emby-button'; -import { LibraryTab } from '../../types/libraryTab.ts'; -import Dashboard from '../../utils/dashboard'; -import Events from '../../utils/events.ts'; -import autoFocuser from '../../components/autoFocuser'; +import 'elements/emby-itemscontainer/emby-itemscontainer'; +import 'elements/emby-button/emby-button'; + +import 'styles/scrollstyles.scss'; function getTabs() { return [{ @@ -119,7 +121,7 @@ function loadResume(view, userId, parentId) { itemsContainer: container, preferThumb: true, inheritThumb: !userSettings.useEpisodeImagesInNextUpAndResume(), - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), scalable: true, overlayPlayButton: true, allowBottomPadding: allowBottomPadding, @@ -217,10 +219,6 @@ function enableScrollX() { return !layoutManager.desktop; } -function getThumbShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; -} - export default function (view, params) { function onBeforeTabChange(e) { preLoadTab(view, parseInt(e.detail.selectedTabIndex, 10)); diff --git a/src/controllers/shows/tvupcoming.js b/src/controllers/shows/tvupcoming.js index f8b2f31eaa..f3c7d90ec6 100644 --- a/src/controllers/shows/tvupcoming.js +++ b/src/controllers/shows/tvupcoming.js @@ -1,11 +1,14 @@ -import layoutManager from '../../components/layoutManager'; -import loading from '../../components/loading/loading'; -import datetime from '../../scripts/datetime'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; -import imageLoader from '../../components/images/imageLoader'; -import globalize from '../../scripts/globalize'; -import '../../styles/scrollstyles.scss'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import imageLoader from 'components/images/imageLoader'; +import layoutManager from 'components/layoutManager'; +import loading from 'components/loading/loading'; +import datetime from 'scripts/datetime'; +import globalize from 'scripts/globalize'; +import { getBackdropShape } from 'utils/card'; + +import 'elements/emby-itemscontainer/emby-itemscontainer'; + +import 'styles/scrollstyles.scss'; function getUpcomingPromise(context, params) { loading.show(); @@ -40,10 +43,6 @@ function enableScrollX() { return !layoutManager.desktop; } -function getThumbShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; -} - function renderUpcoming(elem, items) { const groups = []; let currentGroupName = ''; @@ -105,7 +104,7 @@ function renderUpcoming(elem, items) { html += cardBuilder.getCardsHtml({ items: group.items, showLocationTypeIndicator: false, - shape: getThumbShape(), + shape: getBackdropShape(enableScrollX()), showTitle: true, preferThumb: true, lazy: true, diff --git a/src/scripts/livetvcomponents.js b/src/scripts/livetvcomponents.js index 38035c3dc2..68a2c53453 100644 --- a/src/scripts/livetvcomponents.js +++ b/src/scripts/livetvcomponents.js @@ -1,15 +1,13 @@ -import layoutManager from '../components/layoutManager'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import layoutManager from 'components/layoutManager'; +import { getBackdropShape } from 'utils/card'; + import datetime from './datetime'; -import cardBuilder from '../components/cardbuilder/cardBuilder'; function enableScrollX() { return !layoutManager.desktop; } -function getBackdropShape() { - return enableScrollX() ? 'overflowBackdrop' : 'backdrop'; -} - function getTimersHtml(timers, options) { options = options || {}; @@ -78,7 +76,7 @@ function getTimersHtml(timers, options) { html += cardBuilder.getCardsHtml({ items: group.items, - shape: getBackdropShape(), + shape: getBackdropShape(enableScrollX()), showTitle: true, showParentTitleOrTitle: true, showAirTime: true, diff --git a/src/utils/card.ts b/src/utils/card.ts new file mode 100644 index 0000000000..c3f047a79a --- /dev/null +++ b/src/utils/card.ts @@ -0,0 +1,20 @@ +enum CardShape { + Backdrop = 'backdrop', + BackdropOverflow = 'overflowBackdrop', + Portrait = 'portrait', + PortraitOverflow = 'overflowPortrait', + Square = 'square', + SquareOverflow = 'overflowSquare' +} + +export function getSquareShape(enableOverflow = true) { + return enableOverflow ? CardShape.SquareOverflow : CardShape.Square; +} + +export function getBackdropShape(enableOverflow = true) { + return enableOverflow ? CardShape.BackdropOverflow : CardShape.Backdrop; +} + +export function getPortraitShape(enableOverflow = true) { + return enableOverflow ? CardShape.PortraitOverflow : CardShape.Portrait; +} From 2e99ca1e4ade848a38ae91b4eba0221893581419 Mon Sep 17 00:00:00 2001 From: Guilherme Augusto Belintani Date: Mon, 2 Oct 2023 16:43:16 +0000 Subject: [PATCH 19/70] Translated using Weblate (Portuguese (Brazil)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/ --- src/strings/pt-br.json | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/strings/pt-br.json b/src/strings/pt-br.json index 5239d9d3c1..71068d9ba6 100644 --- a/src/strings/pt-br.json +++ b/src/strings/pt-br.json @@ -1751,5 +1751,25 @@ "Unknown": "Desconhecido", "HeaderConfirmRepositoryInstallation": "Confirme a instalação do repositório de plug-ins", "LabelDeveloper": "Desenvolvedor", - "PleaseConfirmRepositoryInstallation": "Por favor, clique em OK para confirmar que você leu o acima e deseja prosseguir com a instalação do repositório de plug-ins." + "PleaseConfirmRepositoryInstallation": "Por favor, clique em OK para confirmar que você leu o acima e deseja prosseguir com a instalação do repositório de plug-ins.", + "LabelIsHearingImpaired": "Para deficientes auditivos (SDH)", + "BackdropScreensaver": "Imagem de fundo do protetor de tela", + "LogoScreensaver": "Logo da proteção de tela", + "AllowAv1Encoding": "Permitir codificação em formato AV1", + "HeaderGuestCast": "Estrelas Convidadas", + "HeaderEpisodesStatus": "Situação dos Episódios", + "GoHome": "Tela Inicial", + "UnknownError": "Um erro desconhecido aconteceu.", + "LabelBackdropScreensaverInterval": "Intervalo da imagem de fundo da proteção de tela", + "LabelBackdropScreensaverIntervalHelp": "Tempo em segundos entre as diferentes imagens de fundo do protetor de tela.", + "GridView": "Visualização em grelha", + "ListView": "Visualização em Lista", + "AiTranslated": "Traduzido por IA", + "MachineTranslated": "Traduzido por Máquina", + "AllowSegmentDeletion": "Remover segmentos", + "AllowSegmentDeletionHelp": "Remover segmentos antigos após serem enviados ao cliente. Isso previne armazenar o arquivo transcodificado em disco. Funciona apenas com limitação habilitada. Desligue esta opção se você experenciar problemas com a reprodução.", + "LabelThrottleDelaySeconds": "Limitar após", + "LabelThrottleDelaySecondsHelp": "Tempo em segundos em que o transcodificador será limitado. É necessário que seja grande o suficiente para que o cliente mantenha um buffer saudável. Funciona apenas se o limitador estiver habilitado.", + "LabelSegmentKeepSeconds": "Tempo para armazenar seguimentos", + "LabelSegmentKeepSecondsHelp": "Tempo em segundos para que os seguimentos sejam armazenados antes de serem sobrescritos. É necessário que seja maior que \"Limitar após\". Funciona apenas se a remoção de segmentos estiver habilitada." } From d1f2f1caa0bb313798f38a74f59bb8308c5ece22 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Sun, 24 Sep 2023 19:45:29 +0300 Subject: [PATCH 20/70] Add items helper --- src/utils/items.ts | 158 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/utils/items.ts diff --git a/src/utils/items.ts b/src/utils/items.ts new file mode 100644 index 0000000000..08936a3fde --- /dev/null +++ b/src/utils/items.ts @@ -0,0 +1,158 @@ +import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields'; +import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; +import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; +import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; +import * as userSettings from 'scripts/settings/userSettings'; +import { EpisodeFilter, FeatureFilters, LibraryViewSettings, ParentId, VideoBasicFilter, ViewMode } from '../types/library'; +import { LibraryTab } from 'types/libraryTab'; + +export const getVideoBasicFilter = (libraryViewSettings: LibraryViewSettings) => { + let isHd; + + if (libraryViewSettings.Filters?.VideoBasicFilter?.includes(VideoBasicFilter.IsHD)) { + isHd = true; + } + + if (libraryViewSettings.Filters?.VideoBasicFilter?.includes(VideoBasicFilter.IsSD)) { + isHd = false; + } + + return { + isHd, + is4K: libraryViewSettings.Filters?.VideoBasicFilter?.includes(VideoBasicFilter.Is4K) ? + true : + undefined, + is3D: libraryViewSettings.Filters?.VideoBasicFilter?.includes(VideoBasicFilter.Is3D) ? + true : + undefined + }; +}; + +export const getFeatureFilters = (libraryViewSettings: LibraryViewSettings) => { + return { + hasSubtitles: libraryViewSettings.Filters?.Features?.includes(FeatureFilters.HasSubtitles) ? + true : + undefined, + hasTrailer: libraryViewSettings.Filters?.Features?.includes(FeatureFilters.HasTrailer) ? + true : + undefined, + hasSpecialFeature: libraryViewSettings.Filters?.Features?.includes( + FeatureFilters.HasSpecialFeature + ) ? + true : + undefined, + hasThemeSong: libraryViewSettings.Filters?.Features?.includes(FeatureFilters.HasThemeSong) ? + true : + undefined, + hasThemeVideo: libraryViewSettings.Filters?.Features?.includes( + FeatureFilters.HasThemeVideo + ) ? + true : + undefined + }; +}; + +export const getEpisodeFilter = ( + viewType: LibraryTab, + libraryViewSettings: LibraryViewSettings +) => { + return { + parentIndexNumber: libraryViewSettings.Filters?.EpisodeFilter?.includes( + EpisodeFilter.ParentIndexNumber + ) ? + 0 : + undefined, + isMissing: + viewType === LibraryTab.Episodes ? + !!libraryViewSettings.Filters?.EpisodeFilter?.includes(EpisodeFilter.IsMissing) : + undefined, + isUnaired: libraryViewSettings.Filters?.EpisodeFilter?.includes(EpisodeFilter.IsUnaired) ? + true : + undefined + }; +}; + +const getItemFieldsEnum = ( + viewType: LibraryTab, + libraryViewSettings: LibraryViewSettings +) => { + const itemFields: ItemFields[] = []; + + if (viewType !== LibraryTab.Networks) { + itemFields.push(ItemFields.BasicSyncInfo, ItemFields.MediaSourceCount); + } + + if (libraryViewSettings.ImageType === ImageType.Primary) { + itemFields.push(ItemFields.PrimaryImageAspectRatio); + } + + if (viewType === LibraryTab.Networks) { + itemFields.push( + ItemFields.DateCreated, + ItemFields.PrimaryImageAspectRatio + ); + } + + return itemFields; +}; + +export const getFieldsQuery = ( + viewType: LibraryTab, + libraryViewSettings: LibraryViewSettings +) => { + return { + fields: getItemFieldsEnum(viewType, libraryViewSettings) + }; +}; + +export const getLimitQuery = () => { + return { + limit: userSettings.libraryPageSize(undefined) || undefined + }; +}; + +export const getAlphaPickerQuery = (libraryViewSettings: LibraryViewSettings) => { + const alphabetValue = libraryViewSettings.Alphabet !== null ? + libraryViewSettings.Alphabet : undefined; + + return { + nameLessThan: alphabetValue === '#' ? 'A' : undefined, + nameStartsWith: alphabetValue === '#' ? undefined : alphabetValue + }; +}; + +export const getFiltersQuery = ( + viewType: LibraryTab, + libraryViewSettings: LibraryViewSettings +) => { + return { + ...getFeatureFilters(libraryViewSettings), + ...getEpisodeFilter(viewType, libraryViewSettings), + ...getVideoBasicFilter(libraryViewSettings), + seriesStatus: libraryViewSettings?.Filters?.SeriesStatus, + videoTypes: libraryViewSettings?.Filters?.VideoTypes, + filters: libraryViewSettings?.Filters?.Status, + genres: libraryViewSettings?.Filters?.Genres, + officialRatings: libraryViewSettings?.Filters?.OfficialRatings, + tags: libraryViewSettings?.Filters?.Tags, + years: libraryViewSettings?.Filters?.Years, + studioIds: libraryViewSettings?.Filters?.StudioIds + }; +}; + +export const getSettingsKey = (viewType: LibraryTab, parentId: ParentId) => { + return `${viewType} - ${parentId}`; +}; + +export const getDefaultLibraryViewSettings = (): LibraryViewSettings => { + return { + ShowTitle: true, + ShowYear: false, + ViewMode: ViewMode.GridView, + ImageType: ImageType.Primary, + CardLayout: false, + SortBy: ItemSortBy.SortName, + SortOrder: SortOrder.Ascending, + StartIndex: 0 + }; +}; From ab0ffb857cb81491301e051824add0ca2e74bd55 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Mon, 2 Oct 2023 23:38:03 +0300 Subject: [PATCH 21/70] Add Play, Queue, Shuffle and NewCollection Buttons --- .../library/NewCollectionButton.tsx | 34 +++++++++++ .../components/library/PlayAllButton.tsx | 57 +++++++++++++++++++ .../components/library/QueueButton.tsx | 39 +++++++++++++ .../components/library/ShuffleButton.tsx | 49 ++++++++++++++++ .../components/library/SortButton.tsx | 2 +- .../components/library/ViewSettingsButton.tsx | 2 +- 6 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 src/apps/experimental/components/library/NewCollectionButton.tsx create mode 100644 src/apps/experimental/components/library/PlayAllButton.tsx create mode 100644 src/apps/experimental/components/library/QueueButton.tsx create mode 100644 src/apps/experimental/components/library/ShuffleButton.tsx diff --git a/src/apps/experimental/components/library/NewCollectionButton.tsx b/src/apps/experimental/components/library/NewCollectionButton.tsx new file mode 100644 index 0000000000..e337de7ddd --- /dev/null +++ b/src/apps/experimental/components/library/NewCollectionButton.tsx @@ -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 ( + + + + ); +}; + +export default NewCollectionButton; diff --git a/src/apps/experimental/components/library/PlayAllButton.tsx b/src/apps/experimental/components/library/PlayAllButton.tsx new file mode 100644 index 0000000000..d7fb090380 --- /dev/null +++ b/src/apps/experimental/components/library/PlayAllButton.tsx @@ -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 = ({ 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 ( + + + + ); +}; + +export default PlayAllButton; diff --git a/src/apps/experimental/components/library/QueueButton.tsx b/src/apps/experimental/components/library/QueueButton.tsx new file mode 100644 index 0000000000..fdc6a7666b --- /dev/null +++ b/src/apps/experimental/components/library/QueueButton.tsx @@ -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 = ({ item, items, hasFilters }) => { + const queue = useCallback(() => { + if (item && !hasFilters) { + playbackManager.queue({ + items: [item] + }); + } else { + playbackManager.queue({ + items: items + }); + } + }, [hasFilters, item, items]); + + return ( + + + + ); +}; + +export default QueueButton; diff --git a/src/apps/experimental/components/library/ShuffleButton.tsx b/src/apps/experimental/components/library/ShuffleButton.tsx new file mode 100644 index 0000000000..c81ee4c4ba --- /dev/null +++ b/src/apps/experimental/components/library/ShuffleButton.tsx @@ -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 = ({ 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 ( + + + + ); +}; + +export default ShuffleButton; diff --git a/src/apps/experimental/components/library/SortButton.tsx b/src/apps/experimental/components/library/SortButton.tsx index 7deeae349b..2c7425f0de 100644 --- a/src/apps/experimental/components/library/SortButton.tsx +++ b/src/apps/experimental/components/library/SortButton.tsx @@ -98,7 +98,7 @@ const SortButton: FC = ({ 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} > diff --git a/src/apps/experimental/components/library/ViewSettingsButton.tsx b/src/apps/experimental/components/library/ViewSettingsButton.tsx index cec5090acc..b1ca1679e0 100644 --- a/src/apps/experimental/components/library/ViewSettingsButton.tsx +++ b/src/apps/experimental/components/library/ViewSettingsButton.tsx @@ -100,7 +100,7 @@ const ViewSettingsButton: FC = ({ 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} > From 7ee0262c0c5bbecd74eea99d9fffbe53b372befc Mon Sep 17 00:00:00 2001 From: Prasaedonium Date: Tue, 3 Oct 2023 00:59:54 +0000 Subject: [PATCH 22/70] Translated using Weblate (Spanish (Mexico)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_MX/ --- src/strings/es-mx.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/strings/es-mx.json b/src/strings/es-mx.json index 206fc431bf..3fb0abb6a3 100644 --- a/src/strings/es-mx.json +++ b/src/strings/es-mx.json @@ -1773,5 +1773,6 @@ "MachineTranslated": "Traducido por Máquina", "ForeignPartsOnly": "Solamente partes Forzadas/Foráneas", "HearingImpairedShort": "HI/SDH", - "HeaderGuestCast": "Estrellas Invitadas" + "HeaderGuestCast": "Estrellas Invitadas", + "LabelIsHearingImpaired": "Para personas con discapacidad auditiva (SDH)" } From 3c443e29df87ee319977792047acff9a3f7eb650 Mon Sep 17 00:00:00 2001 From: Prasaedonium Date: Tue, 3 Oct 2023 00:54:43 +0000 Subject: [PATCH 23/70] Translated using Weblate (Spanish) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es/ --- src/strings/es.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/strings/es.json b/src/strings/es.json index 30ba43bb85..ab3ac87d95 100644 --- a/src/strings/es.json +++ b/src/strings/es.json @@ -1775,5 +1775,6 @@ "AiTranslated": "Traducido por IA", "MachineTranslated": "Traducido por Máquina", "HeaderGuestCast": "Estrellas Invitadas", - "ForeignPartsOnly": "Partes Forzadas/Foráneas solamente" + "ForeignPartsOnly": "Partes Forzadas/Foráneas solamente", + "LabelIsHearingImpaired": "Para personas con discapacidad auditiva (SDH)" } From ceb501907e4cb37dd588eed43d119cd199aa2d8a Mon Sep 17 00:00:00 2001 From: Slavi Asenov Date: Tue, 3 Oct 2023 04:55:29 +0000 Subject: [PATCH 24/70] Translated using Weblate (Bulgarian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/ --- src/strings/bg-bg.json | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/strings/bg-bg.json b/src/strings/bg-bg.json index f9f61bb285..21502d6402 100644 --- a/src/strings/bg-bg.json +++ b/src/strings/bg-bg.json @@ -528,7 +528,7 @@ "Screenshots": "Снимки на екрана", "Search": "Търсене", "SearchForCollectionInternetMetadata": "Търсене в интернет за картини и метаданни", - "SearchForMissingMetadata": "Търсене за лисващи метаданни", + "SearchForMissingMetadata": "Търсене за липсващи метаданни", "SearchForSubtitles": "Търсене на субтитри", "SendMessage": "Изпращане на съобщение", "SeriesYearToPresent": "{0} - Настояще", @@ -1166,11 +1166,11 @@ "MessageUnsetContentHelp": "Съдържанието ще се показва като обикновени папки. За най-добри резултати използвайте мениджъра на метаданни, за да зададете типовете съдържание на подпапките.", "MessageUnableToConnectToServer": "В момента не можем да се свържем с избрания сървър. Моля, уверете се, че работи и опитайте отново.", "MessageReenableUser": "Вижте по-долу, за да активирате отново", - "MessagePluginInstallDisclaimer": "Приставките, създадени от членове на общността, са чудесен начин да подобрите изживяването с Джелифин чрез допълнителните функции и предимства.Преди да инсталирате, имайте предвид ефектите, които те могат да имат върху вашия Джелифин сървър, като по-дълго време за сканиране на библиотеки, допълнителна обработка на заден фон и намалена стабилност на системата.", + "MessagePluginInstallDisclaimer": "ПРЕДУПРЕЖДЕНИЕ: Инсталирането на плъгин на трета страна носи рискове. Може да съдържа нестабилен или злонамерен код и може да се промени по всяко време. Инсталирайте само плъгини от автори на които имате доверие! Имайте предвид потенциалните ефекти, които може да има, включително заявки към външни услуги, по-дълги сканирания на библиотеки или допълнителна фонова обработка.", "MessagePluginConfigurationRequiresLocalAccess": "За да конфигурирате тази приставка, моля, впишете се директно в локалния си сървър.", "MessagePleaseWait": "Моля,изчакайте. Това може да отнеме минута.", "MessagePlayAccessRestricted": "Възпроизвеждането на това съдържание в момента е ограничено.Моля, свържете се с администратора на вашия сървър за повече информация.", - "MessagePasswordResetForUsers": "Следните потребители са занулили паролите си.Те вече могат да влязат с пин кодовете, използвани за извършване на нулирането.", + "MessagePasswordResetForUsers": "Следните потребители са занулили паролите си.Те вече могат да влязат с ПИН кодовете, използвани за извършване на нулирането.", "MessageNoTrailersFound": "За да подобрите филмовото изживяване инсталирайте канал за трейлъри,може да подредите няколко канала в библиотека.", "MessageNoServersAvailable": "Не са намерени сървъри, използващи функцията за автоматично откриване на сървър.", "MessageNoMovieSuggestionsAvailable": "Понастоящем няма предложени филми. Започнете да гледате и оценявате филмите си, а след това се върнете, за да видите препоръките си.", @@ -1452,7 +1452,7 @@ "LabelAlbumArtMaxResHelp": "Максимална резолюция на изображенията предоставена чрез \"upnp:albumArtURI\" полето.", "KnownProxiesHelp": "Списък от IP ареси или хост имена на известни прокси сървъри, разделени със запетая, използвани при свързване с Jellyfin сървър. Това е задължително за да се използва правилнен \"X-Forwarded-For\" хедър. Изисква рестартиране след прилагане.", "HomeVideosPhotos": "Домашни видеа и снимки", - "DirectPlayHelp": "Основният файл е напълно съвместим с този клиент, което значи че го получавате без модификации.", + "DirectPlayHelp": "Премахване на изображение", "AllowTonemappingHelp": "Тоналното картографиране може да трансформира динамичния обхват на видеото от HDR към SDR, като същевременно запазва детайлите и цветовете на изображението, които са много важна информация за представяне на оригиналната сцена. В момента работи само с 10-битови HDR10,HLG и DoVi видеоклипове. Това изисква съответното време за изпълнение от OpenCL или CUDA.", "LabelMaxAudiobookResumeHelp": "Приема се ,че файловете се възпроизведени до края , ако се спре след като оставащото време е по-малко от тази стойност.", "Experimental": "Експериментални", @@ -1478,5 +1478,10 @@ "EnableAudioNormalizationHelp": "Нормализацията на звука ще усили сигналът за да поддържа средните честоти на желано ниво (-18dB).", "EnableAudioNormalization": "Нормализация на звука", "Unknown": "Неизвестен", - "LabelThrottleDelaySeconds": "Ограничи след" + "LabelThrottleDelaySeconds": "Ограничи след", + "GetThePlugin": "Вземете приставката", + "LabelLocalCustomCss": "Персонализиран CSS код за стилизиране, който се отнася само за този клиент. Може да искате да деактивирате персонализирания CSS код на сървъра.", + "LabelOriginalName": "Оригинално име", + "LabelQuickConnectCode": "Код за бързо свързване", + "LabelMaxVideoResolution": "Максимално разрешена разделителна способност на транскодиране на видео" } From d8b36ed152e7a27b0816bb43f542ba1ac6a6dffc Mon Sep 17 00:00:00 2001 From: FuXiang Shu Date: Tue, 3 Oct 2023 04:23:57 +0000 Subject: [PATCH 25/70] Translated using Weblate (Malay) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ms/ --- src/strings/ms.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/strings/ms.json b/src/strings/ms.json index 1accc4dbb6..6a000566e9 100644 --- a/src/strings/ms.json +++ b/src/strings/ms.json @@ -4,7 +4,7 @@ "LabelFinish": "Habis", "LabelYoureDone": "Kamu Selesai!", "ParentalRating": "Parental Rating", - "SettingsSaved": "Seting Disimpan.", + "SettingsSaved": "Tetapan Disimpan.", "Absolute": "Mutlak", "AccessRestrictedTryAgainLater": "Akses dihalang pada masa ini. Sila cuba sebentar lagi.", "Actor": "Pelakon", @@ -249,5 +249,7 @@ "AllowCollectionManagement": "Benarkan pengguna ini meguruskan koleksi", "AllowSegmentDeletion": "Padam segment", "AllowSegmentDeletionHelp": "Padam segmen lama setelah ia dihantar ke pelayan. Ini menghalang file transcode disimpan dalam disk. Ia akan berfungsi dengan penghad haju dihidupkan. Matikan tetapan ini jika anda mengalami isu dengan pemain video.", - "LabelThrottleDelaySeconds": "Penghad laju setelah" + "LabelThrottleDelaySeconds": "Penghad laju setelah", + "Settings": "Tetapan", + "SelectServer": "Pilih pelayan" } From c794b51995049fffdf15b7167a58782954ba7633 Mon Sep 17 00:00:00 2001 From: Nicolas Berens Date: Tue, 3 Oct 2023 08:23:33 +0000 Subject: [PATCH 26/70] Translated using Weblate (German) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/ --- src/strings/de.json | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/strings/de.json b/src/strings/de.json index c136697d84..625489b77f 100644 --- a/src/strings/de.json +++ b/src/strings/de.json @@ -1377,7 +1377,7 @@ "LabelColorSpace": "Farbraum", "MediaInfoColorSpace": "Farbraum", "VideoAudio": "Videoton", - "AllowTonemappingHelp": "Tone-Mapping kann den Dynamikumfang eines Videos von HDR nach SDR wandeln und dabei die für die Darstellung der Originalszene sehr wichtigen Bilddetails und Farben beibehalten. Dies funktioniert zurzeit nur bei HDR10-, HLG- und Dolby-Vision-Videos und benötigt die entsprechende OpenCL- oder CUDA-Laufzeitumgebung.", + "AllowTonemappingHelp": "Tone-Mapping kann den Dynamikumfang eines Videos von HDR nach SDR wandeln und dabei die für die Darstellung der Originalszene sehr wichtigen Bilddetails und Farben beibehalten. Dies funktioniert zurzeit nur bei HDR10, HLG und Dolby-Vision Videos und benötigt die entsprechende OpenCL- oder CUDA-Laufzeitumgebung.", "TonemappingRangeHelp": "Wähle den Ausgabefarbraum aus. Auto ist derselbe wie der Eingabefarbraum.", "TonemappingAlgorithmHelp": "Das Tone-Mapping kann fein abgestimmt werden. Wenn du mit diesen Optionen nicht vertraut bist, behalte einfach den Standardwert bei. Der empfohlene Wert ist \"BT.2390\".", "LabelTonemappingAlgorithm": "Wähle den zu verwendenden Tone-Mapping-Algorithmus aus", @@ -1407,7 +1407,7 @@ "QuickConnectDescription": "Für das Einloggen mit Quick Connect wähle den 'Quick Connect'-Knopf auf deinem Gerät, mit dem du dich anmelden möchtest, und gib den unten angezeigten Code ein.", "QuickConnectDeactivated": "Quick Connect wurde deaktiviert, bevor der Login verifiziert werden konnte", "QuickConnectAuthorizeFail": "Unbekannter Quick Connect-Code", - "QuickConnectAuthorizeSuccess": "Anfrage autorisiert", + "QuickConnectAuthorizeSuccess": "Das Gerät wurde erfolgreich authentifiziert!", "QuickConnectAuthorizeCode": "Login Code {0} eingeben", "QuickConnectActivationSuccessful": "Erfolgreich aktiviert", "EnableQuickConnect": "Quick Connect auf diesem Server aktivieren", @@ -1733,7 +1733,7 @@ "PasswordRequiredForAdmin": "Für Admin Konten wird ein Passwort benötigt.", "LabelEnableLUFSScan": "LUFS-Scan aktivieren", "LabelSyncPlayNoGroups": "Keine Gruppen verfügbar", - "LabelEnableLUFSScanHelp": "Aktiviert den LUFS-Scan für Musik (Dies erfordert mehr Zeit und Ressourcen).", + "LabelEnableLUFSScanHelp": "Clients können die Audio Wiedergabe normalisieren um die selbe Lautstärke für mehrere Stücke zu bekommen.\nDie verlängert den Bibiliotheks Scan und benötigt mehr Ressourcen.", "Notifications": "Benachrichtigungen", "NotificationsMovedMessage": "Die Benachrichtigungsfunktion wurde zum Webhook Plugin verschoben.", "EnableAudioNormalizationHelp": "Die Audionormalisierung fügt eine konstante Verstärkung hinzu, um den Durchschnitt auf einem gewünschten Pegel zu halten (-18 dB).", @@ -1769,5 +1769,11 @@ "GoHome": "Startseite", "AiTranslated": "AI übersetzt", "MachineTranslated": "maschinenübersetzt", - "AllowAv1Encoding": "Encodierung ins AV1 Format erlauben" + "AllowAv1Encoding": "Encodierung ins AV1 Format erlauben", + "LabelIsHearingImpaired": "Für Hörgeschädigte (SDH)", + "LabelBackdropScreensaverInterval": "Hintergrund Bildschirmschoner Interval", + "BackdropScreensaver": "Hintergrund Bildschirmschoner", + "ForeignPartsOnly": "Erzwungen/Nur ausländische Teile", + "HearingImpairedShort": "BaFa/SDH", + "HeaderGuestCast": "Gast Stars" } From 6f645bf0b279bcba5d064f5c9a40bcbb07adb430 Mon Sep 17 00:00:00 2001 From: R1ckj Date: Tue, 3 Oct 2023 07:52:34 +0000 Subject: [PATCH 27/70] Translated using Weblate (Italian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/it/ --- src/strings/it.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/strings/it.json b/src/strings/it.json index df915cbaca..49aa147f18 100644 --- a/src/strings/it.json +++ b/src/strings/it.json @@ -1762,5 +1762,7 @@ "LabelThrottleDelaySecondsHelp": "Tempo in secondi dopo cui il transcodificatore sarà messo in throttle. Deve essere sufficientemente grande perché il client mantenga un buon buffer. Funziona solo se il throttling è abilitato.", "LabelSegmentKeepSeconds": "Il tempo per cui tenere i segmenti", "LabelSegmentKeepSecondsHelp": "Tempo in secondi per cui i segmenti saranno tenuti prima di essere sovrascritti. Deve essere più grande di \"Throttle dopo\". Funziona solo se l'eliminazione dei segmenti è abilitata.", - "AllowAv1Encoding": "Permetti la codifica nel formato AV1" + "AllowAv1Encoding": "Permetti la codifica nel formato AV1", + "GoHome": "Vai alla Home", + "GridView": "Vista Griglia" } From 9fa62ffc63038798bacbd8ede357f029fc4ffd13 Mon Sep 17 00:00:00 2001 From: engelkek Date: Tue, 3 Oct 2023 12:20:33 +0000 Subject: [PATCH 28/70] Translated using Weblate (German) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/ --- src/strings/de.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/strings/de.json b/src/strings/de.json index 625489b77f..4ec655c886 100644 --- a/src/strings/de.json +++ b/src/strings/de.json @@ -1771,9 +1771,10 @@ "MachineTranslated": "maschinenübersetzt", "AllowAv1Encoding": "Encodierung ins AV1 Format erlauben", "LabelIsHearingImpaired": "Für Hörgeschädigte (SDH)", - "LabelBackdropScreensaverInterval": "Hintergrund Bildschirmschoner Interval", + "LabelBackdropScreensaverInterval": "Hintergrund-Bildschirmschoner-Intervall", "BackdropScreensaver": "Hintergrund Bildschirmschoner", "ForeignPartsOnly": "Erzwungen/Nur ausländische Teile", "HearingImpairedShort": "BaFa/SDH", - "HeaderGuestCast": "Gast Stars" + "HeaderGuestCast": "Gast Stars", + "LabelBackdropScreensaverIntervalHelp": "Die Zeit in Sekunden zwischen dem Wechsel verschiedener Hintergrundbilder im Bildschirmschoner" } From 71089846d1e617cb454f1d4cb18f4827d71a6b37 Mon Sep 17 00:00:00 2001 From: Lukas H Date: Tue, 3 Oct 2023 13:49:08 +0000 Subject: [PATCH 29/70] Translated using Weblate (Lithuanian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/lt/ --- src/strings/lt-lt.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/strings/lt-lt.json b/src/strings/lt-lt.json index 16694a71ed..5a00e5fba0 100644 --- a/src/strings/lt-lt.json +++ b/src/strings/lt-lt.json @@ -1162,5 +1162,7 @@ "LabelMaxVideoResolution": "Maksimali leistina video transkodavimo resoliucija", "LabelEnableAudioVbrHelp": "Kintama bitų sparta siūlo geresnę kokybę lyginant su vidutine bitų sparta, bet retais atvejais gali sukelti krovimo ir palaikymo problemas.", "LabelKodiMetadataEnablePathSubstitutionHelp": "Įjungti kelio pakeitimą nuotraukoms naudojant serverio kelio pakeitimo nustatymus.", - "LabelKodiMetadataDateFormatHelp": "Visos datos iš NFO failų bus ištraukiamos šiuo formatu." + "LabelKodiMetadataDateFormatHelp": "Visos datos iš NFO failų bus ištraukiamos šiuo formatu.", + "AllowSegmentDeletion": "Ištrinti segmentus", + "AllowSegmentDeletionHelp": "Ištrinkite senus segmentus, kai jie buvo išsiųsti klientui. Taip išvengiama viso perkoduoto failo saugojimo diske. Veiks tik su įjungtu droseliu. Išjunkite tai, jei kyla atkūrimo problemų." } From 62f9e7581acf445acf6a2aac4158ada2158c0aac Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 3 Oct 2023 13:15:52 -0400 Subject: [PATCH 30/70] Fix add user route --- src/apps/dashboard/routes/_asyncRoutes.ts | 6 +++--- src/apps/dashboard/routes/users/{new.tsx => add.tsx} | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename src/apps/dashboard/routes/users/{new.tsx => add.tsx} (100%) diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts index 8cae668770..09d40de0e8 100644 --- a/src/apps/dashboard/routes/_asyncRoutes.ts +++ b/src/apps/dashboard/routes/_asyncRoutes.ts @@ -3,10 +3,10 @@ import type { AsyncRoute } from 'components/router/AsyncRoute'; export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ { path: 'activity' }, { path: 'notifications' }, - { path: 'users/new' }, { path: 'users' }, - { path: 'users/profile' }, { path: 'users/access' }, + { path: 'users/add' }, { path: 'users/parentalcontrol' }, - { path: 'users/password' } + { path: 'users/password' }, + { path: 'users/profile' } ]; diff --git a/src/apps/dashboard/routes/users/new.tsx b/src/apps/dashboard/routes/users/add.tsx similarity index 100% rename from src/apps/dashboard/routes/users/new.tsx rename to src/apps/dashboard/routes/users/add.tsx From 1a8f24ba9eef26e71c68da1888e2188de973a496 Mon Sep 17 00:00:00 2001 From: BartalD Date: Tue, 3 Oct 2023 15:22:31 -0400 Subject: [PATCH 31/70] Added translation using Weblate (Faroese) --- src/strings/fo.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/strings/fo.json diff --git a/src/strings/fo.json b/src/strings/fo.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/src/strings/fo.json @@ -0,0 +1 @@ +{} From 09441da88a430ae2275f81b772fb1997031df2b3 Mon Sep 17 00:00:00 2001 From: forkh Date: Tue, 3 Oct 2023 19:28:01 +0000 Subject: [PATCH 32/70] Translated using Weblate (Faroese) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fo/ --- src/strings/fo.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/strings/fo.json b/src/strings/fo.json index 0967ef424b..9b15385fd9 100644 --- a/src/strings/fo.json +++ b/src/strings/fo.json @@ -1 +1,3 @@ -{} +{ + "AccessRestrictedTryAgainLater": "Atgongd er avmarkað. Vinaliga prøva aftur seinni." +} From bcf6ecb6ddb26f162c589876ccd17d5f36bae76a Mon Sep 17 00:00:00 2001 From: krvi Date: Tue, 3 Oct 2023 19:30:51 +0000 Subject: [PATCH 33/70] Translated using Weblate (Faroese) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fo/ --- src/strings/fo.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/strings/fo.json b/src/strings/fo.json index 9b15385fd9..b02892480a 100644 --- a/src/strings/fo.json +++ b/src/strings/fo.json @@ -1,3 +1,4 @@ { - "AccessRestrictedTryAgainLater": "Atgongd er avmarkað. Vinaliga prøva aftur seinni." + "AccessRestrictedTryAgainLater": "Atgongd er avmarkað. Vinaliga royn aftur seinni.", + "Actor": "Sjónleikari" } From d29b47f54be51b76ad57cb13cc4e00cd18754bfc Mon Sep 17 00:00:00 2001 From: forkh Date: Tue, 3 Oct 2023 19:37:19 +0000 Subject: [PATCH 34/70] Translated using Weblate (Faroese) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fo/ --- src/strings/fo.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/strings/fo.json b/src/strings/fo.json index b02892480a..2a8f445abe 100644 --- a/src/strings/fo.json +++ b/src/strings/fo.json @@ -1,4 +1,9 @@ { "AccessRestrictedTryAgainLater": "Atgongd er avmarkað. Vinaliga royn aftur seinni.", - "Actor": "Sjónleikari" + "Actor": "Sjónleikari", + "Add": "Legg afturat", + "AddedOnValue": "{0} lagt afturat", + "AddToCollection": "Koyr í samling", + "AddToFavorites": "Legg til yndislistan", + "AddToPlaylist": "Legg til spælilistan" } From 359dffb73df333a977e0ef10ad67fda7e345aad3 Mon Sep 17 00:00:00 2001 From: BartalD Date: Tue, 3 Oct 2023 20:16:11 +0000 Subject: [PATCH 35/70] Translated using Weblate (Faroese) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fo/ --- src/strings/fo.json | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/strings/fo.json b/src/strings/fo.json index 2a8f445abe..2c028c71a2 100644 --- a/src/strings/fo.json +++ b/src/strings/fo.json @@ -5,5 +5,21 @@ "AddedOnValue": "{0} lagt afturat", "AddToCollection": "Koyr í samling", "AddToFavorites": "Legg til yndislistan", - "AddToPlaylist": "Legg til spælilistan" + "AddToPlaylist": "Legg til spælilistan", + "Alerts": "Ávaringar", + "All": "Øll", + "AllEpisodes": "Allir partar", + "AllLanguages": "Øll tungumál", + "LabelTonemappingMode": "Tónaavmyndingarháttur", + "HearingImpairedShort": "Hoyriveik/SDH", + "MachineTranslated": "Maskintýðing", + "AiTranslated": "Vitlíkistýðing", + "AllowAv1Encoding": "Loyva koding í AV1 bygnaði", + "LabelIsHearingImpaired": "Til hoyriveik (SDH)", + "Unknown": "Ókend", + "TonemappingModeHelp": "Vel tónaavmyndingarháttin. Um tú verður fyri útblástum hálýsingum, royn so heldur RGB-støðuna.", + "Unreleased": "Ikki latið út enn", + "AlbumArtist": "Album Listafólk", + "AllChannels": "Allar rásir", + "AllComplexFormats": "Allar Kompleksu Formatir (ASS, SSA, VobSub, PGS, SUB, IDX, ...)" } From a3193c5b4922fd168faa53d7b26bb83f1ab961b7 Mon Sep 17 00:00:00 2001 From: krvi Date: Tue, 3 Oct 2023 20:09:48 +0000 Subject: [PATCH 36/70] Translated using Weblate (Faroese) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fo/ --- src/strings/fo.json | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/strings/fo.json b/src/strings/fo.json index 2c028c71a2..d348fa1e1a 100644 --- a/src/strings/fo.json +++ b/src/strings/fo.json @@ -21,5 +21,22 @@ "Unreleased": "Ikki latið út enn", "AlbumArtist": "Album Listafólk", "AllChannels": "Allar rásir", - "AllComplexFormats": "Allar Kompleksu Formatir (ASS, SSA, VobSub, PGS, SUB, IDX, ...)" + "AllComplexFormats": "Allar Kompleksu Formatir (ASS, SSA, VobSub, PGS, SUB, IDX, ...)", + "Directors": "Leikstjórar", + "AgeValue": "({0} ára gamalt)", + "AllLibraries": "Øll søvn", + "Artist": "Listafólk", + "Artists": "Listafólk", + "Books": "Bøkur", + "Composer": "Tónaskald", + "DailyAt": "Dagligani kl. {0}", + "DashboardVersionNumber": "Útgáva: {0}", + "DeathDateValue": "Deyð(ur): {0}", + "Digital": "Talgilt", + "Director": "Leikstjóri", + "Friday": "Fríggjadag", + "HeaderAdmin": "Umsiting", + "HeaderDevices": "Eindir", + "HeaderError": "Feilur", + "HeaderForKids": "Fyri Børn" } From a4601efc583146a9780832092f0d39a7a59f9b2f Mon Sep 17 00:00:00 2001 From: BartalD Date: Tue, 3 Oct 2023 20:22:43 +0000 Subject: [PATCH 37/70] Translated using Weblate (Faroese) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fo/ --- src/strings/fo.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/strings/fo.json b/src/strings/fo.json index d348fa1e1a..c901edc34a 100644 --- a/src/strings/fo.json +++ b/src/strings/fo.json @@ -38,5 +38,18 @@ "HeaderAdmin": "Umsiting", "HeaderDevices": "Eindir", "HeaderError": "Feilur", - "HeaderForKids": "Fyri Børn" + "HeaderForKids": "Fyri Børn", + "AllowSegmentDeletion": "Strika partar", + "LabelThrottleDelaySeconds": "Kyrkja", + "AllowMediaConversion": "miðla", + "AllowOnTheFlySubtitleExtraction": "undirteksta", + "AllowCollectionManagement": "brúkara", + "AllowFfmpegThrottling": "Kyrkja", + "AllowFfmpegThrottlingHelp": "umkoding avspæling avspælingar", + "AllowSegmentDeletionHelp": "avspælingar", + "LabelSegmentKeepSecondsHelp": "Kyrkja parta", + "AllowHWTranscodingHelp": "avkoda ambætarinum", + "AllowMediaConversionHelp": "miðla", + "AllowOnTheFlySubtitleExtractionHelp": "Íkervnir undirtekstir avspæling íkervnar undirtekstir", + "AllowRemoteAccess": "ambætaran" } From a2d5c468be03f2119c91d94320377b5df378d6a3 Mon Sep 17 00:00:00 2001 From: krvi Date: Tue, 3 Oct 2023 20:23:13 +0000 Subject: [PATCH 38/70] Translated using Weblate (Faroese) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fo/ --- src/strings/fo.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/strings/fo.json b/src/strings/fo.json index c901edc34a..04d766248a 100644 --- a/src/strings/fo.json +++ b/src/strings/fo.json @@ -51,5 +51,14 @@ "AllowHWTranscodingHelp": "avkoda ambætarinum", "AllowMediaConversionHelp": "miðla", "AllowOnTheFlySubtitleExtractionHelp": "Íkervnir undirtekstir avspæling íkervnar undirtekstir", - "AllowRemoteAccess": "ambætaran" + "AllowRemoteAccess": "ambætaran", + "HeaderPassword": "Loyniorð", + "HeaderLibraries": "Søvn", + "HeaderParentalRatings": "Aldursmark", + "HeaderSecondsValue": "{0} sekund", + "HeaderSendMessage": "Send boð", + "Kids": "Børn", + "LabelArtists": "Listafólk", + "LabelCountry": "Land", + "LabelDeveloper": "Mennari" } From 5968e7637bdc1dab7f2de41e6be8364d49c72355 Mon Sep 17 00:00:00 2001 From: forkh Date: Tue, 3 Oct 2023 22:41:59 +0000 Subject: [PATCH 39/70] Translated using Weblate (Faroese) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fo/ --- src/strings/fo.json | 158 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 155 insertions(+), 3 deletions(-) diff --git a/src/strings/fo.json b/src/strings/fo.json index 04d766248a..6532f3e13a 100644 --- a/src/strings/fo.json +++ b/src/strings/fo.json @@ -12,7 +12,7 @@ "AllLanguages": "Øll tungumál", "LabelTonemappingMode": "Tónaavmyndingarháttur", "HearingImpairedShort": "Hoyriveik/SDH", - "MachineTranslated": "Maskintýðing", + "MachineTranslated": "Maskin týðing", "AiTranslated": "Vitlíkistýðing", "AllowAv1Encoding": "Loyva koding í AV1 bygnaði", "LabelIsHearingImpaired": "Til hoyriveik (SDH)", @@ -47,7 +47,7 @@ "AllowFfmpegThrottling": "Kyrkja", "AllowFfmpegThrottlingHelp": "umkoding avspæling avspælingar", "AllowSegmentDeletionHelp": "avspælingar", - "LabelSegmentKeepSecondsHelp": "Kyrkja parta", + "LabelSegmentKeepSecondsHelp": "Kyrkja parta.", "AllowHWTranscodingHelp": "avkoda ambætarinum", "AllowMediaConversionHelp": "miðla", "AllowOnTheFlySubtitleExtractionHelp": "Íkervnir undirtekstir avspæling íkervnar undirtekstir", @@ -60,5 +60,157 @@ "Kids": "Børn", "LabelArtists": "Listafólk", "LabelCountry": "Land", - "LabelDeveloper": "Mennari" + "LabelDeveloper": "Mennari", + "Absolute": "Absolut", + "AddToPlayQueue": "Legg til spæl bíðirøð", + "AirDate": "Útgávu ár", + "Aired": "Útgivið", + "Album": "Album", + "Albums": "Album", + "LabelSegmentKeepSeconds": "Tíð at halda petti", + "AroundTime": "Umleið {0}", + "Ascending": "Hækkandi", + "SearchForMissingMetadata": "Leita eftir manglandi metadata", + "SearchForSubtitles": "Leita eftir undirtekstið", + "SearchResults": "Leitiúrslit", + "SelectServer": "Vel ambætara", + "SendMessage": "Send boð", + "Season": "Sesong", + "SeriesDisplayOrderHelp": "Raða partar eftir dato, DVD ordan ella absolut nummerering.", + "ServerNameIsRestarting": "Ambætarin á {0} endurbyrjar.", + "ServerNameIsShuttingDown": "Ambætarin á {0} sløknar.", + "ServerUpdateNeeded": "Hesin ambætarin má dagførast. Fyri at heinta nýggjastu útgávuna, vinarliga vitja {0}.", + "Share": "Býta", + "ShowIndicatorsFor": "Vís indikator fyri", + "ShowLess": "Vís minni", + "ShowMore": "Vís meiri", + "ShowParentImages": "Vís røð myndir", + "Shuffle": "Blanda", + "Shows": "Røðir", + "ShowTitle": "Vís heitið", + "Small": "Lítið", + "SmallCaps": "Lítlir stavir", + "Smaller": "Minni", + "Songs": "Sangir", + "Sort": "Skipa", + "SpecialFeatures": "Serstøk eyðkenni", + "StopRecording": "Steðga upptøku", + "Studio": "Filmsfelag", + "Studios": "Filmsfeløg", + "Subtitle": "Undirtekstur", + "SubtitleCyan": "Blágrønur", + "SubtitleGreen": "Grønt", + "SubtitleOffset": "Undirtekstur offset", + "SubtitleRed": "Reytt", + "Subtitles": "Undirtekstur", + "Suggestions": "Uppskot", + "Sync": "Synkronisera", + "SyncPlayGroupDefaultTitle": "{0}'sa bólkur", + "TabAdvanced": "Framkomin", + "TabCatalog": "Skrá", + "TabDashboard": "Kunningarbretti", + "TabDirectPlay": "Beinleiðis avspæling", + "TabLatest": "Lagt afturat fyri stuttum", + "TabLogs": "Gerðalistar", + "TabNetworking": "Net", + "TabNfoSettings": "NFO stillingar", + "TabPlugins": "Ískoytisforrit", + "TabProfiles": "Vangamyndir", + "TabRepositories": "Goymslur", + "TabServer": "Ambætari", + "TabStreaming": "Stroyming", + "TabUpcoming": "Komandi", + "Tags": "Frámerkir", + "TellUsAboutYourself": "Fortel um teg sjálvan", + "ThemeSongs": "Tema sangir", + "ThemeVideos": "Tema sjónbond", + "ThumbCard": "Tummlakort", + "TitleHardwareAcceleration": "Tólbúnað ferðøking", + "TitleHostingSettings": "Hýsingastillingar", + "TrackCount": "{0} spor", + "Trailers": "Forfilmar", + "Tuesday": "Týsdag", + "TV": "Sjónvarp", + "TypeOptionPluralAudio": "Ljóð", + "TypeOptionPluralBoxSet": "Boks sett", + "TypeOptionPluralMusicAlbum": "Tónleika album", + "TypeOptionPluralMusicVideo": "Tónleikasjónbond", + "TypeOptionPluralSeason": "Sesongir", + "TypeOptionPluralVideo": "Sjónbond", + "Typewriter": "Skrivimaskina", + "Uniform": "Einsháttað", + "Unmute": "Skrúva ljóðið upp", + "Unrated": "Eingin meting", + "Up": "Upp", + "ValueAudioCodec": "Ljóð codec: {0}", + "ValueCodec": "Codec: {0}", + "ValueContainer": "Bingja: {0}", + "ValueEpisodeCount": "{0} partar", + "ValueMinutes": "{0} min", + "ValueMovieCount": "{0} filmar", + "ValueOneEpisode": "1 partur", + "ValueOneMovie": "1 filmur", + "ValueOneMusicVideo": "1 tónleika sjónband", + "ValueOneSeries": "1 røð", + "ValueSeconds": "{0} sekund", + "ValueSeriesCount": "{0} røðir", + "Sports": "Ítróttur", + "Smart": "Smart", + "ShowYear": "Vís árið", + "SimultaneousConnectionLimitHelp": "Mest loyvda antalið av samstundis stroymingum. Skriva 0 fyri einki hámark.", + "Settings": "Stillingar", + "SettingsSaved": "Stillingar eru goymdar.", + "SelectAdminUsername": "Vinarliga vel eitt brúkaranavn til fyrisitara kontu.", + "Series": "Røð", + "SeriesCancelled": "Røðin er steðga.", + "Search": "Leita", + "SearchForCollectionInternetMetadata": "Leita eftir list og metadata á alnótini", + "SeriesSettings": "Røð stillingar", + "SeriesYearToPresent": "{0} - Núverandi", + "ServerRestartNeededAfterPluginInstall": "Jellyfin má endurbyrjast, aftaná at eitt ískoytisforrit er lagt inn.", + "ShowAdvancedSettings": "Vís framkomnar stillingar", + "StopPlayback": "Steðga avspæling", + "SubtitleGray": "Grátt", + "SubtitleLightGray": "Ljósagráður", + "SubtitleMagenta": "Viólreyður", + "AllowedRemoteAddressesHelp": "Komma býttur listið av IP addressum ella IP/netmask inngangur fyri netverk, ið verða loyvd at fjarbinda. Um hesin teigur er blankur, so eru allar fjar addressur loyvdar.", + "SubtitleWhite": "Hvítt", + "Arranger": "Fyrireikari", + "SubtitleYellow": "Gult", + "AskAdminToCreateLibrary": "Bið ein fyrisitari upprætta eitt savn.", + "TabAccess": "Atgongd", + "TypeOptionPluralEpisode": "Partar", + "TabContainers": "Kassar", + "TitlePlayback": "Avspæling", + "TabMusic": "Tónleikur", + "TabOther": "Annað", + "TagsValue": "Frámerkir: {0}", + "TabMyPlugins": "Míni ískoytisforrit", + "TabParentalControl": "Foreldra ræði", + "Sunday": "Sunnudag", + "SubtitleBlack": "Svart", + "TabResponses": "Svar", + "SubtitleBlue": "Blátt", + "TextSent": "Tekst sent.", + "TheseSettingsAffectSubtitlesOnThisDevice": "Hesir stillingar ávirka undirteksir á hesari eindini", + "Thumb": "Tummil", + "Thursday": "Hósdagur", + "Track": "Spor", + "Transcoding": "Umkodning", + "TypeOptionPluralBook": "Bøkur", + "TypeOptionPluralMovie": "Filmar", + "TypeOptionPluralMusicArtist": "Tónleikarar", + "TypeOptionPluralSeries": "Sjónvarpsrøðir", + "UnknownError": "Ein ókendur feilur hendi.", + "Unplayed": "Ikki spældur", + "Upload": "Send upp", + "UseEpisodeImagesInNextUp": "Brúka partamyndir í 'Komandi' og 'Hyggj víðari' teigum", + "UserMenu": "Brúkara valmynd", + "ValueAlbumCount": "{0} album", + "ValueConditions": "Treytir: {0}", + "ValueDiscNumber": "Fløga {0}", + "ValueMusicVideoCount": "{0} tónleika sjónbond", + "ValueOneAlbum": "1 album", + "ValueOneSong": "1 sangur", + "ValueSongCount": "{0} sangir" } From f758aea13b9515d46ea8413ff848101d6a838002 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Wed, 4 Oct 2023 00:45:17 +0300 Subject: [PATCH 40/70] fix: Use userId from params --- src/utils/jellyfin-apiclient/getItems.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/jellyfin-apiclient/getItems.ts b/src/utils/jellyfin-apiclient/getItems.ts index 4bbe711f81..37d35f840e 100644 --- a/src/utils/jellyfin-apiclient/getItems.ts +++ b/src/utils/jellyfin-apiclient/getItems.ts @@ -67,7 +67,7 @@ function mergeResults(results: BaseItemDtoQueryResult[]) { export function getItems(apiClient: ApiClient, userId: string, options?: GetItemsRequest) { const ids = options?.Ids?.split(','); if (!options || !ids || ids.length <= ITEMS_PER_REQUEST_LIMIT) { - return apiClient.getItems(apiClient.getCurrentUserId(), options); + return apiClient.getItems(userId, options); } const results = getItemsSplit(apiClient, userId, options); From c077a177e376553f57614331358e64bd70400d05 Mon Sep 17 00:00:00 2001 From: EddieFAF Date: Wed, 4 Oct 2023 17:18:53 +0000 Subject: [PATCH 41/70] Translated using Weblate (German) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/ --- src/strings/de.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/strings/de.json b/src/strings/de.json index 4ec655c886..0e2dd77491 100644 --- a/src/strings/de.json +++ b/src/strings/de.json @@ -1733,7 +1733,7 @@ "PasswordRequiredForAdmin": "Für Admin Konten wird ein Passwort benötigt.", "LabelEnableLUFSScan": "LUFS-Scan aktivieren", "LabelSyncPlayNoGroups": "Keine Gruppen verfügbar", - "LabelEnableLUFSScanHelp": "Clients können die Audio Wiedergabe normalisieren um die selbe Lautstärke für mehrere Stücke zu bekommen.\nDie verlängert den Bibiliotheks Scan und benötigt mehr Ressourcen.", + "LabelEnableLUFSScanHelp": "Clients können die Audio Wiedergabe normalisieren, um die selbe Lautstärke für mehrere Stücke zu bekommen.\nDies verlängert den Bibliotheksscan und benötigt mehr Ressourcen.", "Notifications": "Benachrichtigungen", "NotificationsMovedMessage": "Die Benachrichtigungsfunktion wurde zum Webhook Plugin verschoben.", "EnableAudioNormalizationHelp": "Die Audionormalisierung fügt eine konstante Verstärkung hinzu, um den Durchschnitt auf einem gewünschten Pegel zu halten (-18 dB).", @@ -1758,8 +1758,8 @@ "HeaderEpisodesStatus": "Episodenstatus", "AllowSegmentDeletion": "Segmente löschen", "AllowSegmentDeletionHelp": "Alte Segmente löschen, nachdem sie zum Client gesendet wurden. Damit muss nicht die gesamte transkodierte Datei zwischengespeichert werden. Sollten Wiedergabeprobleme auftreten, kann diese Einstellung deaktiviert werden.", - "LabelThrottleDelaySeconds": "Limitieren nach", - "LabelThrottleDelaySecondsHelp": "Zeit, in Sekunden, nach der die Transkodierung limitiert wird. Muss groß genug sein um dem Client eine problemlose Wiedergabe zu ermöglichen. Funktioniert nur wenn \"Transkodierung drosseln\" aktiviert ist.", + "LabelThrottleDelaySeconds": "Drosseln nach", + "LabelThrottleDelaySecondsHelp": "Zeit, in Sekunden, nach der die Transkodierung gedrosselt wird. Muss groß genug sein um dem Client eine problemlose Wiedergabe zu ermöglichen. Funktioniert nur wenn \"Transkodierung drosseln\" aktiviert ist.", "LabelSegmentKeepSeconds": "Zeit um Segmente zu behalten", "LabelSegmentKeepSecondsHelp": "Zeit, in Sekunden, in der Segmente nicht überschrieben werden dürfen. Muss größer sein als \"Limitieren nach\". Funktioniert nur wenn \"Segmente löschen\" aktiviert ist.", "LogoScreensaver": "Logo Bildschirmschoner", @@ -1776,5 +1776,5 @@ "ForeignPartsOnly": "Erzwungen/Nur ausländische Teile", "HearingImpairedShort": "BaFa/SDH", "HeaderGuestCast": "Gast Stars", - "LabelBackdropScreensaverIntervalHelp": "Die Zeit in Sekunden zwischen dem Wechsel verschiedener Hintergrundbilder im Bildschirmschoner" + "LabelBackdropScreensaverIntervalHelp": "Die Zeit in Sekunden zwischen dem Wechsel verschiedener Hintergrundbilder im Bildschirmschoner." } From 480e683ad606ebf4fde1cbdb9a021fdea9085c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cosmin=20Cioacl=C4=83?= Date: Tue, 3 Oct 2023 22:02:10 +0200 Subject: [PATCH 42/70] fix: remove unnecessary renaming --- CONTRIBUTORS.md | 1 + src/components/itemContextMenu.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index eccfd4cf27..920fd92885 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -66,6 +66,7 @@ - [Fishbigger](https://github.com/fishbigger) - [sleepycatcoding](https://github.com/sleepycatcoding) - [TheMelmacian](https://github.com/TheMelmacian) + - [tehciolo](https://github.com/tehciolo) # Emby Contributors diff --git a/src/components/itemContextMenu.js b/src/components/itemContextMenu.js index 0ec982f15a..0416ce4716 100644 --- a/src/components/itemContextMenu.js +++ b/src/components/itemContextMenu.js @@ -444,7 +444,7 @@ function executeCommand(item, id, options) { }); break; case 'multiSelect': - import('./multiSelect/multiSelect').then(({ startMultiSelect: startMultiSelect }) => { + import('./multiSelect/multiSelect').then(({ startMultiSelect }) => { const card = dom.parentWithClass(options.positionTo, 'card'); startMultiSelect(card); }); From 10101488af48a377c7ebf466c0b7dd40cf0c3fcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cosmin=20Cioacl=C4=83?= Date: Wed, 4 Oct 2023 10:23:59 +0200 Subject: [PATCH 43/70] chore: enable `no-useless-rename` lint rule --- .eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.js b/.eslintrc.js index 7b94a83ed3..4caf5f2b93 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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'], From 8caadde2792c25b71f06ba0f925fb6b9dbb01d18 Mon Sep 17 00:00:00 2001 From: TowyTowy Date: Thu, 5 Oct 2023 00:56:24 +0000 Subject: [PATCH 44/70] Translated using Weblate (Danish) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/ --- src/strings/da.json | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/strings/da.json b/src/strings/da.json index 82c8a8cc77..0b4176d23e 100644 --- a/src/strings/da.json +++ b/src/strings/da.json @@ -1336,7 +1336,7 @@ "EnableFasterAnimations": "Hurtigere animationer", "DisablePlugin": "Deaktiver", "EnablePlugin": "Aktiver", - "DirectPlayHelp": "Kilde filen er kompatibel med denne klient, og modtager filen uden brug af omkodning.", + "DirectPlayHelp": "Kilde filen er kompatibel med denne klient og modtager filen uden brug af omkodning.", "EnableEnhancedNvdecDecoder": "Aktiver forbedret NVDEC-dekoder", "MessagePlaybackError": "Der opstod en fejl under afspilning af denne fil på din Google Cast modtager.", "MessageChromecastConnectionError": "Din Google Cast modtager kan ikke komme i kontakt med Jellyfin serveren. Undersøg venligst forbindelsen og prøv igen.", @@ -1702,5 +1702,19 @@ "SubtitleCyan": "Cyan", "SubtitleMagenta": "Magenta", "AllowCollectionManagement": "Tillad denne bruger at administrere samlinger", - "AllowSegmentDeletion": "Slet segmenter" + "AllowSegmentDeletion": "Slet segmenter", + "HeaderEpisodesStatus": "Episodestatus", + "GoHome": "Gå Hjem", + "EnableAudioNormalizationHelp": "Audionormalisering tilføjer en konstant forstærkning for at holde gennemsnittet på et ønsket niveau (-18 dB).", + "EnableAudioNormalization": "Audio Normalisering", + "GridView": "Gittervisning", + "HeaderConfirmRepositoryInstallation": "Bekræft installation af plugin-repositorium", + "BackdropScreensaver": "Screensaver baggrund", + "GetThePlugin": "Få pluginnet", + "AllowSegmentDeletionHelp": "Slet gamle segmenter, når de er blevet sendt til klienten. Dette forhindrer, at man skal gemme hele den transkodede fil på disken. Fungerer kun med throttling aktiveret. Slå dette fra, hvis du oplever afspilningsproblemer.", + "LabelThrottleDelaySeconds": "Begræns efter", + "LabelThrottleDelaySecondsHelp": "Tid i sekunder, hvorefter transcoderen vil blive begrænset. Skal være stor nok til, at klienten kan opretholde en sund buffer. Virker kun, hvis throttling er aktiveret.", + "LabelSegmentKeepSeconds": "Tid at gemme segmenter i", + "LabelSegmentKeepSecondsHelp": "Tid i sekunder, som segmenter skal gemmes i, før de overskrives. Skal være større end \"Begræns efter\". Virker kun, hvis sletning af segmenter er aktiveret.", + "HeaderGuestCast": "Gæstestjerner" } From 705d0c9a0f61d19c0235d3b40513995095f38d2d Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Tue, 3 Oct 2023 10:25:00 -0400 Subject: [PATCH 45/70] Add vitest test framework Adds two new npm scripts: - 'test' - runs test suite once and exits - 'test:watch' - runs test suite perpetually. Any file suffixed with '.test.[js|ts]' is considered a test suite --- .github/workflows/tsc.yml | 3 + package-lock.json | 1674 +++++++++++++++++++++++++++++++++++++ package.json | 3 + 3 files changed, 1680 insertions(+) diff --git a/.github/workflows/tsc.yml b/.github/workflows/tsc.yml index 54b3208c81..35bde340f9 100644 --- a/.github/workflows/tsc.yml +++ b/.github/workflows/tsc.yml @@ -27,3 +27,6 @@ jobs: - name: Run tsc run: npm run build:check + + - name: Run test suite + run: npm run test diff --git a/package-lock.json b/package-lock.json index f320578826..f2a76ca971 100644 --- a/package-lock.json +++ b/package-lock.json @@ -116,6 +116,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", @@ -2719,6 +2720,358 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2926,6 +3279,18 @@ "axios": "^1.3.4" } }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", @@ -3531,6 +3896,12 @@ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "dev": true }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -3646,6 +4017,21 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==", + "dev": true + }, + "node_modules/@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -4241,6 +4627,119 @@ "integrity": "sha512-V3vzdXunOKKob1F+2ldv/4iGQoQA/iyqtW8PVlr1v16xCCKL831pGUubT+vs5/9wxTE/zBKEhjIjmmpK6nqw2A==", "dev": true }, + "node_modules/@vitest/expect": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", + "dev": true, + "dependencies": { + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", + "dev": true, + "dependencies": { + "@vitest/utils": "0.34.6", + "p-limit": "^4.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@vitest/snapshot/node_modules/magic-string": { + "version": "0.30.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", + "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@vitest/spy": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", + "dev": true, + "dependencies": { + "tinyspy": "^2.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", + "dev": true, + "dependencies": { + "diff-sequences": "^29.4.3", + "loupe": "^2.3.6", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -4817,6 +5316,15 @@ "node": ">=0.10.0" } }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -5399,6 +5907,15 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -5570,6 +6087,24 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -5623,6 +6158,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -6654,6 +7201,18 @@ "node": ">=8" } }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-equal": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", @@ -6858,6 +7417,15 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -7390,6 +7958,43 @@ "ext": "^1.1.2" } }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -9300,6 +9905,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -11083,6 +11697,12 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -11262,6 +11882,18 @@ "node": ">=8.9.0" } }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/localforage": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", @@ -11405,6 +12037,15 @@ "node": ">=0.10.0" } }, + "node_modules/loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -11882,6 +12523,30 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mlly": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", + "dev": true, + "dependencies": { + "acorn": "^8.10.0", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", + "ufo": "^1.3.0" + } + }, + "node_modules/mlly/node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/mrmime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", @@ -12669,6 +13334,21 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/pdfjs-dist": { "version": "3.6.172", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.6.172.tgz", @@ -12744,6 +13424,17 @@ "node": ">=8" } }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, "node_modules/plur": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz", @@ -14465,6 +15156,38 @@ "renderkid": "^3.0.0" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -15598,6 +16321,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "node_modules/signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -16183,6 +16912,12 @@ "node": "*" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, "node_modules/state-toggle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", @@ -16227,6 +16962,12 @@ "node": ">= 0.6" } }, + "node_modules/std-env": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", + "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", + "dev": true + }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -16415,6 +17156,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", + "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "dev": true, + "dependencies": { + "acorn": "^8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/style-loader": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", @@ -19397,6 +20162,30 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/tinybench": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz", + "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -19702,6 +20491,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -19740,6 +20538,12 @@ "node": ">=12.20" } }, + "node_modules/ufo": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz", + "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==", + "dev": true + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -20105,6 +20909,235 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vite": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", + "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.4.0", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/vitest": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", + "dev": true, + "dependencies": { + "@types/chai": "^4.3.5", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "acorn": "^8.9.0", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.10", + "debug": "^4.3.4", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3", + "strip-literal": "^1.0.1", + "tinybench": "^2.5.0", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.6", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*", + "playwright": "*", + "safaridriver": "*", + "webdriverio": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/vitest/node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "0.30.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", + "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -20628,6 +21661,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -22800,6 +23849,160 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, + "@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "dev": true, + "optional": true + }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -22957,6 +24160,15 @@ "integrity": "sha512-1+GXATaJLP5akFnUrpxYzoshLtTPZXJEdy/ozhY1g/DkULlz4LthLTaTJ2qImF0mb8Ayk7LNbh00n4ATk0JycA==", "requires": {} }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, "@jridgewell/gen-mapping": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", @@ -23331,6 +24543,12 @@ } } }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, "@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -23406,6 +24624,21 @@ "@types/node": "*" } }, + "@types/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==", + "dev": true + }, + "@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -23889,6 +25122,93 @@ "integrity": "sha512-V3vzdXunOKKob1F+2ldv/4iGQoQA/iyqtW8PVlr1v16xCCKL831pGUubT+vs5/9wxTE/zBKEhjIjmmpK6nqw2A==", "dev": true }, + "@vitest/expect": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", + "dev": true, + "requires": { + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" + } + }, + "@vitest/runner": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", + "dev": true, + "requires": { + "@vitest/utils": "0.34.6", + "p-limit": "^4.0.0", + "pathe": "^1.1.1" + }, + "dependencies": { + "p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "requires": { + "yocto-queue": "^1.0.0" + } + }, + "yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true + } + } + }, + "@vitest/snapshot": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", + "dev": true, + "requires": { + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "pretty-format": "^29.5.0" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "magic-string": { + "version": "0.30.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", + "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + } + } + }, + "@vitest/spy": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", + "dev": true, + "requires": { + "tinyspy": "^2.1.1" + } + }, + "@vitest/utils": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", + "dev": true, + "requires": { + "diff-sequences": "^29.4.3", + "loupe": "^2.3.6", + "pretty-format": "^29.5.0" + } + }, "@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -24344,6 +25664,12 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -24783,6 +26109,12 @@ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", "dev": true }, + "cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true + }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -24908,6 +26240,21 @@ "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==", "dev": true }, + "chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -24942,6 +26289,15 @@ "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", "dev": true }, + "check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -25681,6 +27037,15 @@ "mimic-response": "^2.0.0" } }, + "deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "deep-equal": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", @@ -25843,6 +27208,12 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -26274,6 +27645,36 @@ "ext": "^1.1.2" } }, + "esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -27715,6 +29116,12 @@ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, + "get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true + }, "get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -29007,6 +30414,12 @@ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -29162,6 +30575,12 @@ "json5": "^2.1.2" } }, + "local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true + }, "localforage": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", @@ -29292,6 +30711,15 @@ "signal-exit": "^3.0.0" } }, + "loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "requires": { + "get-func-name": "^2.0.0" + } + }, "lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -29639,6 +31067,26 @@ "minimist": "^1.2.5" } }, + "mlly": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", + "dev": true, + "requires": { + "acorn": "^8.10.0", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", + "ufo": "^1.3.0" + }, + "dependencies": { + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + } + } + }, "mrmime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", @@ -30245,6 +31693,18 @@ "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==" }, + "pathe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "dev": true + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, "pdfjs-dist": { "version": "3.6.172", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.6.172.tgz", @@ -30297,6 +31757,17 @@ "find-up": "^4.0.0" } }, + "pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "requires": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, "plur": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz", @@ -31411,6 +32882,31 @@ "renderkid": "^3.0.0" } }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -32277,6 +33773,12 @@ "object-inspect": "^1.9.0" } }, + "siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -32750,6 +34252,12 @@ "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", "dev": true }, + "stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, "state-toggle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", @@ -32783,6 +34291,12 @@ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true }, + "std-env": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", + "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", + "dev": true + }, "stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -32928,6 +34442,23 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "strip-literal": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", + "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "dev": true, + "requires": { + "acorn": "^8.10.0" + }, + "dependencies": { + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + } + } + }, "style-loader": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", @@ -35236,6 +36767,24 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "tinybench": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz", + "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==", + "dev": true + }, + "tinypool": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", + "dev": true + }, + "tinyspy": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "dev": true + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -35471,6 +37020,12 @@ "prelude-ls": "^1.2.1" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -35493,6 +37048,12 @@ "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true }, + "ufo": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz", + "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==", + "dev": true + }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -35770,6 +37331,109 @@ "unist-util-stringify-position": "^2.0.0" } }, + "vite": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", + "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "dev": true, + "requires": { + "esbuild": "^0.18.10", + "fsevents": "~2.3.2", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "dependencies": { + "postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + } + } + }, + "vite-node": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", + "dev": true, + "requires": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.4.0", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" + } + }, + "vitest": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", + "dev": true, + "requires": { + "@types/chai": "^4.3.5", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "acorn": "^8.9.0", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.10", + "debug": "^4.3.4", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3", + "strip-literal": "^1.0.1", + "tinybench": "^2.5.0", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.6", + "why-is-node-running": "^2.2.2" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + }, + "magic-string": { + "version": "0.30.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", + "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + } + } + }, "watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -36148,6 +37812,16 @@ "is-typed-array": "^1.1.10" } }, + "why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "requires": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + } + }, "wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", diff --git a/package.json b/package.json index 378942f26a..cacf693046 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "stylelint-scss": "5.0.0", "ts-loader": "9.4.4", "typescript": "5.0.4", + "vitest": "0.34.6", "webpack": "5.88.1", "webpack-bundle-analyzer": "4.9.1", "webpack-cli": "5.1.4", @@ -145,6 +146,8 @@ "build:check": "tsc --noEmit", "escheck": "es-check", "lint": "eslint \"./\"", + "test": "vitest --watch=false", + "test:watch": "vitest", "stylelint": "npm run stylelint:css && npm run stylelint:scss", "stylelint:css": "stylelint \"src/**/*.css\"", "stylelint:scss": "stylelint --config=\".stylelintrc.scss.json\" \"src/**/*.scss\"" From d77d69b57022c0799c78ac04a89b4bc90818a2a2 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Tue, 3 Oct 2023 10:34:40 -0400 Subject: [PATCH 46/70] Refactor desired aspect and posters per row functions to reduce cognitive complexity Fixes gh-4828 --- src/components/cardbuilder/cardBuilder.js | 242 +--------- .../cardbuilder/cardBuilderUtils.js | 173 ++++++++ .../cardbuilder/cardBuilderUtils.test.js | 417 ++++++++++++++++++ 3 files changed, 594 insertions(+), 238 deletions(-) create mode 100644 src/components/cardbuilder/cardBuilderUtils.js create mode 100644 src/components/cardbuilder/cardBuilderUtils.test.js diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index c2f5436b39..d52713283b 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -6,6 +6,7 @@ import escapeHtml from 'escape-html'; +import cardBuilderUtils from './cardBuilderUtils'; import browser from 'scripts/browser'; import datetime from 'scripts/datetime'; import dom from 'scripts/dom'; @@ -46,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. @@ -283,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); } @@ -323,7 +113,7 @@ function setCardData(items, options) { options.preferThumb = options.shape === 'backdrop' || options.shape === 'overflowBackdrop'; } - options.uiAspect = getDesiredAspect(options.shape); + options.uiAspect = cardBuilderUtils.getDesiredAspect(options.shape); options.primaryImageAspectRatio = primaryImageAspectRatio; if (!options.width && options.widths) { @@ -465,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. @@ -514,7 +280,7 @@ function getCardImageUrl(item, apiClient, options, shape) { let imgUrl = null; let imgTag = null; let coverImage = false; - const uiAspect = getDesiredAspect(shape); + const uiAspect = cardBuilderUtils.getDesiredAspect(shape); let imgType = null; let itemId = null; diff --git a/src/components/cardbuilder/cardBuilderUtils.js b/src/components/cardbuilder/cardBuilderUtils.js new file mode 100644 index 0000000000..494dcaf649 --- /dev/null +++ b/src/components/cardbuilder/cardBuilderUtils.js @@ -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 +}; diff --git a/src/components/cardbuilder/cardBuilderUtils.test.js b/src/components/cardbuilder/cardBuilderUtils.test.js new file mode 100644 index 0000000000..46599135db --- /dev/null +++ b/src/components/cardbuilder/cardBuilderUtils.test.js @@ -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); + }); + }); + }); +}); From f1afaa975ee46f230f93b7702ead15447f3bca4e Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Mon, 2 Oct 2023 12:22:36 -0400 Subject: [PATCH 47/70] Refactor home screen --- src/components/cardbuilder/cardBuilder.js | 268 +++---- src/components/homesections/homesections.js | 660 ++---------------- .../homesections/sections/activeRecordings.ts | 92 +++ .../homesections/sections/libraryButtons.ts | 36 + .../homesections/sections/libraryTiles.ts | 46 ++ .../homesections/sections/liveTv.ts | 181 +++++ .../homesections/sections/nextUp.ts | 106 +++ .../homesections/sections/recentlyAdded.ts | 165 +++++ .../homesections/sections/resume.ts | 105 +++ .../homesections/sections/section.d.ts | 11 + src/scripts/settings/userSettings.js | 6 +- src/types/homeSectionType.ts | 27 + 12 files changed, 949 insertions(+), 754 deletions(-) create mode 100644 src/components/homesections/sections/activeRecordings.ts create mode 100644 src/components/homesections/sections/libraryButtons.ts create mode 100644 src/components/homesections/sections/libraryTiles.ts create mode 100644 src/components/homesections/sections/liveTv.ts create mode 100644 src/components/homesections/sections/nextUp.ts create mode 100644 src/components/homesections/sections/recentlyAdded.ts create mode 100644 src/components/homesections/sections/resume.ts create mode 100644 src/components/homesections/sections/section.d.ts create mode 100644 src/types/homeSectionType.ts diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index d52713283b..ef44a1c33f 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -33,11 +33,11 @@ import '../guide/programs.scss'; const enableFocusTransform = !browser.slow && !browser.edge; /** - * Generate the HTML markup for cards for a set of items. - * @param items - The items used to generate cards. - * @param options - The options of the cards. - * @returns {string} The HTML markup for the cards. - */ + * Generate the HTML markup for cards for a set of items. + * @param items - The items used to generate cards. + * @param [options] - The options of the cards. + * @returns {string} The HTML markup for the cards. + */ export function getCardsHtml(items, options) { if (arguments.length === 1) { options = arguments[0]; @@ -48,10 +48,10 @@ export function getCardsHtml(items, options) { } /** - * Checks if the window is resizable. - * @param {number} windowWidth - Width of the device's screen. - * @returns {boolean} - Result of the check. - */ + * Checks if the window is resizable. + * @param {number} windowWidth - Width of the device's screen. + * @returns {boolean} - Result of the check. + */ function isResizable(windowWidth) { const screen = window.screen; if (screen) { @@ -66,22 +66,22 @@ function isResizable(windowWidth) { } /** - * Gets the width of a card's image according to the shape and amount of cards per row. - * @param {string} shape - Shape of the card. - * @param {number} screenWidth - Width of the screen. - * @param {boolean} isOrientationLandscape - Flag for the orientation of the screen. - * @returns {number} Width of the image for a card. - */ + * Gets the width of a card's image according to the shape and amount of cards per row. + * @param {string} shape - Shape of the card. + * @param {number} screenWidth - Width of the screen. + * @param {boolean} isOrientationLandscape - Flag for the orientation of the screen. + * @returns {number} Width of the image for a card. + */ function getImageWidth(shape, screenWidth, isOrientationLandscape) { const imagesPerRow = cardBuilderUtils.getPostersPerRow(shape, screenWidth, isOrientationLandscape, layoutManager.tv); return Math.round(screenWidth / imagesPerRow); } /** - * Normalizes the options for a card. - * @param {Object} items - A set of items. - * @param {Object} options - Options for handling the items. - */ + * Normalizes the options for a card. + * @param {Object} items - A set of items. + * @param {Object} options - Options for handling the items. + */ function setCardData(items, options) { options.shape = options.shape || 'auto'; @@ -138,11 +138,11 @@ function setCardData(items, options) { } /** - * Generates the internal HTML markup for cards. - * @param {Object} items - Items for which to generate the markup. - * @param {Object} options - Options for generating the markup. - * @returns {string} The internal HTML markup of the cards. - */ + * Generates the internal HTML markup for cards. + * @param {Object} items - Items for which to generate the markup. + * @param {Object} options - Options for generating the markup. + * @returns {string} The internal HTML markup of the cards. + */ function buildCardsHtmlInternal(items, options) { let isVertical = false; @@ -256,20 +256,20 @@ function buildCardsHtmlInternal(items, options) { } /** - * @typedef {Object} CardImageUrl - * @property {string} imgUrl - Image URL. - * @property {string} blurhash - Image blurhash. - * @property {boolean} forceName - Force name. - * @property {boolean} coverImage - Use cover style. - */ + * @typedef {Object} CardImageUrl + * @property {string} imgUrl - Image URL. + * @property {string} blurhash - Image blurhash. + * @property {boolean} forceName - Force name. + * @property {boolean} coverImage - Use cover style. + */ /** Get the URL of the card's image. - * @param {Object} item - Item for which to generate a card. - * @param {Object} apiClient - API client object. - * @param {Object} options - Options of the card. - * @param {string} shape - Shape of the desired image. - * @returns {CardImageUrl} Object representing the URL of the card's image. - */ + * @param {Object} item - Item for which to generate a card. + * @param {Object} apiClient - API client object. + * @param {Object} options - Options of the card. + * @param {string} shape - Shape of the desired image. + * @returns {CardImageUrl} Object representing the URL of the card's image. + */ function getCardImageUrl(item, apiClient, options, shape) { item = item.ProgramInfo || item; @@ -412,10 +412,10 @@ function getCardImageUrl(item, apiClient, options, shape) { } /** - * Generates an index used to select the default color of a card based on a string. - * @param {?string} [str] - String to use for generating the index. - * @returns {number} Index of the color. - */ + * Generates an index used to select the default color of a card based on a string. + * @param {?string} [str] - String to use for generating the index. + * @returns {number} Index of the color. + */ function getDefaultColorIndex(str) { const numRandomColors = 5; @@ -435,16 +435,16 @@ function getDefaultColorIndex(str) { } /** - * Generates the HTML markup for a card's text. - * @param {Array} lines - Array containing the text lines. - * @param {string} cssClass - Base CSS class to use for the lines. - * @param {boolean} forceLines - Flag to force the rendering of all lines. - * @param {boolean} isOuterFooter - Flag to mark the text lines as outer footer. - * @param {string} cardLayout - DEPRECATED - * @param {boolean} addRightMargin - Flag to add a right margin to the text. - * @param {number} maxLines - Maximum number of lines to render. - * @returns {string} HTML markup for the card's text. - */ + * Generates the HTML markup for a card's text. + * @param {Array} lines - Array containing the text lines. + * @param {string} cssClass - Base CSS class to use for the lines. + * @param {boolean} forceLines - Flag to force the rendering of all lines. + * @param {boolean} isOuterFooter - Flag to mark the text lines as outer footer. + * @param {string} cardLayout - DEPRECATED + * @param {boolean} addRightMargin - Flag to add a right margin to the text. + * @param {number} maxLines - Maximum number of lines to render. + * @returns {string} HTML markup for the card's text. + */ function getCardTextLines(lines, cssClass, forceLines, isOuterFooter, cardLayout, addRightMargin, maxLines) { let html = ''; @@ -488,21 +488,21 @@ function getCardTextLines(lines, cssClass, forceLines, isOuterFooter, cardLayout } /** - * Determines if the item is live TV. - * @param {Object} item - Item to use for the check. - * @returns {boolean} Flag showing if the item is live TV. - */ + * Determines if the item is live TV. + * @param {Object} item - Item to use for the check. + * @returns {boolean} Flag showing if the item is live TV. + */ function isUsingLiveTvNaming(item) { return item.Type === 'Program' || item.Type === 'Timer' || item.Type === 'Recording'; } /** - * Returns the air time text for the item based on the given times. - * @param {object} item - Item used to generate the air time text. - * @param {boolean} showAirDateTime - ISO8601 date for the start of the show. - * @param {boolean} showAirEndTime - ISO8601 date for the end of the show. - * @returns {string} The air time text for the item based on the given dates. - */ + * Returns the air time text for the item based on the given times. + * @param {object} item - Item used to generate the air time text. + * @param {boolean} showAirDateTime - ISO8601 date for the start of the show. + * @param {boolean} showAirEndTime - ISO8601 date for the end of the show. + * @returns {string} The air time text for the item based on the given dates. + */ function getAirTimeText(item, showAirDateTime, showAirEndTime) { let airTimeText = ''; @@ -529,16 +529,16 @@ function getAirTimeText(item, showAirDateTime, showAirEndTime) { } /** - * Generates the HTML markup for the card's footer text. - * @param {Object} item - Item used to generate the footer text. - * @param {Object} apiClient - API client instance. - * @param {Object} options - Options used to generate the footer text. - * @param {string} footerClass - CSS classes of the footer element. - * @param {string} progressHtml - HTML markup of the progress bar element. - * @param {Object} flags - Various flags for the footer - * @param {Object} urls - Various urls for the footer - * @returns {string} HTML markup of the card's footer text element. - */ + * Generates the HTML markup for the card's footer text. + * @param {Object} item - Item used to generate the footer text. + * @param {Object} apiClient - API client instance. + * @param {Object} options - Options used to generate the footer text. + * @param {string} footerClass - CSS classes of the footer element. + * @param {string} progressHtml - HTML markup of the progress bar element. + * @param {Object} flags - Various flags for the footer + * @param {Object} urls - Various urls for the footer + * @returns {string} HTML markup of the card's footer text element. + */ function getCardFooterText(item, apiClient, options, footerClass, progressHtml, flags, urls) { item = item.ProgramInfo || item; let html = ''; @@ -771,12 +771,12 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml, } /** - * Generates the HTML markup for the action button. - * @param {Object} item - Item used to generate the action button. - * @param {string} text - Text of the action button. - * @param {string} serverId - ID of the server. - * @returns {string} HTML markup of the action button. - */ + * Generates the HTML markup for the action button. + * @param {Object} item - Item used to generate the action button. + * @param {string} text - Text of the action button. + * @param {string} serverId - ID of the server. + * @returns {string} HTML markup of the action button. + */ function getTextActionButton(item, text, serverId) { if (!text) { text = itemHelper.getDisplayName(item); @@ -797,11 +797,11 @@ function getTextActionButton(item, text, serverId) { } /** - * Generates HTML markup for the item count indicator. - * @param {Object} options - Options used to generate the item count. - * @param {Object} item - Item used to generate the item count. - * @returns {string} HTML markup for the item count indicator. - */ + * Generates HTML markup for the item count indicator. + * @param {Object} options - Options used to generate the item count. + * @param {Object} item - Item used to generate the item count. + * @returns {string} HTML markup for the item count indicator. + */ function getItemCountsHtml(options, item) { const counts = []; let childText; @@ -879,8 +879,8 @@ function getItemCountsHtml(options, item) { let refreshIndicatorLoaded; /** - * Imports the refresh indicator element. - */ + * Imports the refresh indicator element. + */ function importRefreshIndicator() { if (!refreshIndicatorLoaded) { refreshIndicatorLoaded = true; @@ -889,22 +889,22 @@ function importRefreshIndicator() { } /** - * Returns the default background class for a card based on a string. - * @param {?string} [str] - Text used to generate the background class. - * @returns {string} CSS classes for default card backgrounds. - */ + * Returns the default background class for a card based on a string. + * @param {?string} [str] - Text used to generate the background class. + * @returns {string} CSS classes for default card backgrounds. + */ export function getDefaultBackgroundClass(str) { return 'defaultCardBackground defaultCardBackground' + getDefaultColorIndex(str); } /** - * Builds the HTML markup for an individual card. - * @param {number} index - Index of the card - * @param {object} item - Item used to generate the card. - * @param {object} apiClient - API client instance. - * @param {object} options - Options used to generate the card. - * @returns {string} HTML markup for the generated card. - */ + * Builds the HTML markup for an individual card. + * @param {number} index - Index of the card + * @param {object} item - Item used to generate the card. + * @param {object} apiClient - API client instance. + * @param {object} options - Options used to generate the card. + * @returns {string} HTML markup for the generated card. + */ function buildCard(index, item, apiClient, options) { let action = options.action || 'link'; @@ -1211,11 +1211,11 @@ function buildCard(index, item, apiClient, options) { } /** - * Generates HTML markup for the card overlay. - * @param {object} item - Item used to generate the card overlay. - * @param {string} action - Action assigned to the overlay. - * @returns {string} HTML markup of the card overlay. - */ + * Generates HTML markup for the card overlay. + * @param {object} item - Item used to generate the card overlay. + * @param {string} action - Action assigned to the overlay. + * @returns {string} HTML markup of the card overlay. + */ function getHoverMenuHtml(item, action) { let html = ''; @@ -1253,11 +1253,11 @@ function getHoverMenuHtml(item, action) { } /** - * Generates the text or icon used for default card backgrounds. - * @param {object} item - Item used to generate the card overlay. - * @param {object} options - Options used to generate the card overlay. - * @returns {string} HTML markup of the card overlay. - */ + * Generates the text or icon used for default card backgrounds. + * @param {object} item - Item used to generate the card overlay. + * @param {object} options - Options used to generate the card overlay. + * @returns {string} HTML markup of the card overlay. + */ export function getDefaultText(item, options) { if (item.CollectionType) { return ''; @@ -1301,10 +1301,10 @@ export function getDefaultText(item, options) { } /** - * Builds a set of cards and inserts them into the page. - * @param {Array} items - Array of items used to build the cards. - * @param {options} options - Options of the cards to build. - */ + * Builds a set of cards and inserts them into the page. + * @param {Array} items - Array of items used to build the cards. + * @param {options} options - Options of the cards to build. + */ export function buildCards(items, options) { // Abort if the container has been disposed if (!document.body.contains(options.itemsContainer)) { @@ -1345,11 +1345,11 @@ export function buildCards(items, options) { } /** - * Ensures the indicators for a card exist and creates them if they don't exist. - * @param {HTMLDivElement} card - DOM element of the card. - * @param {HTMLDivElement} indicatorsElem - DOM element of the indicators. - * @returns {HTMLDivElement} - DOM element of the indicators. - */ + * Ensures the indicators for a card exist and creates them if they don't exist. + * @param {HTMLDivElement} card - DOM element of the card. + * @param {HTMLDivElement} indicatorsElem - DOM element of the indicators. + * @returns {HTMLDivElement} - DOM element of the indicators. + */ function ensureIndicators(card, indicatorsElem) { if (indicatorsElem) { return indicatorsElem; @@ -1368,10 +1368,10 @@ function ensureIndicators(card, indicatorsElem) { } /** - * Adds user data to the card such as progress indicators and played status. - * @param {HTMLDivElement} card - DOM element of the card. - * @param {Object} userData - User data to apply to the card. - */ + * Adds user data to the card such as progress indicators and played status. + * @param {HTMLDivElement} card - DOM element of the card. + * @param {Object} userData - User data to apply to the card. + */ function updateUserData(card, userData) { const type = card.getAttribute('data-type'); const enableCountIndicator = type === 'Series' || type === 'BoxSet' || type === 'Season'; @@ -1447,10 +1447,10 @@ function updateUserData(card, userData) { } /** - * Handles when user data has changed. - * @param {Object} userData - User data to apply to the card. - * @param {HTMLElement} scope - DOM element to use as a scope when selecting cards. - */ + * Handles when user data has changed. + * @param {Object} userData - User data to apply to the card. + * @param {HTMLElement} scope - DOM element to use as a scope when selecting cards. + */ export function onUserDataChanged(userData, scope) { const cards = (scope || document.body).querySelectorAll('.card-withuserdata[data-id="' + userData.ItemId + '"]'); @@ -1460,11 +1460,11 @@ export function onUserDataChanged(userData, scope) { } /** - * Handles when a timer has been created. - * @param {string} programId - ID of the program. - * @param {string} newTimerId - ID of the new timer. - * @param {HTMLElement} itemsContainer - DOM element of the itemsContainer. - */ + * Handles when a timer has been created. + * @param {string} programId - ID of the program. + * @param {string} newTimerId - ID of the new timer. + * @param {HTMLElement} itemsContainer - DOM element of the itemsContainer. + */ export function onTimerCreated(programId, newTimerId, itemsContainer) { const cells = itemsContainer.querySelectorAll('.card[data-id="' + programId + '"]'); @@ -1480,10 +1480,10 @@ export function onTimerCreated(programId, newTimerId, itemsContainer) { } /** - * Handles when a timer has been cancelled. - * @param {string} timerId - ID of the cancelled timer. - * @param {HTMLElement} itemsContainer - DOM element of the itemsContainer. - */ + * Handles when a timer has been cancelled. + * @param {string} timerId - ID of the cancelled timer. + * @param {HTMLElement} itemsContainer - DOM element of the itemsContainer. + */ export function onTimerCancelled(timerId, itemsContainer) { const cells = itemsContainer.querySelectorAll('.card[data-timerid="' + timerId + '"]'); @@ -1497,10 +1497,10 @@ export function onTimerCancelled(timerId, itemsContainer) { } /** - * Handles when a series timer has been cancelled. - * @param {string} cancelledTimerId - ID of the cancelled timer. - * @param {HTMLElement} itemsContainer - DOM element of the itemsContainer. - */ + * Handles when a series timer has been cancelled. + * @param {string} cancelledTimerId - ID of the cancelled timer. + * @param {HTMLElement} itemsContainer - DOM element of the itemsContainer. + */ export function onSeriesTimerCancelled(cancelledTimerId, itemsContainer) { const cells = itemsContainer.querySelectorAll('.card[data-seriestimerid="' + cancelledTimerId + '"]'); diff --git a/src/components/homesections/homesections.js b/src/components/homesections/homesections.js index 27c48f0998..83b8ec551d 100644 --- a/src/components/homesections/homesections.js +++ b/src/components/homesections/homesections.js @@ -1,15 +1,14 @@ -import escapeHtml from 'escape-html'; - import globalize from 'scripts/globalize'; -import imageHelper from 'scripts/imagehelper'; -import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card'; +import { DEFAULT_SECTIONS, HomeSectionType } from 'types/homeSectionType'; import Dashboard from 'utils/dashboard'; -import cardBuilder from '../cardbuilder/cardBuilder'; -import imageLoader from '../images/imageLoader'; -import layoutManager from '../layoutManager'; -import { appRouter } from '../router/appRouter'; -import ServerConnections from '../ServerConnections'; +import { loadRecordings } from './sections/activeRecordings'; +import { loadLibraryButtons } from './sections/libraryButtons'; +import { loadLibraryTiles } from './sections/libraryTiles'; +import { loadLiveTV } from './sections/liveTv'; +import { loadNextUp } from './sections/nextUp'; +import { loadRecentlyAdded } from './sections/recentlyAdded'; +import { loadResume } from './sections/resume'; import 'elements/emby-button/paper-icon-button-light'; import 'elements/emby-itemscontainer/emby-itemscontainer'; @@ -19,26 +18,8 @@ import 'elements/emby-button/emby-button'; import './homesections.scss'; export function getDefaultSection(index) { - switch (index) { - case 0: - return 'smalllibrarytiles'; - case 1: - return 'resume'; - case 2: - return 'resumeaudio'; - case 3: - return 'resumebook'; - case 4: - return 'livetv'; - case 5: - return 'nextup'; - case 6: - return 'latestmedia'; - case 7: - return 'none'; - default: - return ''; - } + if (index < 0 || index > DEFAULT_SECTIONS.length) return ''; + return DEFAULT_SECTIONS[index]; } function getAllSectionsToShow(userSettings, sectionCount) { @@ -138,29 +119,36 @@ export function resume(elem, options) { function loadSection(page, apiClient, user, userSettings, userViews, allSections, index) { const section = allSections[index]; const elem = page.querySelector('.section' + index); + const options = { enableOverflow: enableScrollX() }; - if (section === 'latestmedia') { - loadRecentlyAdded(elem, apiClient, user, userViews); - } else if (section === 'librarytiles' || section === 'smalllibrarytiles' || section === 'smalllibrarytiles-automobile' || section === 'librarytiles-automobile') { - loadLibraryTiles(elem, apiClient, user, userSettings, 'smallBackdrop', userViews); - } else if (section === 'librarybuttons') { - loadlibraryButtons(elem, apiClient, user, userSettings, userViews); - } else if (section === 'resume') { - return loadResume(elem, apiClient, 'HeaderContinueWatching', 'Video', userSettings); - } else if (section === 'resumeaudio') { - return loadResume(elem, apiClient, 'HeaderContinueListening', 'Audio', userSettings); - } else if (section === 'activerecordings') { - loadLatestLiveTvRecordings(elem, true, apiClient); - } else if (section === 'nextup') { - loadNextUp(elem, apiClient, userSettings); - } else if (section === 'onnow' || section === 'livetv') { - return loadOnNow(elem, apiClient, user); - } else if (section === 'resumebook') { - return loadResume(elem, apiClient, 'HeaderContinueReading', 'Book', userSettings); - } else { - elem.innerHTML = ''; - return Promise.resolve(); + switch (section) { + case HomeSectionType.ActiveRecordings: + loadRecordings(elem, true, apiClient, options); + break; + case HomeSectionType.LatestMedia: + loadRecentlyAdded(elem, apiClient, user, userViews, options); + break; + case HomeSectionType.LibraryButtons: + loadLibraryButtons(elem, userViews); + break; + case HomeSectionType.LiveTv: + return loadLiveTV(elem, apiClient, user, options); + case HomeSectionType.NextUp: + loadNextUp(elem, apiClient, userSettings, options); + break; + case HomeSectionType.Resume: + return loadResume(elem, apiClient, 'HeaderContinueWatching', 'Video', userSettings, options); + case HomeSectionType.ResumeAudio: + return loadResume(elem, apiClient, 'HeaderContinueListening', 'Audio', userSettings, options); + case HomeSectionType.ResumeBook: + return loadResume(elem, apiClient, 'HeaderContinueReading', 'Book', userSettings, options); + case HomeSectionType.SmallLibraryTiles: + loadLibraryTiles(elem, userViews, options); + break; + default: + elem.innerHTML = ''; } + return Promise.resolve(); } @@ -174,573 +162,11 @@ function enableScrollX() { return true; } -function getLibraryButtonsHtml(items) { - let html = ''; - - html += '
'; - html += '

' + globalize.translate('HeaderMyMedia') + '

'; - - html += '
'; - - // library card background images - for (let i = 0, length = items.length; i < length; i++) { - const item = items[i]; - const icon = imageHelper.getLibraryIcon(item.CollectionType); - html += '' + escapeHtml(item.Name) + ''; - } - - html += '
'; - html += '
'; - - return html; -} - -function loadlibraryButtons(elem, apiClient, user, userSettings, userViews) { - elem.classList.remove('verticalSection'); - const html = getLibraryButtonsHtml(userViews); - - elem.innerHTML = html; - imageLoader.lazyChildren(elem); -} - -function getFetchLatestItemsFn(serverId, parentId, collectionType) { - return function () { - const apiClient = ServerConnections.getApiClient(serverId); - let limit = 16; - - if (enableScrollX()) { - if (collectionType === 'music') { - limit = 30; - } - } else if (collectionType === 'tvshows') { - limit = 5; - } else if (collectionType === 'music') { - limit = 9; - } else { - limit = 8; - } - - const options = { - Limit: limit, - Fields: 'PrimaryImageAspectRatio,BasicSyncInfo,Path', - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Thumb', - ParentId: parentId - }; - - return apiClient.getLatestItems(options); - }; -} - -function getLatestItemsHtmlFn(itemType, viewType) { - return function (items) { - const cardLayout = false; - let shape; - if (itemType === 'Channel' || viewType === 'movies' || viewType === 'books' || viewType === 'tvshows') { - shape = getPortraitShape(enableScrollX()); - } else if (viewType === 'music' || viewType === 'homevideos') { - shape = getSquareShape(enableScrollX()); - } else { - shape = getBackdropShape(enableScrollX()); - } - - return cardBuilder.getCardsHtml({ - items: items, - shape: shape, - preferThumb: viewType !== 'movies' && viewType !== 'tvshows' && itemType !== 'Channel' && viewType !== 'music' ? 'auto' : null, - showUnplayedIndicator: false, - showChildCountIndicator: true, - context: 'home', - overlayText: false, - centerText: !cardLayout, - overlayPlayButton: viewType !== 'photos', - allowBottomPadding: !enableScrollX() && !cardLayout, - cardLayout: cardLayout, - showTitle: viewType !== 'photos', - showYear: viewType === 'movies' || viewType === 'tvshows' || !viewType, - showParentTitle: viewType === 'music' || viewType === 'tvshows' || !viewType || (cardLayout && (viewType === 'tvshows')), - lines: 2 - }); - }; -} - -function renderLatestSection(elem, apiClient, user, parent) { - let html = ''; - - html += '
'; - if (!layoutManager.tv) { - html += ''; - html += '

'; - html += globalize.translate('LatestFromLibrary', escapeHtml(parent.Name)); - html += '

'; - html += ''; - html += '
'; - } else { - html += '

' + globalize.translate('LatestFromLibrary', escapeHtml(parent.Name)) + '

'; - } - html += '
'; - - if (enableScrollX()) { - html += '
'; - html += '
'; - } else { - html += '
'; - } - - if (enableScrollX()) { - html += '
'; - } - html += '
'; - - elem.innerHTML = html; - - const itemsContainer = elem.querySelector('.itemsContainer'); - itemsContainer.fetchData = getFetchLatestItemsFn(apiClient.serverId(), parent.Id, parent.CollectionType); - itemsContainer.getItemsHtml = getLatestItemsHtmlFn(parent.Type, parent.CollectionType); - itemsContainer.parentContainer = elem; -} - -function loadRecentlyAdded(elem, apiClient, user, userViews) { - elem.classList.remove('verticalSection'); - const excludeViewTypes = ['playlists', 'livetv', 'boxsets', 'channels']; - - for (let i = 0, length = userViews.length; i < length; i++) { - const item = userViews[i]; - if (user.Configuration.LatestItemsExcludes.indexOf(item.Id) !== -1) { - continue; - } - - if (excludeViewTypes.indexOf(item.CollectionType || []) !== -1) { - continue; - } - - const frag = document.createElement('div'); - frag.classList.add('verticalSection'); - frag.classList.add('hide'); - elem.appendChild(frag); - - renderLatestSection(frag, apiClient, user, item); - } -} - -export function loadLibraryTiles(elem, apiClient, user, userSettings, shape, userViews) { - let html = ''; - if (userViews.length) { - html += '

' + globalize.translate('HeaderMyMedia') + '

'; - if (enableScrollX()) { - html += '
'; - html += '
'; - } else { - html += '
'; - } - - html += cardBuilder.getCardsHtml({ - items: userViews, - shape: getBackdropShape(enableScrollX()), - showTitle: true, - centerText: true, - overlayText: false, - lazy: true, - transition: false, - allowBottomPadding: !enableScrollX() - }); - - if (enableScrollX()) { - html += '
'; - } - html += '
'; - } - - elem.innerHTML = html; - imageLoader.lazyChildren(elem); -} - -const dataMonitorHints = { - 'Audio': 'audioplayback,markplayed', - 'Video': 'videoplayback,markplayed' -}; - -function loadResume(elem, apiClient, headerText, mediaType, userSettings) { - let html = ''; - - const dataMonitor = dataMonitorHints[mediaType] || 'markplayed'; - - html += '

' + globalize.translate(headerText) + '

'; - if (enableScrollX()) { - html += '
'; - html += `
`; - } else { - html += `
`; - } - - if (enableScrollX()) { - html += '
'; - } - html += '
'; - - elem.classList.add('hide'); - elem.innerHTML = html; - - const itemsContainer = elem.querySelector('.itemsContainer'); - itemsContainer.fetchData = getItemsToResumeFn(mediaType, apiClient.serverId()); - itemsContainer.getItemsHtml = getItemsToResumeHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), mediaType); - itemsContainer.parentContainer = elem; -} - -function getItemsToResumeFn(mediaType, serverId) { - return function () { - const apiClient = ServerConnections.getApiClient(serverId); - - const limit = enableScrollX() ? 12 : 5; - - const options = { - Limit: limit, - Recursive: true, - Fields: 'PrimaryImageAspectRatio,BasicSyncInfo', - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Thumb', - EnableTotalRecordCount: false, - MediaTypes: mediaType - }; - - return apiClient.getResumableItems(apiClient.getCurrentUserId(), options); - }; -} - -function getItemsToResumeHtmlFn(useEpisodeImages, mediaType) { - return function (items) { - const cardLayout = false; - return cardBuilder.getCardsHtml({ - items: items, - preferThumb: true, - inheritThumb: !useEpisodeImages, - shape: (mediaType === 'Book') ? - getPortraitShape(enableScrollX()) : - getBackdropShape(enableScrollX()), - overlayText: false, - showTitle: true, - showParentTitle: true, - lazy: true, - showDetailsMenu: true, - overlayPlayButton: true, - context: 'home', - centerText: !cardLayout, - allowBottomPadding: false, - cardLayout: cardLayout, - showYear: true, - lines: 2 - }); - }; -} - -function getOnNowFetchFn(serverId) { - return function () { - const apiClient = ServerConnections.getApiClient(serverId); - return apiClient.getLiveTvRecommendedPrograms({ - userId: apiClient.getCurrentUserId(), - IsAiring: true, - limit: 24, - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Thumb,Backdrop', - EnableTotalRecordCount: false, - Fields: 'ChannelInfo,PrimaryImageAspectRatio' - }); - }; -} - -function getOnNowItemsHtml(items) { - return cardBuilder.getCardsHtml({ - items: items, - preferThumb: 'auto', - inheritThumb: false, - shape: (enableScrollX() ? 'autooverflow' : 'auto'), - showParentTitleOrTitle: true, - showTitle: true, - centerText: true, - coverImage: true, - overlayText: false, - allowBottomPadding: !enableScrollX(), - showAirTime: true, - showChannelName: false, - showAirDateTime: false, - showAirEndTime: true, - defaultShape: getBackdropShape(enableScrollX()), - lines: 3, - overlayPlayButton: true - }); -} - -function loadOnNow(elem, apiClient, user) { - if (!user.Policy.EnableLiveTvAccess) { - return Promise.resolve(); - } - - return apiClient.getLiveTvRecommendedPrograms({ - userId: apiClient.getCurrentUserId(), - IsAiring: true, - limit: 1, - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Thumb,Backdrop', - EnableTotalRecordCount: false, - Fields: 'ChannelInfo,PrimaryImageAspectRatio' - }).then(function (result) { - let html = ''; - if (result.Items.length) { - elem.classList.remove('padded-left'); - elem.classList.remove('padded-right'); - elem.classList.remove('padded-bottom'); - elem.classList.remove('verticalSection'); - - html += '
'; - html += '
'; - html += '

' + globalize.translate('LiveTV') + '

'; - html += '
'; - - if (enableScrollX()) { - html += '
'; - html += '
'; - } else { - html += ''; - if (enableScrollX()) { - html += '
'; - } - html += '
'; - html += '
'; - - html += '
'; - html += '
'; - - if (!layoutManager.tv) { - html += ''; - html += '

'; - html += globalize.translate('HeaderOnNow'); - html += '

'; - html += ''; - html += '
'; - } else { - html += '

' + globalize.translate('HeaderOnNow') + '

'; - } - html += '
'; - - if (enableScrollX()) { - html += '
'; - html += '
'; - } else { - html += '
'; - } - - if (enableScrollX()) { - html += '
'; - } - - html += '
'; - html += '
'; - - elem.innerHTML = html; - - const itemsContainer = elem.querySelector('.itemsContainer'); - itemsContainer.parentContainer = elem; - itemsContainer.fetchData = getOnNowFetchFn(apiClient.serverId()); - itemsContainer.getItemsHtml = getOnNowItemsHtml; - } - }); -} - -function getNextUpFetchFn(serverId, userSettings) { - return function () { - const apiClient = ServerConnections.getApiClient(serverId); - const oldestDateForNextUp = new Date(); - oldestDateForNextUp.setDate(oldestDateForNextUp.getDate() - userSettings.maxDaysForNextUp()); - return apiClient.getNextUpEpisodes({ - Limit: enableScrollX() ? 24 : 15, - Fields: 'PrimaryImageAspectRatio,DateCreated,BasicSyncInfo,Path,MediaSourceCount', - UserId: apiClient.getCurrentUserId(), - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', - EnableTotalRecordCount: false, - DisableFirstEpisode: false, - NextUpDateCutoff: oldestDateForNextUp.toISOString(), - EnableRewatching: userSettings.enableRewatchingInNextUp() - }); - }; -} - -function getNextUpItemsHtmlFn(useEpisodeImages) { - return function (items) { - const cardLayout = false; - return cardBuilder.getCardsHtml({ - items: items, - preferThumb: true, - inheritThumb: !useEpisodeImages, - shape: getBackdropShape(enableScrollX()), - overlayText: false, - showTitle: true, - showParentTitle: true, - lazy: true, - overlayPlayButton: true, - context: 'home', - centerText: !cardLayout, - allowBottomPadding: !enableScrollX(), - cardLayout: cardLayout - }); - }; -} - -function loadNextUp(elem, apiClient, userSettings) { - let html = ''; - - html += '
'; - if (!layoutManager.tv) { - html += ''; - html += '

'; - html += globalize.translate('NextUp'); - html += '

'; - html += ''; - html += '
'; - } else { - html += '

'; - html += globalize.translate('NextUp'); - html += '

'; - } - html += '
'; - - if (enableScrollX()) { - html += '
'; - html += '
'; - } else { - html += '
'; - } - - if (enableScrollX()) { - html += '
'; - } - html += '
'; - - elem.classList.add('hide'); - elem.innerHTML = html; - - const itemsContainer = elem.querySelector('.itemsContainer'); - itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings); - itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume()); - itemsContainer.parentContainer = elem; -} - -function getLatestRecordingsFetchFn(serverId, activeRecordingsOnly) { - return function () { - const apiClient = ServerConnections.getApiClient(serverId); - return apiClient.getLiveTvRecordings({ - userId: apiClient.getCurrentUserId(), - Limit: enableScrollX() ? 12 : 5, - Fields: 'PrimaryImageAspectRatio,BasicSyncInfo', - EnableTotalRecordCount: false, - IsLibraryItem: activeRecordingsOnly ? null : false, - IsInProgress: activeRecordingsOnly ? true : null - }); - }; -} - -function getLatestRecordingItemsHtml(activeRecordingsOnly) { - return function (items) { - return cardBuilder.getCardsHtml({ - items: items, - shape: enableScrollX() ? 'autooverflow' : 'auto', - showTitle: true, - showParentTitle: true, - coverImage: true, - lazy: true, - showDetailsMenu: true, - centerText: true, - overlayText: false, - showYear: true, - lines: 2, - overlayPlayButton: !activeRecordingsOnly, - allowBottomPadding: !enableScrollX(), - preferThumb: true, - cardLayout: false, - overlayMoreButton: activeRecordingsOnly, - action: activeRecordingsOnly ? 'none' : null, - centerPlayButton: activeRecordingsOnly - }); - }; -} - -function loadLatestLiveTvRecordings(elem, activeRecordingsOnly, apiClient) { - const title = activeRecordingsOnly ? - globalize.translate('HeaderActiveRecordings') : - globalize.translate('HeaderLatestRecordings'); - - let html = ''; - - html += '
'; - html += '

' + title + '

'; - html += '
'; - - if (enableScrollX()) { - html += '
'; - html += '
'; - } else { - html += '
'; - } - - if (enableScrollX()) { - html += '
'; - } - html += '
'; - - elem.classList.add('hide'); - elem.innerHTML = html; - - const itemsContainer = elem.querySelector('.itemsContainer'); - itemsContainer.fetchData = getLatestRecordingsFetchFn(apiClient.serverId(), activeRecordingsOnly); - itemsContainer.getItemsHtml = getLatestRecordingItemsHtml(activeRecordingsOnly); - itemsContainer.parentContainer = elem; -} - export default { - loadLibraryTiles: loadLibraryTiles, - getDefaultSection: getDefaultSection, - loadSections: loadSections, - destroySections: destroySections, - pause: pause, - resume: resume + getDefaultSection, + loadSections, + destroySections, + pause, + resume }; diff --git a/src/components/homesections/sections/activeRecordings.ts b/src/components/homesections/sections/activeRecordings.ts new file mode 100644 index 0000000000..8b8129f789 --- /dev/null +++ b/src/components/homesections/sections/activeRecordings.ts @@ -0,0 +1,92 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import type { ApiClient } from 'jellyfin-apiclient'; + +import ServerConnections from 'components/ServerConnections'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import globalize from 'scripts/globalize'; + +import type { SectionContainerElement, SectionOptions } from './section'; + +function getLatestRecordingsFetchFn( + serverId: string, + activeRecordingsOnly: boolean, + { enableOverflow }: SectionOptions +) { + return function () { + const apiClient = ServerConnections.getApiClient(serverId); + return apiClient.getLiveTvRecordings({ + userId: apiClient.getCurrentUserId(), + Limit: enableOverflow ? 12 : 5, + Fields: 'PrimaryImageAspectRatio,BasicSyncInfo', + EnableTotalRecordCount: false, + IsLibraryItem: activeRecordingsOnly ? null : false, + IsInProgress: activeRecordingsOnly ? true : null + }); + }; +} + +function getLatestRecordingItemsHtml( + activeRecordingsOnly: boolean, + { enableOverflow }: SectionOptions +) { + return function (items: BaseItemDto[]) { + return cardBuilder.getCardsHtml({ + items: items, + shape: enableOverflow ? 'autooverflow' : 'auto', + showTitle: true, + showParentTitle: true, + coverImage: true, + lazy: true, + showDetailsMenu: true, + centerText: true, + overlayText: false, + showYear: true, + lines: 2, + overlayPlayButton: !activeRecordingsOnly, + allowBottomPadding: !enableOverflow, + preferThumb: true, + cardLayout: false, + overlayMoreButton: activeRecordingsOnly, + action: activeRecordingsOnly ? 'none' : null, + centerPlayButton: activeRecordingsOnly + }); + }; +} + +export function loadRecordings( + elem: HTMLElement, + activeRecordingsOnly: boolean, + apiClient: ApiClient, + options: SectionOptions +) { + const title = activeRecordingsOnly ? + globalize.translate('HeaderActiveRecordings') : + globalize.translate('HeaderLatestRecordings'); + + let html = ''; + + html += '
'; + html += '

' + title + '

'; + html += '
'; + + if (options.enableOverflow) { + html += '
'; + html += '
'; + } else { + html += '
'; + } + + if (options.enableOverflow) { + html += '
'; + } + html += '
'; + + elem.classList.add('hide'); + elem.innerHTML = html; + + const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer'); + if (!itemsContainer) return; + itemsContainer.fetchData = getLatestRecordingsFetchFn(apiClient.serverId(), activeRecordingsOnly, options); + itemsContainer.getItemsHtml = getLatestRecordingItemsHtml(activeRecordingsOnly, options); + itemsContainer.parentContainer = elem; +} diff --git a/src/components/homesections/sections/libraryButtons.ts b/src/components/homesections/sections/libraryButtons.ts new file mode 100644 index 0000000000..06656c343a --- /dev/null +++ b/src/components/homesections/sections/libraryButtons.ts @@ -0,0 +1,36 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import escapeHtml from 'escape-html'; + +import imageLoader from 'components/images/imageLoader'; +import { appRouter } from 'components/router/appRouter'; +import globalize from 'scripts/globalize'; +import imageHelper from 'scripts/imagehelper'; + +function getLibraryButtonsHtml(items: BaseItemDto[]) { + let html = ''; + + html += '
'; + html += '

' + globalize.translate('HeaderMyMedia') + '

'; + + html += '
'; + + // library card background images + for (let i = 0, length = items.length; i < length; i++) { + const item = items[i]; + const icon = imageHelper.getLibraryIcon(item.CollectionType); + html += '' + escapeHtml(item.Name) + ''; + } + + html += '
'; + html += '
'; + + return html; +} + +export function loadLibraryButtons(elem: HTMLElement, userViews: BaseItemDto[]) { + elem.classList.remove('verticalSection'); + const html = getLibraryButtonsHtml(userViews); + + elem.innerHTML = html; + imageLoader.lazyChildren(elem); +} diff --git a/src/components/homesections/sections/libraryTiles.ts b/src/components/homesections/sections/libraryTiles.ts new file mode 100644 index 0000000000..6ccfc528c5 --- /dev/null +++ b/src/components/homesections/sections/libraryTiles.ts @@ -0,0 +1,46 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; + +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import imageLoader from 'components/images/imageLoader'; +import globalize from 'scripts/globalize'; +import { getBackdropShape } from 'utils/card'; + +import type { SectionOptions } from './section'; + +export function loadLibraryTiles( + elem: HTMLElement, + userViews: BaseItemDto[], + { + enableOverflow + }: SectionOptions +) { + let html = ''; + if (userViews.length) { + html += '

' + globalize.translate('HeaderMyMedia') + '

'; + if (enableOverflow) { + html += '
'; + html += '
'; + } else { + html += '
'; + } + + html += cardBuilder.getCardsHtml({ + items: userViews, + shape: getBackdropShape(enableOverflow), + showTitle: true, + centerText: true, + overlayText: false, + lazy: true, + transition: false, + allowBottomPadding: !enableOverflow + }); + + if (enableOverflow) { + html += '
'; + } + html += '
'; + } + + elem.innerHTML = html; + imageLoader.lazyChildren(elem); +} diff --git a/src/components/homesections/sections/liveTv.ts b/src/components/homesections/sections/liveTv.ts new file mode 100644 index 0000000000..7c8606c12b --- /dev/null +++ b/src/components/homesections/sections/liveTv.ts @@ -0,0 +1,181 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto'; +import type { ApiClient } from 'jellyfin-apiclient'; + +import { appRouter } from 'components/router/appRouter'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import layoutManager from 'components/layoutManager'; +import ServerConnections from 'components/ServerConnections'; +import globalize from 'scripts/globalize'; +import { getBackdropShape } from 'utils/card'; + +import type { SectionContainerElement, SectionOptions } from './section'; + +function getOnNowFetchFn( + serverId: string +) { + return function () { + const apiClient = ServerConnections.getApiClient(serverId); + return apiClient.getLiveTvRecommendedPrograms({ + userId: apiClient.getCurrentUserId(), + IsAiring: true, + limit: 24, + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Thumb,Backdrop', + EnableTotalRecordCount: false, + Fields: 'ChannelInfo,PrimaryImageAspectRatio' + }); + }; +} + +function getOnNowItemsHtmlFn( + { enableOverflow }: SectionOptions +) { + return (items: BaseItemDto[]) => ( + cardBuilder.getCardsHtml({ + items: items, + preferThumb: 'auto', + inheritThumb: false, + shape: (enableOverflow ? 'autooverflow' : 'auto'), + showParentTitleOrTitle: true, + showTitle: true, + centerText: true, + coverImage: true, + overlayText: false, + allowBottomPadding: !enableOverflow, + showAirTime: true, + showChannelName: false, + showAirDateTime: false, + showAirEndTime: true, + defaultShape: getBackdropShape(enableOverflow), + lines: 3, + overlayPlayButton: true + }) + ); +} + +function buildSection( + elem: HTMLElement, + serverId: string, + options: SectionOptions +) { + let html = ''; + + elem.classList.remove('padded-left'); + elem.classList.remove('padded-right'); + elem.classList.remove('padded-bottom'); + elem.classList.remove('verticalSection'); + + html += '
'; + html += '
'; + html += '

' + globalize.translate('LiveTV') + '

'; + html += '
'; + + if (options.enableOverflow) { + html += '
'; + html += '
'; + } else { + html += ''; + if (options.enableOverflow) { + html += '
'; + } + html += '
'; + html += '
'; + + html += '
'; + html += '
'; + + if (!layoutManager.tv) { + html += ''; + html += '

'; + html += globalize.translate('HeaderOnNow'); + html += '

'; + html += ''; + html += '
'; + } else { + html += '

' + globalize.translate('HeaderOnNow') + '

'; + } + html += '
'; + + if (options.enableOverflow) { + html += '
'; + html += '
'; + } else { + html += '
'; + } + + if (options.enableOverflow) { + html += '
'; + } + + html += '
'; + html += '
'; + + elem.innerHTML = html; + + const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer'); + if (!itemsContainer) return; + itemsContainer.parentContainer = elem; + itemsContainer.fetchData = getOnNowFetchFn(serverId); + itemsContainer.getItemsHtml = getOnNowItemsHtmlFn(options); +} + +export function loadLiveTV( + elem: HTMLElement, + apiClient: ApiClient, + user: UserDto, + options: SectionOptions +) { + if (!user.Policy?.EnableLiveTvAccess) { + return Promise.resolve(); + } + + return apiClient.getLiveTvRecommendedPrograms({ + userId: apiClient.getCurrentUserId(), + IsAiring: true, + limit: 1, + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Thumb,Backdrop', + EnableTotalRecordCount: false, + Fields: 'ChannelInfo,PrimaryImageAspectRatio' + }).then(function (result) { + if (result.Items?.length) { + buildSection(elem, apiClient.serverId(), options); + } + }); +} diff --git a/src/components/homesections/sections/nextUp.ts b/src/components/homesections/sections/nextUp.ts new file mode 100644 index 0000000000..4ab401ab86 --- /dev/null +++ b/src/components/homesections/sections/nextUp.ts @@ -0,0 +1,106 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import type { ApiClient } from 'jellyfin-apiclient'; + +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import layoutManager from 'components/layoutManager'; +import { appRouter } from 'components/router/appRouter'; +import ServerConnections from 'components/ServerConnections'; +import globalize from 'scripts/globalize'; +import type { UserSettings } from 'scripts/settings/userSettings'; +import { getBackdropShape } from 'utils/card'; + +import type { SectionContainerElement, SectionOptions } from './section'; + +function getNextUpFetchFn( + serverId: string, + userSettings: UserSettings, + { enableOverflow }: SectionOptions +) { + return function () { + const apiClient = ServerConnections.getApiClient(serverId); + const oldestDateForNextUp = new Date(); + oldestDateForNextUp.setDate(oldestDateForNextUp.getDate() - userSettings.maxDaysForNextUp()); + return apiClient.getNextUpEpisodes({ + Limit: enableOverflow ? 24 : 15, + Fields: 'PrimaryImageAspectRatio,DateCreated,BasicSyncInfo,Path,MediaSourceCount', + UserId: apiClient.getCurrentUserId(), + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', + EnableTotalRecordCount: false, + DisableFirstEpisode: false, + NextUpDateCutoff: oldestDateForNextUp.toISOString(), + EnableRewatching: userSettings.enableRewatchingInNextUp() + }); + }; +} + +function getNextUpItemsHtmlFn( + useEpisodeImages: boolean, + { enableOverflow }: SectionOptions +) { + return function (items: BaseItemDto[]) { + const cardLayout = false; + return cardBuilder.getCardsHtml({ + items: items, + preferThumb: true, + inheritThumb: !useEpisodeImages, + shape: getBackdropShape(enableOverflow), + overlayText: false, + showTitle: true, + showParentTitle: true, + lazy: true, + overlayPlayButton: true, + context: 'home', + centerText: !cardLayout, + allowBottomPadding: !enableOverflow, + cardLayout: cardLayout + }); + }; +} + +export function loadNextUp( + elem: HTMLElement, + apiClient: ApiClient, + userSettings: UserSettings, + options: SectionOptions +) { + let html = ''; + + html += '
'; + if (!layoutManager.tv) { + html += ''; + html += '

'; + html += globalize.translate('NextUp'); + html += '

'; + html += ''; + html += '
'; + } else { + html += '

'; + html += globalize.translate('NextUp'); + html += '

'; + } + html += '
'; + + if (options.enableOverflow) { + html += '
'; + html += '
'; + } else { + html += '
'; + } + + if (options.enableOverflow) { + html += '
'; + } + html += '
'; + + elem.classList.add('hide'); + elem.innerHTML = html; + + const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer'); + if (!itemsContainer) return; + itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings, options); + itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), options); + itemsContainer.parentContainer = elem; +} diff --git a/src/components/homesections/sections/recentlyAdded.ts b/src/components/homesections/sections/recentlyAdded.ts new file mode 100644 index 0000000000..8f41cbb5ca --- /dev/null +++ b/src/components/homesections/sections/recentlyAdded.ts @@ -0,0 +1,165 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto'; +import escapeHtml from 'escape-html'; +import type { ApiClient } from 'jellyfin-apiclient'; + +import layoutManager from 'components/layoutManager'; +import { appRouter } from 'components/router/appRouter'; +import globalize from 'scripts/globalize'; +import ServerConnections from 'components/ServerConnections'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card'; + +import type { SectionContainerElement, SectionOptions } from './section'; + +function getFetchLatestItemsFn( + serverId: string, + parentId: string | undefined, + collectionType: string | null | undefined, + { enableOverflow }: SectionOptions +) { + return function () { + const apiClient = ServerConnections.getApiClient(serverId); + let limit = 16; + + if (enableOverflow) { + if (collectionType === 'music') { + limit = 30; + } + } else if (collectionType === 'tvshows') { + limit = 5; + } else if (collectionType === 'music') { + limit = 9; + } else { + limit = 8; + } + + const options = { + Limit: limit, + Fields: 'PrimaryImageAspectRatio,BasicSyncInfo,Path', + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Thumb', + ParentId: parentId + }; + + return apiClient.getLatestItems(options); + }; +} + +function getLatestItemsHtmlFn( + itemType: BaseItemKind | undefined, + viewType: string | null | undefined, + { enableOverflow }: SectionOptions +) { + return function (items: BaseItemDto[]) { + const cardLayout = false; + let shape; + if (itemType === 'Channel' || viewType === 'movies' || viewType === 'books' || viewType === 'tvshows') { + shape = getPortraitShape(enableOverflow); + } else if (viewType === 'music' || viewType === 'homevideos') { + shape = getSquareShape(enableOverflow); + } else { + shape = getBackdropShape(enableOverflow); + } + + return cardBuilder.getCardsHtml({ + items: items, + shape: shape, + preferThumb: viewType !== 'movies' && viewType !== 'tvshows' && itemType !== 'Channel' && viewType !== 'music' ? 'auto' : null, + showUnplayedIndicator: false, + showChildCountIndicator: true, + context: 'home', + overlayText: false, + centerText: !cardLayout, + overlayPlayButton: viewType !== 'photos', + allowBottomPadding: !enableOverflow && !cardLayout, + cardLayout: cardLayout, + showTitle: viewType !== 'photos', + showYear: viewType === 'movies' || viewType === 'tvshows' || !viewType, + showParentTitle: viewType === 'music' || viewType === 'tvshows' || !viewType || (cardLayout && (viewType === 'tvshows')), + lines: 2 + }); + }; +} + +function renderLatestSection( + elem: HTMLElement, + apiClient: ApiClient, + user: UserDto, + parent: BaseItemDto, + options: SectionOptions +) { + let html = ''; + + html += '
'; + if (!layoutManager.tv) { + html += ''; + html += '

'; + html += globalize.translate('LatestFromLibrary', escapeHtml(parent.Name)); + html += '

'; + html += ''; + html += '
'; + } else { + html += '

' + globalize.translate('LatestFromLibrary', escapeHtml(parent.Name)) + '

'; + } + html += '
'; + + if (options.enableOverflow) { + html += '
'; + html += '
'; + } else { + html += '
'; + } + + if (options.enableOverflow) { + html += '
'; + } + html += '
'; + + elem.innerHTML = html; + + const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer'); + if (!itemsContainer) return; + itemsContainer.fetchData = getFetchLatestItemsFn(apiClient.serverId(), parent.Id, parent.CollectionType, options); + itemsContainer.getItemsHtml = getLatestItemsHtmlFn(parent.Type, parent.CollectionType, options); + itemsContainer.parentContainer = elem; +} + +export function loadRecentlyAdded( + elem: HTMLElement, + apiClient: ApiClient, + user: UserDto, + userViews: BaseItemDto[], + options: SectionOptions +) { + elem.classList.remove('verticalSection'); + const excludeViewTypes = ['playlists', 'livetv', 'boxsets', 'channels']; + const userExcludeItems = user.Configuration?.LatestItemsExcludes ?? []; + + for (let i = 0, length = userViews.length; i < length; i++) { + const item = userViews[i]; + if ( + !item.Id + || userExcludeItems.indexOf(item.Id) !== -1 + ) { + continue; + } + + if ( + !item.CollectionType + || excludeViewTypes.indexOf(item.CollectionType) !== -1 + ) { + continue; + } + + const frag = document.createElement('div'); + frag.classList.add('verticalSection'); + frag.classList.add('hide'); + elem.appendChild(frag); + + renderLatestSection(frag, apiClient, user, item, options); + } +} diff --git a/src/components/homesections/sections/resume.ts b/src/components/homesections/sections/resume.ts new file mode 100644 index 0000000000..e96dde3ee0 --- /dev/null +++ b/src/components/homesections/sections/resume.ts @@ -0,0 +1,105 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import type { ApiClient } from 'jellyfin-apiclient'; + +import ServerConnections from 'components/ServerConnections'; +import cardBuilder from 'components/cardbuilder/cardBuilder'; +import globalize from 'scripts/globalize'; +import type { UserSettings } from 'scripts/settings/userSettings'; +import { getBackdropShape, getPortraitShape } from 'utils/card'; + +import type { SectionContainerElement, SectionOptions } from './section'; + +const dataMonitorHints: Record = { + Audio: 'audioplayback,markplayed', + Video: 'videoplayback,markplayed' +}; + +function getItemsToResumeFn( + mediaType: BaseItemKind, + serverId: string, + { enableOverflow }: SectionOptions +) { + return function () { + const apiClient = ServerConnections.getApiClient(serverId); + + const limit = enableOverflow ? 12 : 5; + + const options = { + Limit: limit, + Recursive: true, + Fields: 'PrimaryImageAspectRatio,BasicSyncInfo', + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Thumb', + EnableTotalRecordCount: false, + MediaTypes: mediaType + }; + + return apiClient.getResumableItems(apiClient.getCurrentUserId(), options); + }; +} + +function getItemsToResumeHtmlFn( + useEpisodeImages: boolean, + mediaType: BaseItemKind, + { enableOverflow }: SectionOptions +) { + return function (items: BaseItemDto[]) { + const cardLayout = false; + return cardBuilder.getCardsHtml({ + items: items, + preferThumb: true, + inheritThumb: !useEpisodeImages, + shape: (mediaType === 'Book') ? + getPortraitShape(enableOverflow) : + getBackdropShape(enableOverflow), + overlayText: false, + showTitle: true, + showParentTitle: true, + lazy: true, + showDetailsMenu: true, + overlayPlayButton: true, + context: 'home', + centerText: !cardLayout, + allowBottomPadding: false, + cardLayout: cardLayout, + showYear: true, + lines: 2 + }); + }; +} + +export function loadResume( + elem: HTMLElement, + apiClient: ApiClient, + titleLabel: string, + mediaType: BaseItemKind, + userSettings: UserSettings, + options: SectionOptions +) { + let html = ''; + + const dataMonitor = dataMonitorHints[mediaType] ?? 'markplayed'; + + html += '

' + globalize.translate(titleLabel) + '

'; + if (options.enableOverflow) { + html += '
'; + html += `
`; + } else { + html += `
`; + } + + if (options.enableOverflow) { + html += '
'; + } + html += '
'; + + elem.classList.add('hide'); + elem.innerHTML = html; + + const itemsContainer: SectionContainerElement | null = elem.querySelector('.itemsContainer'); + if (!itemsContainer) return; + itemsContainer.fetchData = getItemsToResumeFn(mediaType, apiClient.serverId(), options); + itemsContainer.getItemsHtml = getItemsToResumeHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), mediaType, options); + itemsContainer.parentContainer = elem; +} diff --git a/src/components/homesections/sections/section.d.ts b/src/components/homesections/sections/section.d.ts new file mode 100644 index 0000000000..27f7c37708 --- /dev/null +++ b/src/components/homesections/sections/section.d.ts @@ -0,0 +1,11 @@ +import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; + +export interface SectionOptions { + enableOverflow: boolean +} + +export type SectionContainerElement = { + fetchData: () => void + getItemsHtml: (items: BaseItemDto[]) => void + parentContainer: HTMLElement +} & Element; diff --git a/src/scripts/settings/userSettings.js b/src/scripts/settings/userSettings.js index 691b07cb4d..9dc0656219 100644 --- a/src/scripts/settings/userSettings.js +++ b/src/scripts/settings/userSettings.js @@ -301,7 +301,7 @@ export class UserSettings { /** * Get or set 'Use Episode Images in Next Up and Continue Watching' state. - * @param {string|boolean|undefined} val - Flag to enable 'Use Episode Images in Next Up and Continue Watching' or undefined. + * @param {string|boolean|undefined} [val] - Flag to enable 'Use Episode Images in Next Up and Continue Watching' or undefined. * @return {boolean} 'Use Episode Images in Next Up' state. */ useEpisodeImagesInNextUpAndResume(val) { @@ -463,7 +463,7 @@ export class UserSettings { /** * Get or set max days for next up list. - * @param {number|undefined} val - Max days for next up. + * @param {number|undefined} [val] - Max days for next up. * @return {number} Max days for a show to stay in next up without being watched. */ maxDaysForNextUp(val) { @@ -482,7 +482,7 @@ export class UserSettings { /** * Get or set rewatching in next up. - * @param {boolean|undefined} val - If rewatching items should be included in next up. + * @param {boolean|undefined} [val] - If rewatching items should be included in next up. * @returns {boolean} Rewatching in next up state. */ enableRewatchingInNextUp(val) { diff --git a/src/types/homeSectionType.ts b/src/types/homeSectionType.ts new file mode 100644 index 0000000000..1a3e6eb876 --- /dev/null +++ b/src/types/homeSectionType.ts @@ -0,0 +1,27 @@ +// NOTE: This should be included in the OpenAPI spec ideally +// https://github.com/jellyfin/jellyfin/blob/1b4394199a2f9883cd601bdb8c9d66015397aa52/Jellyfin.Data/Enums/HomeSectionType.cs +export enum HomeSectionType { + None = 'none', + SmallLibraryTiles = 'smalllibrarytiles', + LibraryButtons = 'librarybuttons', + ActiveRecordings = 'activerecordings', + Resume = 'resume', + ResumeAudio = 'resumeaudio', + LatestMedia = 'latestmedia', + NextUp = 'nextup', + LiveTv = 'livetv', + ResumeBook = 'resumebook' +} + +// NOTE: This needs to match the server defaults +// https://github.com/jellyfin/jellyfin/blob/1b4394199a2f9883cd601bdb8c9d66015397aa52/Jellyfin.Api/Controllers/DisplayPreferencesController.cs#L120 +export const DEFAULT_SECTIONS: HomeSectionType[] = [ + HomeSectionType.SmallLibraryTiles, + HomeSectionType.Resume, + HomeSectionType.ResumeAudio, + HomeSectionType.ResumeBook, + HomeSectionType.LiveTv, + HomeSectionType.NextUp, + HomeSectionType.LatestMedia, + HomeSectionType.None +]; From 8149076fc54c124219091070674e5dfff6add622 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Mon, 2 Oct 2023 13:43:06 -0400 Subject: [PATCH 48/70] Fix function return type --- src/components/homesections/sections/section.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/homesections/sections/section.d.ts b/src/components/homesections/sections/section.d.ts index 27f7c37708..9307ba7c13 100644 --- a/src/components/homesections/sections/section.d.ts +++ b/src/components/homesections/sections/section.d.ts @@ -1,11 +1,12 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto-query-result'; export interface SectionOptions { enableOverflow: boolean } export type SectionContainerElement = { - fetchData: () => void + fetchData: () => Promise getItemsHtml: (items: BaseItemDto[]) => void parentContainer: HTMLElement } & Element; From 81c3a43601708d29c9c7812778e13d81e64251c1 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Thu, 5 Oct 2023 08:55:19 -0400 Subject: [PATCH 49/70] Use foreach for recently added sections --- .../homesections/sections/recentlyAdded.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/components/homesections/sections/recentlyAdded.ts b/src/components/homesections/sections/recentlyAdded.ts index 8f41cbb5ca..3c46ecbbe2 100644 --- a/src/components/homesections/sections/recentlyAdded.ts +++ b/src/components/homesections/sections/recentlyAdded.ts @@ -139,20 +139,13 @@ export function loadRecentlyAdded( const excludeViewTypes = ['playlists', 'livetv', 'boxsets', 'channels']; const userExcludeItems = user.Configuration?.LatestItemsExcludes ?? []; - for (let i = 0, length = userViews.length; i < length; i++) { - const item = userViews[i]; - if ( - !item.Id - || userExcludeItems.indexOf(item.Id) !== -1 - ) { - continue; + userViews.forEach(item => { + if (!item.Id || userExcludeItems.indexOf(item.Id) !== -1) { + return; } - if ( - !item.CollectionType - || excludeViewTypes.indexOf(item.CollectionType) !== -1 - ) { - continue; + if (!item.CollectionType || excludeViewTypes.indexOf(item.CollectionType) !== -1) { + return; } const frag = document.createElement('div'); @@ -161,5 +154,5 @@ export function loadRecentlyAdded( elem.appendChild(frag); renderLatestSection(frag, apiClient, user, item, options); - } + }); } From 60102f28b645fb7b0d1e6c1683f3ba165f49ad16 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Thu, 28 Sep 2023 02:22:58 -0400 Subject: [PATCH 50/70] Refactor GH actions --- .github/workflows/{tsc.yml => build.yml} | 23 ++++++--- .../{codeql-analysis.yml => codeql.yml} | 23 +++++---- .github/workflows/{lint.yml => quality.yml} | 48 ++++++++++++++++++- .../workflows/{repo-stale.yaml => stale.yml} | 0 4 files changed, 76 insertions(+), 18 deletions(-) rename .github/workflows/{tsc.yml => build.yml} (56%) rename .github/workflows/{codeql-analysis.yml => codeql.yml} (71%) rename .github/workflows/{lint.yml => quality.yml} (68%) rename .github/workflows/{repo-stale.yaml => stale.yml} (100%) diff --git a/.github/workflows/tsc.yml b/.github/workflows/build.yml similarity index 56% rename from .github/workflows/tsc.yml rename to .github/workflows/build.yml index 35bde340f9..e6d71a2408 100644 --- a/.github/workflows/tsc.yml +++ b/.github/workflows/build.yml @@ -1,14 +1,19 @@ -name: TypeScript Build Check +name: Build + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true on: push: branches: [ master, release* ] pull_request: branches: [ master, release* ] + workflow_dispatch: jobs: - tsc: - name: Run TypeScript build check + run-build-prod: + name: Run production build runs-on: ubuntu-latest steps: @@ -25,8 +30,12 @@ jobs: - name: Install Node.js dependencies run: npm ci --no-audit - - name: Run tsc - run: npm run build:check + - name: Run a production build + run: npm run build:production - - name: Run test suite - run: npm run test + - name: Upload artifact + uses: actions/upload-artifact@v3.1.3 + with: + name: jellyfin-web__prod + path: | + dist diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql.yml similarity index 71% rename from .github/workflows/codeql-analysis.yml rename to .github/workflows/codeql.yml index 6c8ffdbbf1..3e08d9ce1f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql.yml @@ -1,31 +1,34 @@ -name: "CodeQL" +name: CodeQL + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true on: push: - branches: [ master ] + branches: [ master, release* ] pull_request: - branches: [ master ] + branches: [ master, release* ] schedule: - cron: '30 7 * * 6' jobs: - analyze: - name: Analyze + codeql: + name: CodeQL runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] steps: - name: Checkout repository uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Initialize CodeQL uses: github/codeql-action/init@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3 with: - languages: ${{ matrix.language }} + languages: javascript queries: +security-extended + - name: Autobuild uses: github/codeql-action/autobuild@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3 + - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3 diff --git a/.github/workflows/lint.yml b/.github/workflows/quality.yml similarity index 68% rename from .github/workflows/lint.yml rename to .github/workflows/quality.yml index b754665bca..b4840d90c4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/quality.yml @@ -1,4 +1,8 @@ -name: Lint +name: Quality checks + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true on: push: @@ -99,3 +103,45 @@ jobs: - name: Run stylelint run: npm run stylelint:scss + + run-tsc: + name: Run TypeScript build check + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + + - name: Setup node environment + uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + with: + node-version: 20 + check-latest: true + cache: npm + + - name: Install Node.js dependencies + run: npm ci --no-audit + + - name: Run tsc + run: npm run build:check + + run-test: + name: Run tests + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + + - name: Setup node environment + uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + with: + node-version: 20 + check-latest: true + cache: npm + + - name: Install Node.js dependencies + run: npm ci --no-audit + + - name: Run test suite + run: npm run test diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/stale.yml similarity index 100% rename from .github/workflows/repo-stale.yaml rename to .github/workflows/stale.yml From 203102c4b9f63ce8229e0f660442a29997c5262c Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Thu, 28 Sep 2023 18:57:00 -0400 Subject: [PATCH 51/70] Add CF pages publish actions --- .github/workflows/job-messages.yml | 65 +++++++++++++++++++++ .github/workflows/publish.yml | 94 ++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 .github/workflows/job-messages.yml create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/job-messages.yml b/.github/workflows/job-messages.yml new file mode 100644 index 0000000000..0e47838ba9 --- /dev/null +++ b/.github/workflows/job-messages.yml @@ -0,0 +1,65 @@ +name: Job messages + +on: + workflow_call: + inputs: + branch: + required: false + type: string + commit: + required: true + type: string + preview_url: + required: false + type: string + build_workflow_run_id: + required: false + type: number + commenting_workflow_run_id: + required: true + type: string + in_progress: + required: true + type: boolean + outputs: + msg: + description: The composed message + value: ${{ jobs.msg.outputs.msg }} + marker: + description: Hidden marker to detect PR comments composed by the bot + value: "CFPages-deployment" + +jobs: + msg: + name: Deployment status + runs-on: ubuntu-latest + outputs: + msg: ${{ env.msg }} + + steps: + - name: Compose message + if: ${{ always() }} + id: compose + env: + COMMIT: ${{ inputs.commit }} + PREVIEW_URL: ${{ inputs.preview_url != '' && (inputs.branch != 'master' && inputs.preview_url || format('https://jf-web.pages.dev ({0})', inputs.preview_url)) || 'Not available' }} + DEPLOY_STATUS: ${{ inputs.in_progress && '🔄 Deploying...' || (inputs.preview_url != '' && '✅ Deployed!' || '❌ Failure. Check workflow logs for details') }} + DEPLOYMENT_TYPE: ${{ inputs.branch != 'master' && '🔀 Preview' || '⚙️ Production' }} + BUILD_WORKFLOW_RUN: ${{ !inputs.in_progress && format('**[View build logs](https://github.com/{0}/actions/runs/{1})**', 'jellyfin/jellyfin-web', inputs.build_workflow_run_id) || '' }} + COMMENTING_WORKFLOW_RUN: ${{ format('**[View bot logs](https://github.com/{0}/actions/runs/{1})**', 'jellyfin/jellyfin-web', inputs.commenting_workflow_run_id) }} + # EOF is needed for multiline environment variables in a GitHub Actions context + run: | + echo "## Cloudflare Pages deployment" > $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| **Latest commit** | ${COMMIT::7} |" >> $GITHUB_STEP_SUMMARY + echo "|------------------------- |:----------------------------: |" >> $GITHUB_STEP_SUMMARY + echo "| **Status** | $DEPLOY_STATUS |" >> $GITHUB_STEP_SUMMARY + echo "| **Preview URL** | $PREVIEW_URL |" >> $GITHUB_STEP_SUMMARY + echo "| **Type** | $DEPLOYMENT_TYPE |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "$BUILD_WORKFLOW_RUN" >> $GITHUB_STEP_SUMMARY + echo "$COMMENTING_WORKFLOW_RUN" >> $GITHUB_STEP_SUMMARY + COMPOSED_MSG=$(cat $GITHUB_STEP_SUMMARY) + echo "msg<> $GITHUB_ENV + echo "$COMPOSED_MSG" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000000..2b7bbea712 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,94 @@ +on: + workflow_run: + workflows: + - Build + types: + - completed + +jobs: + publish: + permissions: + contents: read + deployments: write + + name: Deploy to Cloudflare Pages + runs-on: ubuntu-latest + outputs: + url: ${{ steps.cf.outputs.url }} + + steps: + - name: Download workflow artifact + uses: dawidd6/action-download-artifact@v2.27.0 + with: + run_id: ${{ github.event.workflow_run.id }} + name: jellyfin-web__prod + path: dist + + - name: Publish + id: cf + uses: cloudflare/pages-action@1 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: jf-web + directory: dist + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + + pr-context: + name: PR context + if: ${{ always() && github.event.workflow_run.event == 'pull_request' }} + runs-on: ubuntu-latest + outputs: + commit: ${{ env.pr_sha }} + pr_number: ${{ env.pr_number }} + + steps: + - name: Get PR context + uses: dawidd6/action-download-artifact@v2.27.0 + id: pr_context + with: + run_id: ${{ github.event.workflow_run.id }} + name: PR_context + + - name: Set PR context environment variables + if: ${{ steps.pr_context.conclusion == 'success' }} + run: | + echo "pr_number=$(cat PR_number)" >> $GITHUB_ENV + echo "pr_sha=$(cat PR_sha)" >> $GITHUB_ENV + + compose-comment: + name: Compose comment + if: ${{ always() }} + uses: ./.github/workflows/job-messages.yml + needs: + - publish + - pr-context + + with: + branch: ${{ github.event.workflow_run.head_branch }} + commit: ${{ needs.pr-context.outputs.commit != '' && needs.pr-context.outputs.commit || github.event.workflow_run.head_sha }} + preview_url: ${{ needs.publish.outputs.url }} + build_workflow_run_id: ${{ github.event.workflow_run.id }} + commenting_workflow_run_id: ${{ github.run_id }} + in_progress: false + + comment-status: + name: Create comment status + if: | + always() && + github.event.workflow_run.event == 'pull_request' && + needs.pr-context.outputs.pr_number != '' + runs-on: ubuntu-latest + needs: + - compose-comment + - pr-context + + steps: + - name: Update job summary in PR comment + uses: thollander/actions-comment-pull-request@v2.4.2 + with: + GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }} + message: ${{ needs.compose-comment.outputs.msg }} + pr_number: ${{ needs.pr-context.outputs.pr_number }} + comment_tag: ${{ needs.compose-comment.outputs.marker }} + mode: recreate From c1b3a3f60d394c28bc5d51c5c269ff1b592b18f0 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Fri, 29 Sep 2023 00:34:49 -0400 Subject: [PATCH 52/70] Update codeql job name --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3e08d9ce1f..34d1fafbf1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -14,7 +14,7 @@ on: jobs: codeql: - name: CodeQL + name: Run CodeQL runs-on: ubuntu-latest steps: From 87ff0e4fda8db912b9acf9f1eda31e6e0516cf54 Mon Sep 17 00:00:00 2001 From: Zoe Date: Thu, 5 Oct 2023 17:15:50 +0000 Subject: [PATCH 53/70] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)=20Translation:=20Jellyfin/Jellyfin=20Web=20Tran?= =?UTF-8?q?slate-URL:=20https://translate.jellyfin.org/projects/jellyfin/j?= =?UTF-8?q?ellyfin-web/nb=5FNO/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/strings/nb.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/strings/nb.json b/src/strings/nb.json index eaf915e98a..91f77b623c 100644 --- a/src/strings/nb.json +++ b/src/strings/nb.json @@ -1770,5 +1770,10 @@ "MachineTranslated": "Maskinoversatt", "HeaderGuestCast": "Gjestestjerner", "HearingImpairedShort": "HI/SDH", - "LogoScreensaver": "Logoskjermsparer" + "LogoScreensaver": "Logoskjermsparer", + "LabelIsHearingImpaired": "For hørselshemmede (SDH)", + "LabelBackdropScreensaverInterval": "Skjermsparingsbakgrunn-intervall", + "LabelBackdropScreensaverIntervalHelp": "Tid i milisekunder mellom forskjellige bakgrunner ved bruk av skjermsparingsbakgrunner.", + "BackdropScreensaver": "Skjermsparingsbakgrunn", + "ForeignPartsOnly": "Kun tvungne/fremmede deler" } From ac6c4c449c42fa31c56298b07d7c8335b856a36d Mon Sep 17 00:00:00 2001 From: felix920506 Date: Thu, 5 Oct 2023 23:45:30 +0000 Subject: [PATCH 54/70] Translated using Weblate (Chinese (Traditional)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant/ --- src/strings/zh-tw.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strings/zh-tw.json b/src/strings/zh-tw.json index b4825af3bd..d6362e2c0b 100644 --- a/src/strings/zh-tw.json +++ b/src/strings/zh-tw.json @@ -822,7 +822,7 @@ "LabelFormat": "格式", "LabelFriendlyName": "好聽的名字", "LabelGroupMoviesIntoCollections": "將電影分組", - "LabelKodiMetadataDateFormat": "釋出日期格式", + "LabelKodiMetadataDateFormat": "發行日期格式", "LabelIconMaxWidth": "Icon 最寬寬度", "LabelGroupMoviesIntoCollectionsHelp": "選擇檢視電影清單時,集合中的電影將作為一個分組項目顯示。", "LabelEncoderPreset": "預設編碼", From 70557d6c1ca3051688e6d3522a8bf9b33b11b799 Mon Sep 17 00:00:00 2001 From: Leo Date: Fri, 6 Oct 2023 03:21:32 +0000 Subject: [PATCH 55/70] Translated using Weblate (Chinese (Simplified)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/ --- src/strings/zh-cn.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/strings/zh-cn.json b/src/strings/zh-cn.json index 8a0f36db6c..47986abc89 100644 --- a/src/strings/zh-cn.json +++ b/src/strings/zh-cn.json @@ -1773,5 +1773,8 @@ "ForeignPartsOnly": "仅限强制开启/外语部分", "HearingImpairedShort": "听障/聋哑人士字幕", "UnknownError": "发生未知错误。", - "GoHome": "回家" + "GoHome": "回家", + "BackdropScreensaver": "背景屏保", + "LogoScreensaver": "徽标屏保", + "LabelIsHearingImpaired": "用于听障/聋哑人士" } From 73af57b81e55a2b0094909577dafc9d9dfed7159 Mon Sep 17 00:00:00 2001 From: Brad Beattie Date: Thu, 15 Dec 2022 18:55:41 -0800 Subject: [PATCH 56/70] Upping search limit --- src/components/search/SearchResults.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/search/SearchResults.tsx b/src/components/search/SearchResults.tsx index f8e5a12fbd..ebb5240b28 100644 --- a/src/components/search/SearchResults.tsx +++ b/src/components/search/SearchResults.tsx @@ -49,7 +49,7 @@ const SearchResults: FunctionComponent = ({ serverId = windo const getDefaultParameters = useCallback(() => ({ ParentId: parentId, searchTerm: query, - Limit: 24, + Limit: 100, Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount', Recursive: true, EnableTotalRecordCount: false, From cf516e9debbf4d644259d3c658111417dd65ac5b Mon Sep 17 00:00:00 2001 From: Randy Torres Date: Sat, 7 Oct 2023 04:53:01 +0000 Subject: [PATCH 57/70] Translated using Weblate (Malay) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ms/ --- src/strings/ms.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/strings/ms.json b/src/strings/ms.json index 6a000566e9..d950d6ff6c 100644 --- a/src/strings/ms.json +++ b/src/strings/ms.json @@ -251,5 +251,12 @@ "AllowSegmentDeletionHelp": "Padam segmen lama setelah ia dihantar ke pelayan. Ini menghalang file transcode disimpan dalam disk. Ia akan berfungsi dengan penghad haju dihidupkan. Matikan tetapan ini jika anda mengalami isu dengan pemain video.", "LabelThrottleDelaySeconds": "Penghad laju setelah", "Settings": "Tetapan", - "SelectServer": "Pilih pelayan" + "SelectServer": "Pilih pelayan", + "ButtonBackspace": "pendikit", + "ButtonSpace": "Ruang", + "BackdropScreensaver": "Penyimpan skrin latar belakang", + "Cursive": "Sambung", + "DefaultSubtitlesHelp": "Sari kata dimuatkan berdasarkan bendera lalai dan paksa dalam metadata yang disematkan. Keutamaan bahasa diambil kira apabila terdapat beberapa pilihan.", + "LabelThrottleDelaySecondsHelp": "Masa dalam saat selepas mana transkoder akan dihadkan. Mesti cukup besar untuk klien mengekalkan penimbal yang sihat. Hanya berfungsi jika penghadan diaktifkan.", + "LabelSegmentKeepSeconds": "Masa untuk mengekalkan segmen" } From 0587cda60823e8c7a65083978816ff9e6fba145e Mon Sep 17 00:00:00 2001 From: Luis Aceituno Date: Sat, 7 Oct 2023 14:53:14 +0000 Subject: [PATCH 58/70] Translated using Weblate (Spanish (Mexico)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_MX/ --- src/strings/es-mx.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/strings/es-mx.json b/src/strings/es-mx.json index 3fb0abb6a3..7431d0fdf7 100644 --- a/src/strings/es-mx.json +++ b/src/strings/es-mx.json @@ -1264,7 +1264,7 @@ "AskAdminToCreateLibrary": "Pide a un administrador crear una biblioteca.", "Artist": "Artista", "AllowFfmpegThrottlingHelp": "Cuando una transcodificación o remuxeado se adelanta lo suficiente de la posición de reproducción actual, se pausa el proceso para que consuma menos recursos. Esto es más útil cuando se mira sin buscar con frecuencia. Apaga esto si experimentas problemas de reproducción.", - "AllowFfmpegThrottling": "Regular transcodificaciones", + "AllowFfmpegThrottling": "Limitar transcodificaciones", "AlbumArtist": "Artista del álbum", "Album": "Album", "Yadif": "YADIF", @@ -1756,10 +1756,10 @@ "AllowSegmentDeletion": "Borrar segmentos", "HeaderEpisodesStatus": "Estatus de los Episodios", "AllowSegmentDeletionHelp": "Borrar los viejos segmentos después de que hayan sido enviados al cliente. Esto previene que se tenga almacenado la totalidad de la transcodificación en el disco. Esto funciona unicamente cuando se tenga habilitado el throttling. Apagar esta opción cuando se tengan problemas de reproducción.", - "LabelThrottleDelaySeconds": "Acelerar después", + "LabelThrottleDelaySeconds": "Limitar después de", "LabelThrottleDelaySecondsHelp": "Tiempo en segundos después de que la transcodificación entre en aceleración. Deben ser los suficientes para que el buffer del cliente siga operando. Unicamente funciona si la aceleración está habilitada.", "LabelSegmentKeepSeconds": "Tiempo para guardar segmentos", - "LabelSegmentKeepSecondsHelp": "Tiempo en segundos en los que los segmentos deben permanecer antes de que sean sobrescritos. Estos deben de ser mayores a los indicados en \"Acelerar despues de\". Esto funciona unicamente si esta habilitada la opción de eliminar el segmento.", + "LabelSegmentKeepSecondsHelp": "Tiempo en segundos en los que los segmentos deben permanecer antes de que sean sobrescritos. Estos deben de ser mayores a los indicados en \"Limitar después de\". Esto funciona unicamente si esta habilitada la opción de eliminar el segmento.", "AllowAv1Encoding": "Permitir encodificación en formato AV1", "GoHome": "Ir a Inicio", "UnknownError": "Un error desconocido ocurrió.", From be2db1351ec6e4f89f2dba3abf3f434eda333992 Mon Sep 17 00:00:00 2001 From: Luis Aceituno Date: Sat, 7 Oct 2023 14:24:22 +0000 Subject: [PATCH 59/70] Translated using Weblate (Spanish) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es/ --- src/strings/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strings/es.json b/src/strings/es.json index ab3ac87d95..9c28914c1c 100644 --- a/src/strings/es.json +++ b/src/strings/es.json @@ -1246,7 +1246,7 @@ "LabelDroppedFrames": "Frames perdidos", "LabelCorruptedFrames": "Frames corruptos", "AskAdminToCreateLibrary": "Pídele a un administrador que cree una biblioteca.", - "AllowFfmpegThrottling": "Acelerar las conversiones", + "AllowFfmpegThrottling": "Limitar transcodificaciones", "ClientSettings": "Ajustes de cliente", "PreferEmbeddedEpisodeInfosOverFileNames": "Priorizar la información embebida sobre los nombres de archivos", "PreferEmbeddedEpisodeInfosOverFileNamesHelp": "Usar la información de episodio de los metadatos embebidos si está disponible.", From c3c6ebef9591e767f8d928aa0af3e17f4818fa59 Mon Sep 17 00:00:00 2001 From: Luis Aceituno Date: Sat, 7 Oct 2023 15:05:37 +0000 Subject: [PATCH 60/70] Translated using Weblate (Spanish (Latin America)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_419/ --- src/strings/es_419.json | 61 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/src/strings/es_419.json b/src/strings/es_419.json index 0a208c94e2..682ad9b6d9 100644 --- a/src/strings/es_419.json +++ b/src/strings/es_419.json @@ -1307,7 +1307,7 @@ "AllowRemoteAccessHelp": "Si no se marca, se bloquearán todas las conexiones remotas.", "AllowRemoteAccess": "Permitir conexiones remotas a este servidor", "AllowFfmpegThrottlingHelp": "Cuando una transcodificación o remuxeado se adelanta lo suficiente de la posición de reproducción actual, se pausa el proceso para que consuma menos recursos. Esto es más útil cuando se mira sin buscar con frecuencia. Apaga esto si experimentas problemas de reproducción.", - "AllowFfmpegThrottling": "Regular transcodificaciones", + "AllowFfmpegThrottling": "Limitar transcodificaciones", "AllowOnTheFlySubtitleExtractionHelp": "Los subtítulos incrustados pueden extraerse de los videos y entregarse a los clientes en texto plano para ayudar a evitar la transcodificación de video. En algunos sistemas, esto puede tardar mucho tiempo y provocar que la reproducción de video se detenga durante el proceso de extracción. Deshabilite esta opción para que los subtítulos incrustados se graben con transcodificación de video cuando no estén soportados de forma nativa por el dispositivo cliente.", "AllowOnTheFlySubtitleExtraction": "Permitir la extracción de subtítulos sobre la marcha", "AllowMediaConversionHelp": "Permitir o denegar acceso a la función de convertir medios.", @@ -1578,5 +1578,62 @@ "LabelMaxDaysForNextUp": "Días máximos en «A continuación»", "LabelEnableAudioVbrHelp": "La tasa de bits variable ofrece mejor calidad a la tasa media de bits promedio, pero en algunos raros casos puede causar almacenamiento en búfer y problemas de compatibilidad.", "LabelEnableAudioVbr": "Habilitar codificación VBR de audio", - "HeaderPerformance": "Rendimiento" + "HeaderPerformance": "Rendimiento", + "HeaderEpisodesStatus": "Status de los Episodios", + "LabelBackdropScreensaverInterval": "Intervalo del Protector de Pantalla de Fondo", + "LabelBackdropScreensaverIntervalHelp": "Intervalo en segundos entre los diferentes fondos cuando se usa el protector de pantalla de fondo.", + "LabelEnableLUFSScanHelp": "Permite a los clientes normalizar la reproducción de audio para tener el mismo volumen en todas las pistas. Esto hará que los escaneos de biblioteca tomen más tiempo y utilicen más recursos.", + "LabelSyncPlaySettingsSpeedToSyncHelp": "Método de corrección de sincronización que consiste en acelerar la reproducción. La corrección de sincronización debe estar activada.", + "LabelSyncPlaySettingsSkipToSyncHelp": "Método de corrección de sincronización que consiste en saltar a la posición de reproducción estimada. La Corrección de Sincronización debe estar activada.", + "ListView": "Vista en Lista", + "MessageNoItemsAvailable": "No hay elementos disponibles actualmente.", + "Bold": "Negrita", + "Larger": "Más grande", + "LabelSyncPlaySettingsMinDelaySkipToSyncHelp": "Retraso mínimo (en milisegundos) después del cual SkipToSync intentará corregir la posición de reproducción.", + "Localization": "Localización", + "GoHome": "Ir a Inicio", + "LabelSyncPlaySettingsSpeedToSync": "Activar SpeedToSync", + "LabelDeveloper": "Desarrollador", + "LabelLevel": "Nivel", + "LabelDate": "Fecha", + "EnableCardLayout": "Mostrar CardBox visual", + "LabelMediaDetails": "Detalles de multimedia", + "LabelSyncPlaySettingsSkipToSync": "Activar SkipToSync", + "LabelSystem": "Sistema", + "LogLevel.Debug": "Depurar", + "LogLevel.Trace": "Rastreo", + "LogLevel.Warning": "Advertencia", + "LogLevel.Error": "Error", + "LogLevel.Critical": "Crítico", + "GridView": "Vista en Cuadrícula", + "GetThePlugin": "Obtener el Plugin", + "LabelSyncPlayNoGroups": "No hay grupos disponibles", + "LabelTextWeight": "Grosor del texto", + "LogLevel.None": "Ningún", + "LabelSyncPlaySettingsSpeedToSyncDuration": "Duración de SpeedToSync", + "LabelSyncPlaySettingsMinDelaySkipToSync": "Retraso mínimo de SkipToSync", + "EnableAudioNormalizationHelp": "La normalización de audio añadirá una ganancia constante para mantener la media en el nivel deseado (-18dB).", + "LabelEnableLUFSScan": "Activar escaneo LUFS", + "MenuOpen": "Abrir Menú", + "MenuClose": "Cerrar Menú", + "BackdropScreensaver": "Protector de Pantalla de Fondo", + "LabelSyncPlaySettingsMaxDelaySpeedToSyncHelp": "Retraso máximo de reproducción (en milisegundos) después del cual SkipToSync se usará en lugar de SpeedToSync.", + "LogoScreensaver": "Protector de Pantalla de Logo", + "MessageNoFavoritesAvailable": "No hay favoritos disponibles actualmente.", + "LabelSyncPlaySettingsSpeedToSyncDurationHelp": "Cantidad de milisegundos que SpeedToSync utilizará para corregir la posición de reproducción.", + "HeaderConfirmRepositoryInstallation": "Confirma la instalación del repositorio de plugins", + "EnableAudioNormalization": "Normalización de audio", + "LogLevel.Information": "Información", + "AllowCollectionManagement": "Permitir a este usuario gestionar colecciones", + "Lyricist": "Letrista", + "AllowSegmentDeletion": "Borrar segmentos", + "AllowSegmentDeletionHelp": "Borrar segmentos viejos que ya hayan sido enviados al cliente. Esto evita tener que guardar la totalidad del archivo transcodificado en el disco. Funciona solamente si la opción \"regular transcodificaciones\" también está activada. Si se experimentan problemas en la reproducción, desactivar esta opción.", + "LabelThrottleDelaySeconds": "Limitar después de", + "LabelThrottleDelaySecondsHelp": "Duración en segundos después de la cual la transcodificación será limitada. Debe ser suficiente para permitirle al cliente mantener un buffer adecuado. Solo funciona si la limitación de la transcodificación está activada.", + "LabelSegmentKeepSeconds": "Tiempo de retención de segmentos", + "LabelSegmentKeepSecondsHelp": "Duración en segundos que se retendrán los segmentos antes de ser sobreescritos. Debe ser mayor que \"Limitar después de\". Solo funciona si la opción \"Borrar segmentos\" está activada.", + "LabelParallelImageEncodingLimit": "Límite de codificaciones paralelas de imágenes", + "LabelParallelImageEncodingLimitHelp": "Cantidad máxima de codificaciones de imágenes que pueden ejecutarse en paralelo. Al establecer 0 se elegirá un límite basado en las especificaciones de su sistema.", + "MediaInfoTitle": "Título", + "HeaderGuestCast": "Estrellas Invitadas" } From e5ad3c899e99ff34a3f06714e8399e90df9c8c1a Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Sun, 8 Oct 2023 23:43:31 -0400 Subject: [PATCH 61/70] Fix admin check in dashboard routes --- src/apps/dashboard/App.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/apps/dashboard/App.tsx b/src/apps/dashboard/App.tsx index c5ab679297..640a60d1f8 100644 --- a/src/apps/dashboard/App.tsx +++ b/src/apps/dashboard/App.tsx @@ -53,11 +53,11 @@ const DashboardApp = () => ( } /> - - {/* Suppress warnings for unhandled routes */} - + {/* Suppress warnings for unhandled routes */} + + {/* Redirects for old paths */} {REDIRECTS.map(toRedirectRoute)} From 50c3e04aacaf71a8291de8bb59e0e5a7709636f4 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Mon, 9 Oct 2023 10:17:54 -0400 Subject: [PATCH 62/70] Update CF Pages project name --- .github/workflows/job-messages.yml | 2 +- .github/workflows/publish.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/job-messages.yml b/.github/workflows/job-messages.yml index 0e47838ba9..7a58ecce76 100644 --- a/.github/workflows/job-messages.yml +++ b/.github/workflows/job-messages.yml @@ -42,7 +42,7 @@ jobs: id: compose env: COMMIT: ${{ inputs.commit }} - PREVIEW_URL: ${{ inputs.preview_url != '' && (inputs.branch != 'master' && inputs.preview_url || format('https://jf-web.pages.dev ({0})', inputs.preview_url)) || 'Not available' }} + PREVIEW_URL: ${{ inputs.preview_url != '' && (inputs.branch != 'master' && inputs.preview_url || format('https://jellyfin-web.pages.dev ({0})', inputs.preview_url)) || 'Not available' }} DEPLOY_STATUS: ${{ inputs.in_progress && '🔄 Deploying...' || (inputs.preview_url != '' && '✅ Deployed!' || '❌ Failure. Check workflow logs for details') }} DEPLOYMENT_TYPE: ${{ inputs.branch != 'master' && '🔀 Preview' || '⚙️ Production' }} BUILD_WORKFLOW_RUN: ${{ !inputs.in_progress && format('**[View build logs](https://github.com/{0}/actions/runs/{1})**', 'jellyfin/jellyfin-web', inputs.build_workflow_run_id) || '' }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2b7bbea712..1fcf65711e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,7 +30,7 @@ jobs: with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - projectName: jf-web + projectName: jellyfin-web directory: dist gitHubToken: ${{ secrets.GITHUB_TOKEN }} From 6c5393d2d397d6e42ef346e3d49e5aaf19ac176c Mon Sep 17 00:00:00 2001 From: Stas Ivanov Date: Mon, 9 Oct 2023 21:50:57 +0000 Subject: [PATCH 63/70] Translated using Weblate (Russian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/ --- src/strings/ru.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/strings/ru.json b/src/strings/ru.json index 4b1cd2c2a2..d584214bd0 100644 --- a/src/strings/ru.json +++ b/src/strings/ru.json @@ -1,6 +1,6 @@ { "Absolute": "Абсолютный", - "AccessRestrictedTryAgainLater": "В настоящее время доступ запрещён. Повторите попытку позже.", + "AccessRestrictedTryAgainLater": "В настоящее время доступ ограничен. Повторите попытку позже.", "Actor": "Актёр", "Add": "Добавить", "AddToCollection": "Добавить в коллекцию", @@ -1763,5 +1763,8 @@ "LabelSegmentKeepSeconds": "Время сохранения сегментов", "LogLevel.Error": "Ошибка", "LabelBackdropScreensaverInterval": "Интервал между фонами у заставки", - "LabelBackdropScreensaverIntervalHelp": "Время в секундах между разными фонами, когда используется заставка." + "LabelBackdropScreensaverIntervalHelp": "Время в секундах между разными фонами, когда используется заставка.", + "BackdropScreensaver": "Заставка", + "GoHome": "Домой", + "GridView": "Сеткой" } From 1320f96c719c973f4b42ac7c78c51a4915d815e3 Mon Sep 17 00:00:00 2001 From: SaddFox Date: Mon, 9 Oct 2023 20:13:19 +0000 Subject: [PATCH 64/70] Translated using Weblate (Slovenian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sl/ --- src/strings/sl-si.json | 74 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/src/strings/sl-si.json b/src/strings/sl-si.json index d583cf90d6..34164f0f64 100644 --- a/src/strings/sl-si.json +++ b/src/strings/sl-si.json @@ -648,7 +648,7 @@ "MessageNoMovieSuggestionsAvailable": "Trenutno ni na voljo nobenih predlogov za filme. Začnite gledati in ocenjevati vaše filme, ter se nato vrnite sem in si oglejte predloge.", "LabelSelectFolderGroups": "Samodejno združi vsebine iz spodnjih map v poglede kot so 'Filmi', 'Glasba' in 'TV'", "TitlePlayback": "Predvajanje", - "MessagePasswordResetForUsers": "Gesla naslednjih uporabnikov so bila ponastavljena. Zdaj se lahko prijavijo z Enostavnimi PIN kodami, ki so bile uporabljene za ponastavitev.", + "MessagePasswordResetForUsers": "Gesla naslednjih uporabnikov so bila ponastavljena. Zdaj se lahko prijavijo s PIN kodami, ki so bile uporabljene za ponastavitev.", "OptionHideUserFromLoginHelp": "Koristno za zasebne ali skrite skrbniške račune. Uporabnik se bo moral prijaviti ročno z vpisom svojega uporabniškega imena in gesla.", "OnlyForcedSubtitlesHelp": "Naložijo se zgolj podnapisi, ki so označeni kot prisiljeni.", "OptionEnableExternalContentInSuggestionsHelp": "Dovoli, da so spletni napovedniki in TV kanali v živo vključeni med priporočenimi vsebinami.", @@ -810,7 +810,7 @@ "OptionAutomaticallyGroupSeries": "Samodejno združi serije, ki so razdeljene po več mapah", "OptionAllowUserToManageServer": "Dovoli temu uporabniku upravljanje strežnika", "MessageTheFollowingLocationWillBeRemovedFromLibrary": "Naslednja mesta predstavnosti bodo odstranjena iz vaše knjižnice", - "MessagePluginInstallDisclaimer": "Dodatki ustvarjeni s strani članov skupnosti so odličen način za izboljšanje vaše izkušnje z dodatnimi funkcijami in prednostmi. Preden namestite dodatke se zavedajte, da imajo lahko ti različne vplive na vaš strežnik, kot na primer počasnejše preiskovanje knjižnic, dodatna obdelava podatkov v ozadju in zmanjšana stabilnost sistema.", + "MessagePluginInstallDisclaimer": "POZOR: Nameščanje dodatkov tretjih oseb predstavlja tveganje. Vsebujejo lahko nestabilno ali zlonamerno kodo, ki se lahko kadarkoli spremeni. Namestite zgolj dodatke avtorjev ki jim zaupate. Zavedajte se morebitnih stranskih učinkov, kot na primer komunikacija z zunanjimi storitvami, podaljšan čas pregledovanja knjižnice in dodatno procesiranje v ozadju.", "MessagePleaseWait": "Prosimo, počakajte. To lahko traja nekaj minut.", "MessagePleaseEnsureInternetMetadata": "Prosimo poskrbite, da je prenašanje spletnih metapodatkov omogočeno.", "MessageNothingHere": "Tu ni ničesar.", @@ -1308,7 +1308,7 @@ "QuickConnectDescription": "Za vpis s Hitro povezavo izberi 'Hitra povezava' na napravi preko katere se vpisuješ in vnesi kodo.", "QuickConnectDeactivated": "Hitra povezava je bila onemogočena pred dokončanjem vpisa", "QuickConnectAuthorizeFail": "Neznana koda za Hitro povezavo", - "QuickConnectAuthorizeSuccess": "Avtorizirano", + "QuickConnectAuthorizeSuccess": "Uspešno ste overili svojo napravo!", "QuickConnectAuthorizeCode": "Vnesi kodo {0} za vpis", "QuickConnectActivationSuccessful": "Aktivirano", "QuickConnect": "Hitra povezava", @@ -1402,7 +1402,7 @@ "LabelTonemappingAlgorithm": "Izberite algoritem za preslikavo barv", "LabelOpenclDeviceHelp": "To je naprava OpenCL, ki bo uporabljena za preslikavo barv. Na levi strani pike je številka platforme, desno je številka naprave na tej platformi. Privzeta vrednost je 0.0. Zahtevana je datoteka FFmpeg, ki vsebuje metodo strojnega pospeševanja OpenCL.", "LabelColorPrimaries": "Barvni prostor", - "AllowTonemappingHelp": "Preslikava barv lahko preslika dinamični razpon videa HDR v SDR, pri tem pa ohranja podrobnosti in barve, kar je zelo pomembno za predstavitev izvorne scene. Trenutno deluje zgolj z HDR10 in HLG videi. Zahteva ustrezne OpenCL ali CUDA knjižnice.", + "AllowTonemappingHelp": "Preslikava barv lahko preslika dinamični razpon videa iz HDR v SDR, pri tem pa ohranja podrobnosti in barve, kar je zelo pomembno za predstavitev izvorne scene. Trenutno deluje zgolj z HDR10 in HLG videi. Zahteva ustrezne OpenCL ali CUDA knjižnice.", "MediaInfoVideoRange": "Barvni razpon", "LabelVideoRange": "Barvni razpon", "LabelSonyAggregationFlags": "Sonyjeve agregacijske oznake", @@ -1647,10 +1647,10 @@ "LabelUserMaxActiveSessions": "Največje število hkratnih uporabniških sej", "LabelUDPPortRangeHelp": "Omeji Jellyfin na ta razpon vrat za UDP komunikacije. (Privzeto 1024 - 645535).
Opomba: Nekatere funkcije zahtevajo fiksna vrata, ki so lahko izven tega razpona.", "LabelUDPPortRange": "Razpon komunikacij UDP", - "LabelVppTonemappingContrastHelp": "Uporabi povečanje kontrasta pri VPP preslikavi barv. Priporočena in privzeta vrednost je 0.", + "LabelVppTonemappingContrastHelp": "Uporabi povečanje kontrasta pri VPP preslikavi barv. Priporočeni in privzeti vrednosti sta 1.", "LabelVppTonemappingBrightness": "VPP preslikava barv povečanje svetlosti", "LabelVppTonemappingContrast": "VPP preslikava barv povečanje kontrasta", - "LabelVppTonemappingBrightnessHelp": "Uporabi povečanje svetlosti pri VPP preslikavi barv. Priporočena in privzeta vrednost je 0.", + "LabelVppTonemappingBrightnessHelp": "Uporabi povečanje svetlosti pri VPP preslikavi barv. Priporočeni in privzeti vrednosti sta 16 in 0.", "AllowVppTonemappingHelp": "Preslikava barv v celoti na podlagi gonilnikov Intel. Trenutno deluje zgolj na določeni strojni opremi z HDR10 videi. Ima prednost pred drugimi OpenCL implementacijami.", "TonemappingAlgorithmHelp": "Preslikavo barv lahko podrobno nastavite. Če teh možnosti ne poznate, pustite privzete vrednosti. Priporočena vrednost je 'BT.2390'.", "LabelTonemappingThresholdHelp": "Parametri preslikave barv so natančno nastavljeni za vsak prizor. Prag je uporabljen za zaznavanje ali se je prizor spremenil ali ne. Če je razlika med povprečno svetlostjo trenutne sličice in tekočim povprečjem večja od nastavljenega praga, se vrednosti za povprečno in najvišjo svetlost prizora znova izračunajo. Priporočena in privzeta vrednost je 0,8 in 0,2.", @@ -1693,11 +1693,11 @@ "PreferEmbeddedExtrasTitlesOverFileNames": "Raje uporabi vdelane naslove kot imena datotek", "SaveRecordingNFOHelp": "Shrani metapodatke v isto mapo.", "LabelDummyChapterDuration": "interval", - "LabelDummyChapterDurationHelp": "Interval ekstrakcije slike poglavja v sekundah.", + "LabelDummyChapterDurationHelp": "Interval med navideznimi poglavji. Nastavite na 0 za onemogočanje ustvarjanja navideznih poglavij. Sprememba ne bo imela vpliva na obstoječa navidezna poglavja.", "LabelDummyChapterCount": "Limit", "LabelDummyChapterCountHelp": "Največje število slik poglavij, ki bodo ekstrahirane za vsako medijsko datoteko.", "LabelChapterImageResolution": "Resolucija", - "LabelChapterImageResolutionHelp": "Ločljivost slik poglavij.", + "LabelChapterImageResolutionHelp": "Ločljivost slik poglavij. Spreminjanje ne bo vplivalo na obstoječa lažna poglavja.", "ResolutionMatchSource": "ujemanje z virom", "HeaderRecordingMetadataSaving": "Snemanje metapodatkov", "AllowCollectionManagement": "Dovoli uporabniku upravljanje zbirk", @@ -1714,5 +1714,61 @@ "LabelSegmentKeepSeconds": "Čas ohranitve segmentov", "AllowSegmentDeletionHelp": "Izbriši segmente, ki so če bili poslani odjemalcu. S tem se prepreči, da bi na disku bila shranjena celotna prekodirana datoteka. To deluje le, če je omogočeno zaviranje prekodiranja. Če se pojavijo težave pri predvajanju, onemogočite to možnost.", "LabelThrottleDelaySecondsHelp": "Čas v sekundah, po katerem se bo prekodirnik upočasnil. Mora biti dovolj, da odjemalec ohranja ustrezen medpomnilnik. Deluje le, če je zaviranje prekodiranja omogočeno.", - "LabelSegmentKeepSecondsHelp": "Čas v skundah, ko naj se segmenti ohranijo, preden se jih prepiše. Čas mora biti večji od \"Zaviraj po\". Deluje le, če je brisanje segmentov omogočeno." + "LabelSegmentKeepSecondsHelp": "Čas v skundah, ko naj se segmenti ohranijo, preden se jih prepiše. Čas mora biti večji od \"Zaviraj po\". Deluje le, če je brisanje segmentov omogočeno.", + "MessageRepositoryInstallDisclaimer": "POZOR: Nameščanje skladišča dodatkov tretjih oseb predstavlja tveganje. Lahko vsebuje nestabilno ali zlonamerno kodo, ki se lahko kadarkoli spremeni. Namestite zgolj skladišča avtorjev ki jim zaupate.", + "Studio": "Studio", + "TonemappingModeHelp": "Izberi način preslikave barv. Če opazite razbarvanje svetlih delov slike, poskusite uporabiti način RGB.", + "LabelIsHearingImpaired": "Za naglušne (SDH)", + "AllowAv1Encoding": "Dovoli kodiranje v AV1 format", + "BackdropScreensaver": "Ozadje ohranjevalnika zaslona", + "LogoScreensaver": "Ohranjevalnik zaslona z logotipom", + "Short": "Kratki film", + "PasswordRequiredForAdmin": "Administratorski računi zahtevajo geslo.", + "SaveRecordingImages": "Shrani posnetke slik EPG", + "SubtitleBlack": "Črna", + "SubtitleRed": "Rdeča", + "LabelSyncPlayNoGroups": "Nobena skupina ni na voljo", + "NotificationsMovedMessage": "Funkcionalnost obvestil se je premaknila v dodatek Webhook.", + "SecondarySubtitles": "Sekundarni podnapisi", + "LabelDeveloper": "Razvijalec", + "LabelEnableAudioVbrHelp": "Spremenljiva bitna hitrost omogoča boljše razmerje med kvaliteto zvoka in povprečno bitno hitrostjo vendar lahko povzroči težave z medpomnjenjem in kompatibilnostjo.", + "LogLevel.None": "Brez", + "ForeignPartsOnly": "Samo vsiljeni/tuji deli", + "MachineTranslated": "Strojno prevedeno", + "LabelDate": "Datum", + "LabelEnableLUFSScan": "Omogoči skeniranje LUFS", + "LabelEnableLUFSScanHelp": "Odjemalci lahko normalizirajo glasnost zvoka za enako glasnost med skladbami. Pregledovanje knjižnice bo počasnejše in bo porabilo več sistemskih virov.", + "LabelLevel": "Nivo", + "LabelSystem": "Sistem", + "LogLevel.Trace": "", + "LogLevel.Warning": "Opozorilo", + "LogLevel.Information": "Informacije", + "Select": "Izberi", + "LogLevel.Error": "Napaka", + "LogLevel.Critical": "Kritično", + "GoHome": "Domov", + "LabelMediaDetails": "Podrobnosti predstavnosti", + "SubtitleWhite": "Bela", + "UnknownError": "Prišlo je do neznane napake.", + "Unknown": "Neznano", + "LabelBackdropScreensaverInterval": "Interval ozadja ohranjevalnika zaslona", + "LabelBackdropScreensaverIntervalHelp": "Čas v sekundah med različnimi ozadji za ohranjevalnik zaslona.", + "Notifications": "Obvestila", + "LabelEnableAudioVbr": "Omogoči VBR kodiranje zvoka", + "ListView": "Pogled seznama", + "PleaseConfirmRepositoryInstallation": "S klikom na OK potrjujete, da ste prebrali zgornje opozorilo in želite nadaljevati z namestitvijo skladišča dodatkov.", + "MenuOpen": "Odpri meni", + "MenuClose": "Zapri meni", + "UserMenu": "Uporabniški meni", + "LabelTonemappingMode": "Način preslikave barv", + "LabelParallelImageEncodingLimit": "Omejitev vzporednega kodiranja slik", + "SubtitleYellow": "Rumena", + "SubtitleBlue": "Modra", + "SubtitleCyan": "Cian", + "SubtitleGray": "Siva", + "SubtitleGreen": "Zelena", + "SubtitleLightGray": "Svetlo siva", + "SubtitleMagenta": "Magenta", + "LabelParallelImageEncodingLimitHelp": "Največje dovoljeno število vzporednih kodiranj slik. Nastavite na 0 za samodejno omejitev glede na zmogljivost vašega sistema.", + "AiTranslated": "AI prevedeno" } From 10db65500e7847b5f30d0d80b76ab64977e7ef39 Mon Sep 17 00:00:00 2001 From: krvi Date: Mon, 9 Oct 2023 20:45:09 +0000 Subject: [PATCH 65/70] Translated using Weblate (Faroese) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fo/ --- src/strings/fo.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/strings/fo.json b/src/strings/fo.json index 6532f3e13a..c98afcf767 100644 --- a/src/strings/fo.json +++ b/src/strings/fo.json @@ -212,5 +212,8 @@ "ValueMusicVideoCount": "{0} tónleika sjónbond", "ValueOneAlbum": "1 album", "ValueOneSong": "1 sangur", - "ValueSongCount": "{0} sangir" + "ValueSongCount": "{0} sangir", + "ButtonForgotPassword": "Gloymt loyniorð", + "ButtonSignIn": "Innrita", + "ButtonSignOut": "Útrita" } From e0f723a13a7e0e759450908dd5697ffaccf6425c Mon Sep 17 00:00:00 2001 From: hoanghuy309 Date: Tue, 10 Oct 2023 02:11:39 +0000 Subject: [PATCH 66/70] Translated using Weblate (Vietnamese) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/ --- src/strings/vi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strings/vi.json b/src/strings/vi.json index a53d197c41..693eb272a5 100644 --- a/src/strings/vi.json +++ b/src/strings/vi.json @@ -1,6 +1,6 @@ { "Add": "Thêm", - "All": "Tất cả", + "All": "Tất cả", "MessageBrowsePluginCatalog": "Duyệt danh mục plugin của chúng tôi để xem các plugin có sẵn.", "ButtonAddUser": "Thêm Người Dùng", "ButtonCancel": "Hủy bỏ", From a318b1d395408bb81faac79908ebbf9e52ee51f4 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 10 Oct 2023 00:39:31 -0400 Subject: [PATCH 67/70] Fix PR publish in GH actions --- .github/workflows/build.yml | 24 ++++++++++++++++++++++++ .github/workflows/publish.yml | 2 ++ 2 files changed, 26 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6d71a2408..0a94de4264 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,3 +39,27 @@ jobs: name: jellyfin-web__prod path: | dist + + pr_context: + name: Save PR context as artifact + if: ${{ always() && !cancelled() && github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + needs: + - run-build-prod + + steps: + - name: Save PR context + env: + PR_NUMBER: ${{ github.event.number }} + PR_SHA: ${{ github.sha }} + run: | + echo $PR_NUMBER > PR_number + echo $PR_SHA > PR_sha + + - name: Upload PR number as artifact + uses: actions/upload-artifact@v3.1.3 + with: + name: PR_context + path: | + PR_number + PR_sha diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1fcf65711e..96eaef368e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,3 +1,5 @@ +name: Publish + on: workflow_run: workflows: From 54d9d90e849ae458dc9af88ccdc92b67fb98bd11 Mon Sep 17 00:00:00 2001 From: RNGIllSkillz Date: Tue, 10 Oct 2023 14:31:55 +0000 Subject: [PATCH 68/70] Translated using Weblate (Russian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/ --- src/strings/ru.json | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/strings/ru.json b/src/strings/ru.json index d584214bd0..070ad5b791 100644 --- a/src/strings/ru.json +++ b/src/strings/ru.json @@ -1401,7 +1401,7 @@ "QuickConnectDescription": "Чтобы войти в систему с помощью Быстрого подключения, нажмите кнопку «Быстрое подключение» на устройстве, с которого вы выполняете вход, и введите указанный ниже код.", "QuickConnectDeactivated": "Быстрое подключение было деактивировано до утверждения запроса на вход", "QuickConnectAuthorizeFail": "Неопознанный код Быстрого подключения", - "QuickConnectAuthorizeSuccess": "Запрос авторизован", + "QuickConnectAuthorizeSuccess": "Вы успешно авторизовали ваше устройство!", "QuickConnectAuthorizeCode": "Введите код {0} для входа", "QuickConnectActivationSuccessful": "Активировано успешно", "QuickConnect": "Быстрое подключение", @@ -1424,7 +1424,7 @@ "LabelTonemappingPeakHelp": "Этим значением перекрывается сигнальный/номинальный/эталонный пик. Полезно, когда встроенная информация о пиках в метаданных дисплея ненадёжна или при тонмаппинге из узкого диапазона в более широкий. Значения рекомендуемое и по умолчанию - 100 и 0.", "LabelTonemappingThresholdHelp": "Параметры алгоритма тонмаппинга подстраиваются для каждой сцены. А порог используется, чтобы определить, изменилась ли сцена или нет. Если дистанция между средней яркостью текущего кадра и текущим скользящим средним превышает пороговое значение, мы пересчитаем среднюю и пиковую яркость сцены. Значения рекомендуемое и по умолчанию - 0.8 и 0.2.", "TonemappingAlgorithmHelp": "Тонмаппинг можно подстроить. Если вы не уверены с этими параметрами, оставьте значения по умолчанию. Рекомендуемое значение - \"BT.2390\".", - "AllowTonemappingHelp": "Тонмаппинг может преобразовать динамический диапазон видео из HDR в SDR, сохраняя детали изображения и цвета, которые являются очень важной информацией для представления исходной сцены. В настоящее время работает только с 10-бит HDR10, HLGи DoVi видео. Для этого требуется соответствующая среда выполнения OpenCL или CUDA.", + "AllowTonemappingHelp": "Тонмаппинг может преобразовать динамический диапазон видео из HDR в SDR, сохраняя детали изображения и цвета, которые являются очень важной информацией для представления исходного видео. В настоящее время работает только с 10-бит HDR10, HLG и DoVi. Для этого требуется соответствующая среда выполнения OpenCL или CUDA.", "LabelOpenclDeviceHelp": "Это устройство OpenCL, которое используется для тонмаппинга. Слева от точки - номер платформы, а справа - это номер устройства на платформе. Значение по умолчанию - 0.0. Требуется файл приложения FFmpeg, содержащий метод аппаратного ускорения OpenCL.", "OptionAllowContentDownloadHelp": "Пользователи могут загружать медиафайлы и хранить их на своих устройствах. Это не то же самое, как функция синхронизации. Для правильной работы книжных медиатек это необходимо.", "HeaderDeleteDevices": "Удалить все устройства", @@ -1742,7 +1742,7 @@ "LogLevel.Critical": "Критично", "LogLevel.None": "Ничего", "HeaderEpisodesStatus": "Статус эпизодов", - "LabelEnableLUFSScanHelp": "Включите сканирование LUFS для музыки (это займет больше времени и ресурсов).", + "LabelEnableLUFSScanHelp": "Клиенты могут нормализировать воспроизведения звука, чтобы обеспечить одинаковую громкость в разных треках. Сканированния библеотек займет больше времени и ресурсов.", "LabelLevel": "Уровень", "LogLevel.Debug": "Отладка", "MessageRepositoryInstallDisclaimer": "ПРЕДУПРЕЖДЕНИЕ: Установка стороннего репозитория плагинов несет определенные риски. Он может содержать нестабильный или вредоносный код и может изменяться в любое время. Устанавливайте репозитории только от авторов, которым вы доверяете.", @@ -1764,7 +1764,17 @@ "LogLevel.Error": "Ошибка", "LabelBackdropScreensaverInterval": "Интервал между фонами у заставки", "LabelBackdropScreensaverIntervalHelp": "Время в секундах между разными фонами, когда используется заставка.", - "BackdropScreensaver": "Заставка", + "BackdropScreensaver": "Фоновая заставка", "GoHome": "Домой", - "GridView": "Сеткой" + "GridView": "Отображение сеткой", + "LabelIsHearingImpaired": "Для людей со слабым слухом (SDH)", + "AllowAv1Encoding": "Разрешить кодирование AV1 формата", + "LogoScreensaver": "Логотип заставки", + "HeaderGuestCast": "Приглашенные звезды", + "UnknownError": "Возникла не известная ошибка.", + "ListView": "Отображение списком", + "AiTranslated": "Переведено при помощи ИИ", + "MachineTranslated": "Машинный перевод", + "HearingImpairedShort": "HI/SDH", + "ForeignPartsOnly": "Только для принудительных и иностранных частей" } From f173cf447d4d18df839be52e9c379ff985cca01a Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 10 Oct 2023 16:45:59 -0400 Subject: [PATCH 69/70] Add current branch to CF Pages publish job --- .github/workflows/publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 96eaef368e..c0e690e737 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -33,6 +33,7 @@ jobs: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} projectName: jellyfin-web + branch: ${{ github.event.workflow_run.head_branch }} directory: dist gitHubToken: ${{ secrets.GITHUB_TOKEN }} From cd7e23ed593224666b49b2010fb1132773a7af5c Mon Sep 17 00:00:00 2001 From: Guillermo Fernandez Date: Mon, 2 Oct 2023 20:17:23 +0200 Subject: [PATCH 70/70] Clean backdrops when tab is not selected --- src/components/backdrop/backdrop.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/backdrop/backdrop.js b/src/components/backdrop/backdrop.js index 46655777ff..9e97843294 100644 --- a/src/components/backdrop/backdrop.js +++ b/src/components/backdrop/backdrop.js @@ -37,8 +37,11 @@ class Backdrop { parent.appendChild(backdropImage); if (!enableAnimation()) { - if (existingBackdropImage?.parentNode) { - existingBackdropImage.parentNode.removeChild(existingBackdropImage); + if (existingBackdropImage) { + const backdropContainer = getBackdropContainer(); + while (backdropContainer.childNodes.length > 1) { + backdropContainer.removeChild(backdropContainer.firstChild); + } } internalBackdrop(true); return; @@ -51,8 +54,11 @@ class Backdrop { if (backdropImage === self.currentAnimatingElement) { self.currentAnimatingElement = null; } - if (existingBackdropImage?.parentNode) { - existingBackdropImage.parentNode.removeChild(existingBackdropImage); + if (existingBackdropImage) { + const backdropContainer = getBackdropContainer(); + while (backdropContainer.childNodes.length > 1) { + backdropContainer.removeChild(backdropContainer.firstChild); + } } };