Merge branch 'master' into libbitsub

This commit is contained in:
Neptune
2026-08-01 23:05:28 +07:00
committed by GitHub
198 changed files with 4963 additions and 2729 deletions
+4 -4
View File
@@ -20,21 +20,21 @@ jobs:
steps: steps:
- name: Checkout repository ⬇️ - name: Checkout repository ⬇️
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
ref: ${{ inputs.commit }} ref: ${{ inputs.commit }}
show-progress: false show-progress: false
- name: Initialize CodeQL 🛠️ - name: Initialize CodeQL 🛠️
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 uses: github/codeql-action/init@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
with: with:
queries: security-and-quality queries: security-and-quality
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: Autobuild 📦 - name: Autobuild 📦
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 uses: github/codeql-action/autobuild@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
- name: Perform CodeQL Analysis 🧪 - name: Perform CodeQL Analysis 🧪
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 uses: github/codeql-action/analyze@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'
+1 -1
View File
@@ -62,7 +62,7 @@ jobs:
github-token: ${{ github.token }} github-token: ${{ github.token }}
- name: Publish to Cloudflare Pages 📃 - name: Publish to Cloudflare Pages 📃
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0 uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
id: cf id: cf
with: with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+2 -2
View File
@@ -14,12 +14,12 @@ jobs:
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
ref: ${{ inputs.commit || github.sha }} ref: ${{ inputs.commit || github.sha }}
- name: Setup node environment - name: Setup node environment
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with: with:
node-version-file: .nvmrc node-version-file: .nvmrc
cache: npm cache: npm
+4 -4
View File
@@ -14,13 +14,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
ref: ${{ inputs.commit }} ref: ${{ inputs.commit }}
show-progress: false show-progress: false
- name: Scan - name: Scan
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with: with:
## Workaround from https://github.com/actions/dependency-review-action/issues/456 ## Workaround from https://github.com/actions/dependency-review-action/issues/456
## TODO: Remove when necessary ## TODO: Remove when necessary
@@ -42,13 +42,13 @@ jobs:
steps: steps:
- name: Checkout ⬇️ - name: Checkout ⬇️
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
ref: ${{ inputs.commit }} ref: ${{ inputs.commit }}
show-progress: false show-progress: false
- name: Setup node environment ⚙️ - name: Setup node environment ⚙️
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with: with:
node-version-file: .nvmrc node-version-file: .nvmrc
cache: npm cache: npm
+2 -2
View File
@@ -46,12 +46,12 @@ jobs:
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Setup node environment - name: Setup node environment
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with: with:
node-version-file: .nvmrc node-version-file: .nvmrc
cache: npm cache: npm
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }} if: ${{ contains(github.repository, 'jellyfin/') }}
steps: steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
with: with:
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
operations-per-run: 75 operations-per-run: 75
+1
View File
@@ -108,6 +108,7 @@
- [lmaotrigine](https://github.com/lmaotrigine) - [lmaotrigine](https://github.com/lmaotrigine)
- [bjorntp](https://github.com/bjorntp) - [bjorntp](https://github.com/bjorntp)
- [austinhardaway](https://github.com/austinhardaway) - [austinhardaway](https://github.com/austinhardaway)
- [Alex Dickens](https://github.com/alex-dicko)
- [shindouj](https://github.com/shindouj) - [shindouj](https://github.com/shindouj)
## Emby Contributors ## Emby Contributors
+421 -552
View File
File diff suppressed because it is too large Load Diff
+27 -27
View File
@@ -10,7 +10,7 @@
"@babel/preset-env": "7.29.7", "@babel/preset-env": "7.29.7",
"@babel/preset-react": "7.29.7", "@babel/preset-react": "7.29.7",
"@eslint-community/eslint-plugin-eslint-comments": "4.7.2", "@eslint-community/eslint-plugin-eslint-comments": "4.7.2",
"@eslint/js": "9.39.4", "@eslint/js": "9.39.5",
"@stylistic/eslint-plugin": "4.4.1", "@stylistic/eslint-plugin": "4.4.1",
"@stylistic/stylelint-plugin": "3.1.3", "@stylistic/stylelint-plugin": "3.1.3",
"@types/dompurify": "3.0.5", "@types/dompurify": "3.0.5",
@@ -22,12 +22,12 @@
"@types/react-dom": "18.3.7", "@types/react-dom": "18.3.7",
"@types/react-lazy-load-image-component": "1.6.5", "@types/react-lazy-load-image-component": "1.6.5",
"@types/sortablejs": "1.15.9", "@types/sortablejs": "1.15.9",
"@typescript-eslint/parser": "8.62.0", "@typescript-eslint/parser": "8.65.0",
"@uupaa/dynamic-import-polyfill": "1.0.2", "@uupaa/dynamic-import-polyfill": "1.0.2",
"@vitest/coverage-v8": "3.2.6", "@vitest/coverage-v8": "3.2.7",
"autoprefixer": "10.5.2", "autoprefixer": "10.5.4",
"babel-loader": "10.1.1", "babel-loader": "10.1.1",
"caniuse-lite": "1.0.30001799", "caniuse-lite": "1.0.30001806",
"clean-webpack-plugin": "4.0.0", "clean-webpack-plugin": "4.0.0",
"confusing-browser-globals": "1.0.11", "confusing-browser-globals": "1.0.11",
"copy-webpack-plugin": "14.0.0", "copy-webpack-plugin": "14.0.0",
@@ -35,7 +35,7 @@
"css-loader": "7.1.4", "css-loader": "7.1.4",
"cssnano": "7.1.9", "cssnano": "7.1.9",
"es-check": "9.6.4", "es-check": "9.6.4",
"eslint": "9.39.4", "eslint": "9.39.5",
"eslint-plugin-compat": "6.2.1", "eslint-plugin-compat": "6.2.1",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",
"eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-jsx-a11y": "6.10.2",
@@ -47,14 +47,14 @@
"fork-ts-checker-webpack-plugin": "9.1.0", "fork-ts-checker-webpack-plugin": "9.1.0",
"globals": "17.7.0", "globals": "17.7.0",
"html-loader": "5.1.0", "html-loader": "5.1.0",
"html-webpack-plugin": "5.6.7", "html-webpack-plugin": "5.6.8",
"jsdom": "29.1.1", "jsdom": "29.1.1",
"mini-css-extract-plugin": "2.10.2", "mini-css-extract-plugin": "2.10.2",
"postcss": "8.5.15", "postcss": "8.5.23",
"postcss-loader": "8.2.1", "postcss-loader": "8.2.1",
"postcss-preset-env": "10.6.1", "postcss-preset-env": "10.6.1",
"postcss-scss": "4.0.9", "postcss-scss": "4.0.9",
"sass": "1.101.0", "sass": "1.102.0",
"sass-loader": "16.0.8", "sass-loader": "16.0.8",
"source-map-loader": "5.0.0", "source-map-loader": "5.0.0",
"speed-measure-webpack-plugin": "1.6.0", "speed-measure-webpack-plugin": "1.6.0",
@@ -66,35 +66,35 @@
"stylelint-scss": "6.14.0", "stylelint-scss": "6.14.0",
"ts-loader": "9.6.2", "ts-loader": "9.6.2",
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.62.0", "typescript-eslint": "8.65.0",
"vite-tsconfig-paths": "6.1.1", "vite-tsconfig-paths": "6.1.1",
"vitest": "3.2.6", "vitest": "3.2.7",
"webpack": "5.107.2", "webpack": "5.109.0",
"webpack-bundle-analyzer": "5.3.0", "webpack-bundle-analyzer": "5.3.1",
"webpack-cli": "7.0.3", "webpack-cli": "7.2.1",
"webpack-dev-server": "5.2.5", "webpack-dev-server": "5.2.6",
"webpack-merge": "6.0.1", "webpack-merge": "6.0.1",
"worker-loader": "3.0.8" "worker-loader": "3.0.8"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "11.14.0", "@emotion/react": "11.14.0",
"@emotion/styled": "11.14.1", "@emotion/styled": "11.14.1",
"@fontsource/noto-sans": "5.2.10", "@fontsource/noto-sans": "5.3.0",
"@fontsource/noto-sans-hk": "5.2.9", "@fontsource/noto-sans-hk": "5.3.0",
"@fontsource/noto-sans-jp": "5.2.9", "@fontsource/noto-sans-jp": "5.3.0",
"@fontsource/noto-sans-kr": "5.2.9", "@fontsource/noto-sans-kr": "5.3.0",
"@fontsource/noto-sans-sc": "5.2.9", "@fontsource/noto-sans-sc": "5.3.0",
"@fontsource/noto-sans-tc": "5.2.9", "@fontsource/noto-sans-tc": "5.3.0",
"@jellyfin/libass-wasm": "4.2.4", "@jellyfin/libass-wasm": "4.2.4",
"@jellyfin/sdk": "0.0.0-unstable.202607010629", "@jellyfin/sdk": "0.0.0-unstable.202607220710",
"@jellyfin/ux-web": "1.0.0", "@jellyfin/ux-web": "1.0.0",
"@mui/icons-material": "6.5.0", "@mui/icons-material": "6.5.0",
"@mui/material": "6.5.0", "@mui/material": "6.5.0",
"@mui/x-date-pickers": "7.29.4", "@mui/x-date-pickers": "7.29.4",
"@react-hook/resize-observer": "2.0.2", "@react-hook/resize-observer": "2.0.2",
"@tanstack/react-query": "5.101.1", "@tanstack/react-query": "5.91.3",
"@tanstack/react-query-devtools": "5.101.1", "@tanstack/react-query-devtools": "5.91.3",
"@tanstack/react-query-persist-client": "5.101.1", "@tanstack/react-query-persist-client": "5.90.27",
"abortcontroller-polyfill": "1.7.8", "abortcontroller-polyfill": "1.7.8",
"axios": "1.18.1", "axios": "1.18.1",
"blurhash": "2.0.5", "blurhash": "2.0.5",
@@ -111,7 +111,7 @@
"headroom.js": "0.12.0", "headroom.js": "0.12.0",
"history": "5.3.0", "history": "5.3.0",
"hls.js": "1.6.16", "hls.js": "1.6.16",
"idb-keyval": "6.2.5", "idb-keyval": "6.3.0",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"jellyfin-apiclient": "1.11.0", "jellyfin-apiclient": "1.11.0",
"jquery": "3.7.1", "jquery": "3.7.1",
@@ -119,7 +119,7 @@
"libarchive.js": "2.0.2", "libarchive.js": "2.0.2",
"libbitsub": "1.11.0", "libbitsub": "1.11.0",
"lodash-es": "4.18.1", "lodash-es": "4.18.1",
"markdown-it": "14.2.0", "markdown-it": "14.3.0",
"material-design-icons-iconfont": "6.7.0", "material-design-icons-iconfont": "6.7.0",
"material-react-table": "3.2.1", "material-react-table": "3.2.1",
"native-promise-only": "0.8.1", "native-promise-only": "0.8.1",
+1 -1
View File
@@ -59,7 +59,6 @@ export const Component: FC = () => {
<StrictMode> <StrictMode>
<OffsetAppBar <OffsetAppBar
dense dense
elevation={4}
sx={{ sx={{
width: { width: {
xs: '100%', xs: '100%',
@@ -79,6 +78,7 @@ export const Component: FC = () => {
buttons={ buttons={
<HelpButton /> <HelpButton />
} }
className='dashboard-appBar'
> >
{isMetadataManager && ( {isMetadataManager && (
<ServerButton /> <ServerButton />
@@ -1,6 +1,4 @@
import React from 'react'; import { getDisplayVersion } from '@jellyfin/sdk/lib/utils/versioning';
import globalize from 'lib/globalize';
import Widget from './Widget';
import Paper from '@mui/material/Paper'; import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
@@ -9,8 +7,12 @@ import Skeleton from '@mui/material/Skeleton';
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import RestartAltIcon from '@mui/icons-material/RestartAlt'; import RestartAltIcon from '@mui/icons-material/RestartAlt';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import React from 'react';
import { useSystemInfo } from 'hooks/useSystemInfo'; import { useSystemInfo } from 'hooks/useSystemInfo';
import { getDisplayVersion } from 'utils/versions'; import globalize from 'lib/globalize';
import Widget from './Widget';
type ServerInfoWidgetProps = { type ServerInfoWidgetProps = {
onScanLibrariesClick?: () => void; onScanLibrariesClick?: () => void;
@@ -1,17 +1,17 @@
import type { ActivityLogApiGetLogEntriesRequest } from '@jellyfin/sdk/lib/generated-client/api/activity-log-api'; import type { SystemApiGetLogEntriesRequest } from '@jellyfin/sdk/lib/generated-client/api/system-api';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
import type { Api } from '@jellyfin/sdk'; import type { Api } from '@jellyfin/sdk';
import { getActivityLogApi } from '@jellyfin/sdk/lib/utils/api/activity-log-api'; import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
const fetchLogEntries = async ( const fetchLogEntries = async (
api: Api, api: Api,
requestParams?: ActivityLogApiGetLogEntriesRequest, requestParams?: SystemApiGetLogEntriesRequest,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getActivityLogApi(api).getLogEntries(requestParams, { const response = await getSystemApi(api).getLogEntries(requestParams, {
signal: options?.signal signal: options?.signal
}); });
@@ -19,7 +19,7 @@ const fetchLogEntries = async (
}; };
export const useLogEntries = ( export const useLogEntries = (
requestParams: ActivityLogApiGetLogEntriesRequest requestParams: SystemApiGetLogEntriesRequest
) => { ) => {
const { api } = useApi(); const { api } = useApi();
return useQuery({ return useQuery({
@@ -26,7 +26,7 @@ const RestoreConfirmationDialog: FunctionComponent<IProps> = ({ open, onClose, o
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText sx={{ whiteSpace: 'pre-line' }}>
{globalize.translate('MessageRestoreDisclaimer')} {globalize.translate('MessageRestoreDisclaimer')}
</DialogContentText> </DialogContentText>
</DialogContent> </DialogContent>
@@ -1,5 +1,5 @@
import type { DevicesApiDeleteDeviceRequest } from '@jellyfin/sdk/lib/generated-client/api/devices-api'; import type { DeviceApiDeleteDeviceRequest } from '@jellyfin/sdk/lib/generated-client/api/device-api';
import { getDevicesApi } from '@jellyfin/sdk/lib/utils/api/devices-api'; import { getDeviceApi } from '@jellyfin/sdk/lib/utils/api/device-api';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
@@ -10,8 +10,8 @@ export const useDeleteDevice = () => {
const { api } = useApi(); const { api } = useApi();
return useMutation({ return useMutation({
mutationFn: (params: DevicesApiDeleteDeviceRequest) => ( mutationFn: (params: DeviceApiDeleteDeviceRequest) => (
getDevicesApi(api!) getDeviceApi(api!)
.deleteDevice(params) .deleteDevice(params)
), ),
onSuccess: () => { onSuccess: () => {
@@ -1,7 +1,7 @@
import type { DevicesApiGetDevicesRequest } from '@jellyfin/sdk/lib/generated-client/api/devices-api'; import type { DeviceApiGetDevicesRequest } from '@jellyfin/sdk/lib/generated-client/api/device-api';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
import type { Api } from '@jellyfin/sdk'; import type { Api } from '@jellyfin/sdk';
import { getDevicesApi } from '@jellyfin/sdk/lib/utils/api/devices-api'; import { getDeviceApi } from '@jellyfin/sdk/lib/utils/api/device-api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
@@ -10,10 +10,10 @@ export const QUERY_KEY = 'Devices';
const fetchDevices = async ( const fetchDevices = async (
api: Api, api: Api,
requestParams?: DevicesApiGetDevicesRequest, requestParams?: DeviceApiGetDevicesRequest,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getDevicesApi(api).getDevices(requestParams, { const response = await getDeviceApi(api).getDevices(requestParams, {
signal: options?.signal signal: options?.signal
}); });
@@ -21,7 +21,7 @@ const fetchDevices = async (
}; };
export const useDevices = ( export const useDevices = (
requestParams: DevicesApiGetDevicesRequest requestParams: DeviceApiGetDevicesRequest
) => { ) => {
const { api } = useApi(); const { api } = useApi();
return useQuery({ return useQuery({
@@ -1,5 +1,5 @@
import type { DevicesApiUpdateDeviceOptionsRequest } from '@jellyfin/sdk/lib/generated-client/api/devices-api'; import type { DeviceApiUpdateDeviceOptionsRequest } from '@jellyfin/sdk/lib/generated-client/api/device-api';
import { getDevicesApi } from '@jellyfin/sdk/lib/utils/api/devices-api'; import { getDeviceApi } from '@jellyfin/sdk/lib/utils/api/device-api';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
@@ -10,8 +10,8 @@ export const useUpdateDevice = () => {
const { api } = useApi(); const { api } = useApi();
return useMutation({ return useMutation({
mutationFn: (params: DevicesApiUpdateDeviceOptionsRequest) => ( mutationFn: (params: DeviceApiUpdateDeviceOptionsRequest) => (
getDevicesApi(api!) getDeviceApi(api!)
.updateDeviceOptions(params) .updateDeviceOptions(params)
), ),
onSuccess: () => { onSuccess: () => {
@@ -1,5 +1,5 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import type { SessionInfo } from '@jellyfin/sdk/lib/generated-client/models/session-info'; import type { SessionInfoDto } from '@jellyfin/sdk/lib/generated-client/models/session-info-dto';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
import CardMedia from '@mui/material/CardMedia'; import CardMedia from '@mui/material/CardMedia';
@@ -27,7 +27,7 @@ import InputDialog from 'components/InputDialog';
import { useSendMessage } from '../../sessions/api/useSendMessage'; import { useSendMessage } from '../../sessions/api/useSendMessage';
type DeviceCardProps = { type DeviceCardProps = {
device: SessionInfo; device: SessionInfoDto;
}; };
const DeviceCard = ({ device }: DeviceCardProps) => { const DeviceCard = ({ device }: DeviceCardProps) => {
@@ -1,5 +1,5 @@
import { Api } from '@jellyfin/sdk'; import { Api } from '@jellyfin/sdk';
import { getApiKeyApi } from '@jellyfin/sdk/lib/utils/api/api-key-api'; import { getAuthenticationApi } from '@jellyfin/sdk/lib/utils/api/authentication-api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { AxiosRequestConfig } from 'axios'; import { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
@@ -7,7 +7,7 @@ import { useApi } from 'hooks/useApi';
export const QUERY_KEY = 'ApiKeys'; export const QUERY_KEY = 'ApiKeys';
const fetchApiKeys = async (api: Api, options?: AxiosRequestConfig) => { const fetchApiKeys = async (api: Api, options?: AxiosRequestConfig) => {
const response = await getApiKeyApi(api).getKeys(options); const response = await getAuthenticationApi(api).getKeys(options);
return response.data; return response.data;
}; };
@@ -1,5 +1,5 @@
import { ApiKeyApiCreateKeyRequest } from '@jellyfin/sdk/lib/generated-client/api/api-key-api'; import { AuthenticationApiCreateKeyRequest } from '@jellyfin/sdk/lib/generated-client/api/authentication-api';
import { getApiKeyApi } from '@jellyfin/sdk/lib/utils/api/api-key-api'; import { getAuthenticationApi } from '@jellyfin/sdk/lib/utils/api/authentication-api';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient'; import { queryClient } from 'utils/query/queryClient';
@@ -9,8 +9,8 @@ export const useCreateKey = () => {
const { api } = useApi(); const { api } = useApi();
return useMutation({ return useMutation({
mutationFn: (params: ApiKeyApiCreateKeyRequest) => ( mutationFn: (params: AuthenticationApiCreateKeyRequest) => (
getApiKeyApi(api!) getAuthenticationApi(api!)
.createKey(params) .createKey(params)
), ),
onSuccess: () => { onSuccess: () => {
@@ -1,5 +1,5 @@
import type { ApiKeyApiRevokeKeyRequest } from '@jellyfin/sdk/lib/generated-client/api/api-key-api'; import type { AuthenticationApiRevokeKeyRequest } from '@jellyfin/sdk/lib/generated-client/api/authentication-api';
import { getApiKeyApi } from '@jellyfin/sdk/lib/utils/api/api-key-api'; import { getAuthenticationApi } from '@jellyfin/sdk/lib/utils/api/authentication-api';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient'; import { queryClient } from 'utils/query/queryClient';
@@ -9,8 +9,8 @@ export const useRevokeKey = () => {
const { api } = useApi(); const { api } = useApi();
return useMutation({ return useMutation({
mutationFn: (params: ApiKeyApiRevokeKeyRequest) => ( mutationFn: (params: AuthenticationApiRevokeKeyRequest) => (
getApiKeyApi(api!) getAuthenticationApi(api!)
.revokeKey(params) .revokeKey(params)
), ),
onSuccess: () => { onSuccess: () => {
@@ -27,7 +27,12 @@ const LogItemList: FunctionComponent<LogItemProps> = ({ logs }: LogItemProps) =>
secondary={getDate(log)} secondary={getDate(log)}
slotProps={{ slotProps={{
primary: { primary: {
variant: 'h3' variant: 'h3',
sx: {
whiteSpace: 'normal',
overflowWrap: 'anywhere',
wordBreak: 'break-word'
}
}, },
secondary: { secondary: {
variant: 'body1' variant: 'body1'
@@ -1,6 +1,6 @@
import type { Api } from '@jellyfin/sdk'; import type { Api } from '@jellyfin/sdk';
import type { DashboardApiGetConfigurationPagesRequest } from '@jellyfin/sdk/lib/generated-client/api/dashboard-api'; import type { PluginApiGetConfigurationPagesRequest } from '@jellyfin/sdk/lib/generated-client/api/plugin-api';
import { getDashboardApi } from '@jellyfin/sdk/lib/utils/api/dashboard-api'; import { getPluginApi } from '@jellyfin/sdk/lib/utils/api/plugin-api';
import { queryOptions, useQuery } from '@tanstack/react-query'; import { queryOptions, useQuery } from '@tanstack/react-query';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
@@ -10,17 +10,17 @@ import { QueryKey } from './queryKey';
const fetchConfigurationPages = async ( const fetchConfigurationPages = async (
api: Api, api: Api,
params?: DashboardApiGetConfigurationPagesRequest, params?: PluginApiGetConfigurationPagesRequest,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getDashboardApi(api) const response = await getPluginApi(api)
.getConfigurationPages(params, options); .getConfigurationPages(params, options);
return response.data; return response.data;
}; };
const getConfigurationPagesQuery = ( const getConfigurationPagesQuery = (
api?: Api, api?: Api,
params?: DashboardApiGetConfigurationPagesRequest params?: PluginApiGetConfigurationPagesRequest
) => queryOptions({ ) => queryOptions({
queryKey: [ QueryKey.ConfigurationPages, params?.enableInMainMenu ], queryKey: [ QueryKey.ConfigurationPages, params?.enableInMainMenu ],
queryFn: ({ signal }) => fetchConfigurationPages(api!, params, { signal }), queryFn: ({ signal }) => fetchConfigurationPages(api!, params, { signal }),
@@ -28,7 +28,7 @@ const getConfigurationPagesQuery = (
}); });
export const useConfigurationPages = ( export const useConfigurationPages = (
params?: DashboardApiGetConfigurationPagesRequest params?: PluginApiGetConfigurationPagesRequest
) => { ) => {
const { api } = useApi(); const { api } = useApi();
return useQuery(getConfigurationPagesQuery(api, params)); return useQuery(getConfigurationPagesQuery(api, params));
@@ -1,5 +1,5 @@
import type { PluginsApiDisablePluginRequest } from '@jellyfin/sdk/lib/generated-client/api/plugins-api'; import type { PluginApiDisablePluginRequest } from '@jellyfin/sdk/lib/generated-client/api/plugin-api';
import { getPluginsApi } from '@jellyfin/sdk/lib/utils/api/plugins-api'; import { getPluginApi } from '@jellyfin/sdk/lib/utils/api/plugin-api';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
@@ -10,8 +10,8 @@ import { QueryKey } from './queryKey';
export const useDisablePlugin = () => { export const useDisablePlugin = () => {
const { api } = useApi(); const { api } = useApi();
return useMutation({ return useMutation({
mutationFn: (params: PluginsApiDisablePluginRequest) => ( mutationFn: (params: PluginApiDisablePluginRequest) => (
getPluginsApi(api!) getPluginApi(api!)
.disablePlugin(params) .disablePlugin(params)
), ),
onSuccess: () => { onSuccess: () => {
@@ -1,5 +1,5 @@
import type { PluginsApiEnablePluginRequest } from '@jellyfin/sdk/lib/generated-client/api/plugins-api'; import type { PluginApiEnablePluginRequest } from '@jellyfin/sdk/lib/generated-client/api/plugin-api';
import { getPluginsApi } from '@jellyfin/sdk/lib/utils/api/plugins-api'; import { getPluginApi } from '@jellyfin/sdk/lib/utils/api/plugin-api';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
@@ -10,8 +10,8 @@ import { QueryKey } from './queryKey';
export const useEnablePlugin = () => { export const useEnablePlugin = () => {
const { api } = useApi(); const { api } = useApi();
return useMutation({ return useMutation({
mutationFn: (params: PluginsApiEnablePluginRequest) => ( mutationFn: (params: PluginApiEnablePluginRequest) => (
getPluginsApi(api!) getPluginApi(api!)
.enablePlugin(params) .enablePlugin(params)
), ),
onSuccess: () => { onSuccess: () => {
@@ -1,5 +1,5 @@
import type { PackageApiInstallPackageRequest } from '@jellyfin/sdk/lib/generated-client/api/package-api'; import type { PluginApiInstallPackageRequest } from '@jellyfin/sdk/lib/generated-client/api/plugin-api';
import { getPackageApi } from '@jellyfin/sdk/lib/utils/api/package-api'; import { getPluginApi } from '@jellyfin/sdk/lib/utils/api/plugin-api';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
@@ -10,8 +10,8 @@ import { QueryKey } from './queryKey';
export const useInstallPackage = () => { export const useInstallPackage = () => {
const { api } = useApi(); const { api } = useApi();
return useMutation({ return useMutation({
mutationFn: (params: PackageApiInstallPackageRequest) => ( mutationFn: (params: PluginApiInstallPackageRequest) => (
getPackageApi(api!) getPluginApi(api!)
.installPackage(params) .installPackage(params)
), ),
onSuccess: () => { onSuccess: () => {
@@ -1,7 +1,7 @@
import { queryOptions, useQuery } from '@tanstack/react-query'; import { queryOptions, useQuery } from '@tanstack/react-query';
import type { Api } from '@jellyfin/sdk'; import type { Api } from '@jellyfin/sdk';
import type { PackageApiGetPackageInfoRequest } from '@jellyfin/sdk/lib/generated-client/api/package-api'; import type { PluginApiGetPackageInfoRequest } from '@jellyfin/sdk/lib/generated-client/api/plugin-api';
import { getPackageApi } from '@jellyfin/sdk/lib/utils/api/package-api'; import { getPluginApi } from '@jellyfin/sdk/lib/utils/api/plugin-api';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
@@ -12,7 +12,7 @@ import type { PackageInfo } from '@jellyfin/sdk/lib/generated-client/models/pack
const fetchPackageInfo = async ( const fetchPackageInfo = async (
api: Api, api: Api,
params: PackageApiGetPackageInfoRequest, params: PluginApiGetPackageInfoRequest,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const packagesData = queryClient.getQueryData([ QueryKey.Packages ]) as PackageInfo[]; const packagesData = queryClient.getQueryData([ QueryKey.Packages ]) as PackageInfo[];
@@ -25,14 +25,14 @@ const fetchPackageInfo = async (
} }
} }
const response = await getPackageApi(api) const response = await getPluginApi(api)
.getPackageInfo(params, options); .getPackageInfo(params, options);
return response.data; return response.data;
}; };
const getPackageInfoQuery = ( const getPackageInfoQuery = (
api: Api | undefined, api: Api | undefined,
params?: PackageApiGetPackageInfoRequest params?: PluginApiGetPackageInfoRequest
) => queryOptions({ ) => queryOptions({
// Don't retry since requests for plugins not available in repos fail // Don't retry since requests for plugins not available in repos fail
retry: false, retry: false,
@@ -42,7 +42,7 @@ const getPackageInfoQuery = (
}); });
export const usePackageInfo = ( export const usePackageInfo = (
params?: PackageApiGetPackageInfoRequest params?: PluginApiGetPackageInfoRequest
) => { ) => {
const { api } = useApi(); const { api } = useApi();
return useQuery(getPackageInfoQuery(api, params)); return useQuery(getPackageInfoQuery(api, params));
@@ -1,5 +1,5 @@
import type { Api } from '@jellyfin/sdk'; import type { Api } from '@jellyfin/sdk';
import { getPackageApi } from '@jellyfin/sdk/lib/utils/api/package-api'; import { getPluginApi } from '@jellyfin/sdk/lib/utils/api/plugin-api';
import { queryOptions, useQuery } from '@tanstack/react-query'; import { queryOptions, useQuery } from '@tanstack/react-query';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
@@ -11,7 +11,7 @@ const fetchPackages = async (
api: Api, api: Api,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getPackageApi(api) const response = await getPluginApi(api)
.getPackages(options); .getPackages(options);
return response.data; return response.data;
}; };
@@ -29,4 +29,3 @@ export const usePackages = () => {
const { api } = useApi(); const { api } = useApi();
return useQuery(getPackagesQuery(api)); return useQuery(getPackagesQuery(api));
}; };
@@ -1,5 +1,5 @@
import type { Api } from '@jellyfin/sdk'; import type { Api } from '@jellyfin/sdk';
import { getPluginsApi } from '@jellyfin/sdk/lib/utils/api/plugins-api'; import { getPluginApi } from '@jellyfin/sdk/lib/utils/api/plugin-api';
import { queryOptions, useQuery } from '@tanstack/react-query'; import { queryOptions, useQuery } from '@tanstack/react-query';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
@@ -11,8 +11,7 @@ const fetchPlugins = async (
api: Api, api: Api,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getPluginsApi(api) const response = await getPluginApi(api).getPlugins(options);
.getPlugins(options);
return response.data; return response.data;
}; };
@@ -28,4 +27,3 @@ export const usePlugins = () => {
const { api } = useApi(); const { api } = useApi();
return useQuery(getPluginsQuery(api)); return useQuery(getPluginsQuery(api));
}; };
@@ -3,13 +3,13 @@ import { queryOptions, useQuery } from '@tanstack/react-query';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { QueryKey } from './queryKey'; import { QueryKey } from './queryKey';
import { getPackageApi } from '@jellyfin/sdk/lib/utils/api/package-api'; import { getPluginApi } from '@jellyfin/sdk/lib/utils/api/plugin-api';
const fetchRepositories = async ( const fetchRepositories = async (
api: Api, api: Api,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getPackageApi(api) const response = await getPluginApi(api)
.getRepositories(options); .getRepositories(options);
return response.data; return response.data;
}; };
@@ -26,4 +26,3 @@ export const useRepositories = () => {
const { api } = useApi(); const { api } = useApi();
return useQuery(getRepositoriesQuery(api)); return useQuery(getRepositoriesQuery(api));
}; };
@@ -2,14 +2,14 @@ import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient'; import { queryClient } from 'utils/query/queryClient';
import { QueryKey } from './queryKey'; import { QueryKey } from './queryKey';
import { getPackageApi } from '@jellyfin/sdk/lib/utils/api/package-api'; import { getPluginApi } from '@jellyfin/sdk/lib/utils/api/plugin-api';
import { PackageApiSetRepositoriesRequest } from '@jellyfin/sdk/lib/generated-client/api/package-api'; import { PluginApiSetRepositoriesRequest } from '@jellyfin/sdk/lib/generated-client/api/plugin-api';
export const useSetRepositories = () => { export const useSetRepositories = () => {
const { api } = useApi(); const { api } = useApi();
return useMutation({ return useMutation({
mutationFn: (params: PackageApiSetRepositoriesRequest) => ( mutationFn: (params: PluginApiSetRepositoriesRequest) => (
getPackageApi(api!) getPluginApi(api!)
.setRepositories(params) .setRepositories(params)
), ),
onSuccess: () => { onSuccess: () => {
@@ -1,5 +1,5 @@
import type { PluginsApiUninstallPluginByVersionRequest } from '@jellyfin/sdk/lib/generated-client/api/plugins-api'; import type { PluginApiUninstallPluginByVersionRequest } from '@jellyfin/sdk/lib/generated-client/api/plugin-api';
import { getPluginsApi } from '@jellyfin/sdk/lib/utils/api/plugins-api'; import { getPluginApi } from '@jellyfin/sdk/lib/utils/api/plugin-api';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
@@ -10,8 +10,8 @@ import { QueryKey } from './queryKey';
export const useUninstallPlugin = () => { export const useUninstallPlugin = () => {
const { api } = useApi(); const { api } = useApi();
return useMutation({ return useMutation({
mutationFn: (params: PluginsApiUninstallPluginByVersionRequest) => ( mutationFn: (params: PluginApiUninstallPluginByVersionRequest) => (
getPluginsApi(api!) getPluginApi(api!)
.uninstallPluginByVersion(params) .uninstallPluginByVersion(params)
), ),
onSuccess: () => { onSuccess: () => {
@@ -1,4 +1,4 @@
import type { SessionInfo } from '@jellyfin/sdk/lib/generated-client/models/session-info'; import type { SessionInfoDto } from '@jellyfin/sdk/lib/generated-client/models/session-info-dto';
import itemHelper from 'components/itemHelper'; import itemHelper from 'components/itemHelper';
import formatDistanceToNow from 'date-fns/formatDistanceToNow'; import formatDistanceToNow from 'date-fns/formatDistanceToNow';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
@@ -11,7 +11,7 @@ type NowPlayingInfo = {
image?: string; image?: string;
}; };
const getNowPlayingName = (session: SessionInfo): NowPlayingInfo => { const getNowPlayingName = (session: SessionInfoDto): NowPlayingInfo => {
let imgUrl = ''; let imgUrl = '';
const nowPlayingItem = session.NowPlayingItem; const nowPlayingItem = session.NowPlayingItem;
// FIXME: It seems that, sometimes, server sends date in the future, so date-fns displays messages like 'in less than a minute'. We should fix // FIXME: It seems that, sometimes, server sends date in the future, so date-fns displays messages like 'in less than a minute'. We should fix
@@ -1,9 +1,9 @@
import type { SessionInfo } from '@jellyfin/sdk/lib/generated-client/models/session-info'; import type { SessionInfoDto } from '@jellyfin/sdk/lib/generated-client/models/session-info-dto';
import playmethodhelper from 'components/playback/playmethodhelper'; import playmethodhelper from 'components/playback/playmethodhelper';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
const getSessionNowPlayingStreamInfo = (session: SessionInfo): string => { const getSessionNowPlayingStreamInfo = (session: SessionInfoDto): string => {
let text = ''; let text = '';
let showTranscodingInfo = false; let showTranscodingInfo = false;
const displayPlayMethod = playmethodhelper.getDisplayPlayMethod(session); const displayPlayMethod = playmethodhelper.getDisplayPlayMethod(session);
@@ -1,7 +1,7 @@
import type { SessionInfo } from '@jellyfin/sdk/lib/generated-client/models/session-info'; import type { SessionInfoDto } from '@jellyfin/sdk/lib/generated-client/models/session-info-dto';
import datetime from 'scripts/datetime'; import datetime from 'scripts/datetime';
const getSessionNowPlayingTime = (session: SessionInfo) => { const getSessionNowPlayingTime = (session: SessionInfoDto) => {
const nowPlayingItem = session.NowPlayingItem; const nowPlayingItem = session.NowPlayingItem;
let start = '0:00'; let start = '0:00';
@@ -1,5 +1,5 @@
import { ScheduledTasksApiStartTaskRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-tasks-api'; import { ScheduledTaskApiStartTaskRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-task-api';
import { getScheduledTasksApi } from '@jellyfin/sdk/lib/utils/api/scheduled-tasks-api'; import { getScheduledTaskApi } from '@jellyfin/sdk/lib/utils/api/scheduled-task-api';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient'; import { queryClient } from 'utils/query/queryClient';
@@ -9,8 +9,8 @@ export const useStartTask = () => {
const { api } = useApi(); const { api } = useApi();
return useMutation({ return useMutation({
mutationFn: (params: ScheduledTasksApiStartTaskRequest) => ( mutationFn: (params: ScheduledTaskApiStartTaskRequest) => (
getScheduledTasksApi(api!) getScheduledTaskApi(api!)
.startTask(params) .startTask(params)
), ),
onSuccess: () => { onSuccess: () => {
@@ -1,5 +1,5 @@
import { ScheduledTasksApiStartTaskRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-tasks-api'; import { ScheduledTaskApiStartTaskRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-task-api';
import { getScheduledTasksApi } from '@jellyfin/sdk/lib/utils/api/scheduled-tasks-api'; import { getScheduledTaskApi } from '@jellyfin/sdk/lib/utils/api/scheduled-task-api';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient'; import { queryClient } from 'utils/query/queryClient';
@@ -9,8 +9,8 @@ export const useStopTask = () => {
const { api } = useApi(); const { api } = useApi();
return useMutation({ return useMutation({
mutationFn: (params: ScheduledTasksApiStartTaskRequest) => ( mutationFn: (params: ScheduledTaskApiStartTaskRequest) => (
getScheduledTasksApi(api!) getScheduledTaskApi(api!)
.stopTask(params) .stopTask(params)
), ),
onSuccess: () => { onSuccess: () => {
@@ -1,7 +1,7 @@
import type { ScheduledTasksApiGetTaskRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-tasks-api'; import type { ScheduledTaskApiGetTaskRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-task-api';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
import type { Api } from '@jellyfin/sdk'; import type { Api } from '@jellyfin/sdk';
import { getScheduledTasksApi } from '@jellyfin/sdk/lib/utils/api/scheduled-tasks-api'; import { getScheduledTaskApi } from '@jellyfin/sdk/lib/utils/api/scheduled-task-api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
@@ -9,15 +9,15 @@ import { QUERY_KEY } from './useTasks';
const fetchTask = async ( const fetchTask = async (
api: Api, api: Api,
params: ScheduledTasksApiGetTaskRequest, params: ScheduledTaskApiGetTaskRequest,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getScheduledTasksApi(api).getTask(params, options); const response = await getScheduledTaskApi(api).getTask(params, options);
return response.data; return response.data;
}; };
export const useTask = (params: ScheduledTasksApiGetTaskRequest) => { export const useTask = (params: ScheduledTaskApiGetTaskRequest) => {
const { api } = useApi(); const { api } = useApi();
return useQuery({ return useQuery({
@@ -1,7 +1,7 @@
import type { ScheduledTasksApiGetTasksRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-tasks-api'; import type { ScheduledTaskApiGetTasksRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-task-api';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
import type { Api } from '@jellyfin/sdk'; import type { Api } from '@jellyfin/sdk';
import { getScheduledTasksApi } from '@jellyfin/sdk/lib/utils/api/scheduled-tasks-api'; import { getScheduledTaskApi } from '@jellyfin/sdk/lib/utils/api/scheduled-task-api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
@@ -10,15 +10,15 @@ export const QUERY_KEY = 'Tasks';
const fetchTasks = async ( const fetchTasks = async (
api: Api, api: Api,
params?: ScheduledTasksApiGetTasksRequest, params?: ScheduledTaskApiGetTasksRequest,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getScheduledTasksApi(api).getTasks(params, options); const response = await getScheduledTaskApi(api).getTasks(params, options);
return response.data; return response.data;
}; };
export const useTasks = (params?: ScheduledTasksApiGetTasksRequest) => { export const useTasks = (params?: ScheduledTaskApiGetTasksRequest) => {
const { api } = useApi(); const { api } = useApi();
return useQuery({ return useQuery({
@@ -1,5 +1,5 @@
import { ScheduledTasksApiUpdateTaskRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-tasks-api'; import { ScheduledTaskApiUpdateTaskRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-task-api';
import { getScheduledTasksApi } from '@jellyfin/sdk/lib/utils/api/scheduled-tasks-api'; import { getScheduledTaskApi } from '@jellyfin/sdk/lib/utils/api/scheduled-task-api';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient'; import { queryClient } from 'utils/query/queryClient';
@@ -9,8 +9,8 @@ export const useUpdateTask = () => {
const { api } = useApi(); const { api } = useApi();
return useMutation({ return useMutation({
mutationFn: (params: ScheduledTasksApiUpdateTaskRequest) => ( mutationFn: (params: ScheduledTaskApiUpdateTaskRequest) => (
getScheduledTasksApi(api!) getScheduledTaskApi(api!)
.updateTask(params) .updateTask(params)
), ),
onSuccess: (_data, params) => { onSuccess: (_data, params) => {
@@ -1,11 +1,11 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { QUERY_KEY, useTasks } from '../api/useTasks'; import { QUERY_KEY, useTasks } from '../api/useTasks';
import type { ScheduledTasksApiGetTasksRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-tasks-api'; import type { ScheduledTaskApiGetTasksRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-task-api';
import { queryClient } from 'utils/query/queryClient'; import { queryClient } from 'utils/query/queryClient';
import { OutboundWebSocketMessageType } from '@jellyfin/sdk/lib/websocket'; import { OutboundWebSocketMessageType } from '@jellyfin/sdk/lib/websocket';
const useLiveTasks = (params: ScheduledTasksApiGetTasksRequest) => { const useLiveTasks = (params: ScheduledTaskApiGetTasksRequest) => {
const { api } = useApi(); const { api } = useApi();
const tasksQuery = useTasks(params); const tasksQuery = useTasks(params);
@@ -1,17 +1,17 @@
import { Api } from '@jellyfin/sdk'; import { Api } from '@jellyfin/sdk';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { getChannelsApi } from '@jellyfin/sdk/lib/utils/api/channels-api'; import { getChannelApi } from '@jellyfin/sdk/lib/utils/api/channel-api';
import { ChannelsApiGetChannelsRequest } from '@jellyfin/sdk/lib/generated-client/api/channels-api'; import { ChannelApiGetChannelsRequest } from '@jellyfin/sdk/lib/generated-client/api/channel-api';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
const fetchChannels = async (api: Api, params?: ChannelsApiGetChannelsRequest, options?: AxiosRequestConfig) => { const fetchChannels = async (api: Api, params?: ChannelApiGetChannelsRequest, options?: AxiosRequestConfig) => {
const response = await getChannelsApi(api).getChannels(params, options); const response = await getChannelApi(api).getChannels(params, options);
return response.data; return response.data;
}; };
export const useChannels = (params?: ChannelsApiGetChannelsRequest) => { export const useChannels = (params?: ChannelApiGetChannelsRequest) => {
const { api } = useApi(); const { api } = useApi();
return useQuery({ return useQuery({
@@ -1,12 +1,12 @@
import { Api } from '@jellyfin/sdk'; import { Api } from '@jellyfin/sdk';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
import type { NetworkConfiguration } from '@jellyfin/sdk/lib/generated-client/models/network-configuration'; import type { NetworkConfiguration } from '@jellyfin/sdk/lib/generated-client/models/network-configuration';
const fetchNetworkConfig = async (api: Api, options?: AxiosRequestConfig) => { const fetchNetworkConfig = async (api: Api, options?: AxiosRequestConfig) => {
const response = await getConfigurationApi(api).getNamedConfiguration({ key: 'network' }, options); const response = await getSystemApi(api).getNamedConfiguration({ key: 'network' }, options);
return response.data as NetworkConfiguration; return response.data as NetworkConfiguration;
}; };
@@ -17,6 +17,7 @@ import { useChannels } from 'apps/dashboard/features/users/api/useChannels';
import { useUpdateUser } from 'apps/dashboard/features/users/api/useUpdateUser'; import { useUpdateUser } from 'apps/dashboard/features/users/api/useUpdateUser';
import { useUpdateUserPolicy } from 'apps/dashboard/features/users/api/useUpdateUserPolicy'; import { useUpdateUserPolicy } from 'apps/dashboard/features/users/api/useUpdateUserPolicy';
import { useNetworkConfig } from 'apps/dashboard/features/users/api/useNetworkConfig'; import { useNetworkConfig } from 'apps/dashboard/features/users/api/useNetworkConfig';
import Toast from 'apps/dashboard/components/Toast';
interface ProfileProps { interface ProfileProps {
userDto: UserDto; userDto: UserDto;
@@ -45,6 +46,12 @@ const Profile = ({ userDto }: ProfileProps) => {
const { data: channels, isSuccess: isChannelsSuccess } = useChannels({ supportsMediaDeletion: true }); const { data: channels, isSuccess: isChannelsSuccess } = useChannels({ supportsMediaDeletion: true });
const { data: netConfig, isSuccess: isNetConfigSuccess } = useNetworkConfig(); const { data: netConfig, isSuccess: isNetConfigSuccess } = useNetworkConfig();
const [ isErrorToastOpen, setIsErrorToastOpen ] = useState(false);
const handleToastClose = useCallback(() => {
setIsErrorToastOpen(false);
}, []);
const updateUser = useUpdateUser(); const updateUser = useUpdateUser();
const updateUserPolicy = useUpdateUserPolicy(); const updateUserPolicy = useUpdateUserPolicy();
@@ -255,6 +262,10 @@ const Profile = ({ userDto }: ProfileProps) => {
} }
}); });
} }
},
onError: () => {
loading.hide();
setIsErrorToastOpen(true);
} }
}); });
}; };
@@ -306,6 +317,11 @@ const Profile = ({ userDto }: ProfileProps) => {
return ( return (
<div ref={element}> <div ref={element}>
<Toast
open={isErrorToastOpen}
onClose={handleToastClose}
message={globalize.translate('ErrorDefault')}
/>
<div <div
className='lnkEditUserPreferencesContainer' className='lnkEditUserPreferencesContainer'
style={{ paddingBottom: '1em' }} style={{ paddingBottom: '1em' }}
@@ -332,6 +348,9 @@ const Profile = ({ userDto }: ProfileProps) => {
label={globalize.translate('LabelName')} label={globalize.translate('LabelName')}
required required
/> />
<div className='fieldDescription'>
{globalize.translate('LabelUsernameAllowedCharactersHelp')}
</div>
</div> </div>
<div className='selectContainer fldSelectLoginProvider hide'> <div className='selectContainer fldSelectLoginProvider hide'>
<SelectElement <SelectElement
+5 -5
View File
@@ -1,5 +1,5 @@
import type { BrandingOptions } from '@jellyfin/sdk/lib/generated-client/models/branding-options'; import type { BrandingOptionsDto } from '@jellyfin/sdk/lib/generated-client/models/branding-options-dto';
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
import { getImageApi } from '@jellyfin/sdk/lib/utils/api/image-api'; import { getImageApi } from '@jellyfin/sdk/lib/utils/api/image-api';
import Delete from '@mui/icons-material/Delete'; import Delete from '@mui/icons-material/Delete';
import Upload from '@mui/icons-material/Upload'; import Upload from '@mui/icons-material/Upload';
@@ -39,13 +39,13 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData(); const formData = await request.formData();
const data = Object.fromEntries(formData); const data = Object.fromEntries(formData);
const brandingOptions: BrandingOptions = { const brandingOptions: BrandingOptionsDto = {
CustomCss: data.CustomCss?.toString(), CustomCss: data.CustomCss?.toString(),
LoginDisclaimer: data.LoginDisclaimer?.toString(), LoginDisclaimer: data.LoginDisclaimer?.toString(),
SplashscreenEnabled: data.SplashscreenEnabled?.toString() === 'on' SplashscreenEnabled: data.SplashscreenEnabled?.toString() === 'on'
}; };
await getConfigurationApi(api) await getSystemApi(api)
.updateNamedConfiguration({ .updateNamedConfiguration({
key: BRANDING_CONFIG_KEY, key: BRANDING_CONFIG_KEY,
body: JSON.stringify(brandingOptions) body: JSON.stringify(brandingOptions)
@@ -150,7 +150,7 @@ export const Component = () => {
const setSplashscreenEnabled = useCallback(async (_: React.ChangeEvent<HTMLInputElement>, isEnabled: boolean) => { const setSplashscreenEnabled = useCallback(async (_: React.ChangeEvent<HTMLInputElement>, isEnabled: boolean) => {
setIsSplashscreenEnabled(isEnabled); setIsSplashscreenEnabled(isEnabled);
await getConfigurationApi(api!) await getSystemApi(api!)
.updateNamedConfiguration({ .updateNamedConfiguration({
key: BRANDING_CONFIG_KEY, key: BRANDING_CONFIG_KEY,
body: JSON.stringify({ body: JSON.stringify({
+2 -2
View File
@@ -77,7 +77,7 @@ export const Component = () => {
const onConfirmDelete = useCallback(() => { const onConfirmDelete = useCallback(() => {
if (pendingDeleteDeviceId) { if (pendingDeleteDeviceId) {
deleteDevice.mutate({ deleteDevice.mutate({
id: pendingDeleteDeviceId id: [pendingDeleteDeviceId]
}, { }, {
onSettled: onCloseDeleteConfirmDialog onSettled: onCloseDeleteConfirmDialog
}); });
@@ -97,7 +97,7 @@ export const Component = () => {
Promise Promise
.all(devices.map(item => { .all(devices.map(item => {
if (api && item.Id && api.deviceInfo.id === item.Id) { if (api && item.Id && api.deviceInfo.id === item.Id) {
return deleteDevice.mutateAsync({ id: item.Id }); return deleteDevice.mutateAsync({ id: [item.Id] });
} }
return Promise.resolve(); return Promise.resolve();
})) }))
@@ -12,7 +12,7 @@ import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Loading from 'components/loading/LoadingComponent'; import Loading from 'components/loading/LoadingComponent';
import Page from 'components/Page'; import Page from 'components/Page';
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
import { QUERY_KEY as CONFIG_QUERY_KEY, useConfiguration } from 'hooks/useConfiguration'; import { QUERY_KEY as CONFIG_QUERY_KEY, useConfiguration } from 'hooks/useConfiguration';
import { QUERY_KEY as NAMED_CONFIG_QUERY_KEY, useNamedConfiguration } from 'hooks/useNamedConfiguration'; import { QUERY_KEY as NAMED_CONFIG_QUERY_KEY, useNamedConfiguration } from 'hooks/useNamedConfiguration';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
@@ -31,7 +31,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData(); const formData = await request.formData();
const data = Object.fromEntries(formData); const data = Object.fromEntries(formData);
const { data: config } = await getConfigurationApi(api).getConfiguration(); const { data: config } = await getSystemApi(api).getConfiguration();
const metadataConfig: MetadataConfiguration = { const metadataConfig: MetadataConfiguration = {
UseFileCreationTimeForDateAdded: data.DateAddedBehavior.toString() === '1' UseFileCreationTimeForDateAdded: data.DateAddedBehavior.toString() === '1'
@@ -43,10 +43,10 @@ export const action = async ({ request }: ActionFunctionArgs) => {
config.EnableGroupingShowsIntoCollections = data.GroupShowsIntoCollections?.toString() === 'on'; config.EnableGroupingShowsIntoCollections = data.GroupShowsIntoCollections?.toString() === 'on';
config.EnableExternalContentInSuggestions = data.EnableExternalContentInSuggestions?.toString() === 'on'; config.EnableExternalContentInSuggestions = data.EnableExternalContentInSuggestions?.toString() === 'on';
await getConfigurationApi(api) await getSystemApi(api)
.updateConfiguration({ serverConfiguration: config }); .updateConfiguration({ serverConfiguration: config });
await getConfigurationApi(api) await getSystemApi(api)
.updateNamedConfiguration({ key: CONFIG_KEY, body: metadataConfig }); .updateNamedConfiguration({ key: CONFIG_KEY, body: metadataConfig });
void queryClient.invalidateQueries({ void queryClient.invalidateQueries({
@@ -1,5 +1,5 @@
import { ImageResolution } from '@jellyfin/sdk/lib/generated-client/models/image-resolution'; import { ImageResolution } from '@jellyfin/sdk/lib/generated-client/models/image-resolution';
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
import Alert from '@mui/material/Alert'; import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
@@ -27,14 +27,14 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData(); const formData = await request.formData();
const data = Object.fromEntries(formData); const data = Object.fromEntries(formData);
const { data: config } = await getConfigurationApi(api).getConfiguration(); const { data: config } = await getSystemApi(api).getConfiguration();
config.PreferredMetadataLanguage = data.Language.toString(); config.PreferredMetadataLanguage = data.Language.toString();
config.MetadataCountryCode = data.Country.toString(); config.MetadataCountryCode = data.Country.toString();
config.DummyChapterDuration = parseInt(data.DummyChapterDuration.toString(), 10); config.DummyChapterDuration = parseInt(data.DummyChapterDuration.toString(), 10);
config.ChapterImageResolution = data.ChapterImageResolution.toString() as ImageResolution; config.ChapterImageResolution = data.ChapterImageResolution.toString() as ImageResolution;
await getConfigurationApi(api) await getSystemApi(api)
.updateConfiguration({ serverConfiguration: config }); .updateConfiguration({ serverConfiguration: config });
void queryClient.invalidateQueries({ void queryClient.invalidateQueries({
+2 -2
View File
@@ -1,4 +1,4 @@
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
import Alert from '@mui/material/Alert'; import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
@@ -40,7 +40,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
EnableExtraThumbsDuplication: data.EnableExtraThumbsDuplication?.toString() === 'on' EnableExtraThumbsDuplication: data.EnableExtraThumbsDuplication?.toString() === 'on'
}; };
await getConfigurationApi(api) await getSystemApi(api)
.updateNamedConfiguration({ key: CONFIG_KEY, body: newConfig }); .updateNamedConfiguration({ key: CONFIG_KEY, body: newConfig });
void queryClient.invalidateQueries({ void queryClient.invalidateQueries({
@@ -21,7 +21,7 @@ import Checkbox from '@mui/material/Checkbox';
import FormHelperText from '@mui/material/FormHelperText'; import FormHelperText from '@mui/material/FormHelperText';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { ServerConnections } from 'lib/jellyfin-apiclient'; import { ServerConnections } from 'lib/jellyfin-apiclient';
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
import { queryClient } from 'utils/query/queryClient'; import { queryClient } from 'utils/query/queryClient';
import { ActionData } from 'types/actionData'; import { ActionData } from 'types/actionData';
@@ -33,7 +33,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const data = await request.json() as LiveTvOptions; const data = await request.json() as LiveTvOptions;
await getConfigurationApi(api) await getSystemApi(api)
.updateNamedConfiguration({ key: CONFIG_KEY, body: data }); .updateNamedConfiguration({ key: CONFIG_KEY, body: data });
void queryClient.invalidateQueries({ void queryClient.invalidateQueries({
+10 -1
View File
@@ -110,7 +110,16 @@ export const Component = () => {
ref={contentPrimaryRef} ref={contentPrimaryRef}
> >
<Box> <Box>
<Typography variant='h1'>{fileName}</Typography> <Typography
variant='h1'
sx={{
whiteSpace: 'normal',
overflowWrap: 'anywhere',
wordBreak: 'break-word'
}}
>
{fileName}
</Typography>
{error && ( {error && (
<Alert <Alert
+3 -3
View File
@@ -1,5 +1,5 @@
import React, { ChangeEvent, useCallback, useEffect, useState } from 'react'; import React, { ChangeEvent, useCallback, useEffect, useState } from 'react';
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
import Loading from 'components/loading/LoadingComponent'; import Loading from 'components/loading/LoadingComponent';
import Page from 'components/Page'; import Page from 'components/Page';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
@@ -24,7 +24,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
if (!api) throw new Error('No Api instance available'); if (!api) throw new Error('No Api instance available');
const formData = await request.formData(); const formData = await request.formData();
const { data: config } = await getConfigurationApi(api).getConfiguration(); const { data: config } = await getSystemApi(api).getConfiguration();
const enableWarningMessage = formData.get('EnableWarningMessage'); const enableWarningMessage = formData.get('EnableWarningMessage');
config.EnableSlowResponseWarning = enableWarningMessage === 'on'; config.EnableSlowResponseWarning = enableWarningMessage === 'on';
@@ -34,7 +34,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
config.SlowResponseThresholdMs = parseInt(responseTime.toString(), 10); config.SlowResponseThresholdMs = parseInt(responseTime.toString(), 10);
} }
await getConfigurationApi(api) await getSystemApi(api)
.updateConfiguration({ serverConfiguration: config }); .updateConfiguration({ serverConfiguration: config });
return { return {
@@ -21,7 +21,7 @@ import DirectoryBrowser from 'components/directorybrowser/directorybrowser';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { ServerConnections } from 'lib/jellyfin-apiclient'; import { ServerConnections } from 'lib/jellyfin-apiclient';
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
import { QUERY_KEY as CONFIG_QUERY_KEY } from 'hooks/useConfiguration'; import { QUERY_KEY as CONFIG_QUERY_KEY } from 'hooks/useConfiguration';
import { queryClient } from 'utils/query/queryClient'; import { queryClient } from 'utils/query/queryClient';
import { encodePublishedServerUris, getPublishedServerUris, PublishedServerUris, splitString } from 'apps/dashboard/features/networking/utils'; import { encodePublishedServerUris, getPublishedServerUris, PublishedServerUris, splitString } from 'apps/dashboard/features/networking/utils';
@@ -69,7 +69,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
newConfig.PublishedServerUriBySubnet = encodePublishedServerUris(publishedServerUri); newConfig.PublishedServerUriBySubnet = encodePublishedServerUris(publishedServerUri);
await getConfigurationApi(api) await getSystemApi(api)
.updateNamedConfiguration({ key: CONFIG_KEY, body: newConfig }); .updateNamedConfiguration({ key: CONFIG_KEY, body: newConfig });
void queryClient.invalidateQueries({ void queryClient.invalidateQueries({
@@ -12,14 +12,14 @@ import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'rea
import { ActionData } from 'types/actionData'; import { ActionData } from 'types/actionData';
import { QUERY_KEY, useConfiguration } from 'hooks/useConfiguration'; import { QUERY_KEY, useConfiguration } from 'hooks/useConfiguration';
import Loading from 'components/loading/LoadingComponent'; import Loading from 'components/loading/LoadingComponent';
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
import { queryClient } from 'utils/query/queryClient'; import { queryClient } from 'utils/query/queryClient';
export const action = async ({ request }: ActionFunctionArgs) => { export const action = async ({ request }: ActionFunctionArgs) => {
const api = ServerConnections.getApi(); const api = ServerConnections.getApi();
if (!api) throw new Error('No Api instance available'); if (!api) throw new Error('No Api instance available');
const { data: config } = await getConfigurationApi(api).getConfiguration(); const { data: config } = await getSystemApi(api).getConfiguration();
const formData = await request.formData(); const formData = await request.formData();
const minResumePercentage = formData.get('MinResumePercentage')?.toString(); const minResumePercentage = formData.get('MinResumePercentage')?.toString();
@@ -34,7 +34,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
if (maxAudiobookResume) config.MaxAudiobookResume = parseInt(maxAudiobookResume, 10); if (maxAudiobookResume) config.MaxAudiobookResume = parseInt(maxAudiobookResume, 10);
if (minResumeDuration) config.MinResumeDurationSeconds = parseInt(minResumeDuration, 10); if (minResumeDuration) config.MinResumeDurationSeconds = parseInt(minResumeDuration, 10);
await getConfigurationApi(api) await getSystemApi(api)
.updateConfiguration({ serverConfiguration: config }); .updateConfiguration({ serverConfiguration: config });
void queryClient.invalidateQueries({ void queryClient.invalidateQueries({
@@ -9,7 +9,7 @@ import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom'; import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom';
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
import { QUERY_KEY, useConfiguration } from 'hooks/useConfiguration'; import { QUERY_KEY, useConfiguration } from 'hooks/useConfiguration';
import Loading from 'components/loading/LoadingComponent'; import Loading from 'components/loading/LoadingComponent';
import { ActionData } from 'types/actionData'; import { ActionData } from 'types/actionData';
@@ -19,13 +19,13 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const api = ServerConnections.getApi(); const api = ServerConnections.getApi();
if (!api) throw new Error('No Api instance available'); if (!api) throw new Error('No Api instance available');
const { data: config } = await getConfigurationApi(api).getConfiguration(); const { data: config } = await getSystemApi(api).getConfiguration();
const formData = await request.formData(); const formData = await request.formData();
const bitrateLimit = formData.get('StreamingBitrateLimit')?.toString(); const bitrateLimit = formData.get('StreamingBitrateLimit')?.toString();
config.RemoteClientBitrateLimit = Math.trunc(1e6 * parseFloat(bitrateLimit || '0')); config.RemoteClientBitrateLimit = Math.trunc(1e6 * parseFloat(bitrateLimit || '0'));
await getConfigurationApi(api) await getSystemApi(api)
.updateConfiguration({ serverConfiguration: config }); .updateConfiguration({ serverConfiguration: config });
void queryClient.invalidateQueries({ void queryClient.invalidateQueries({
@@ -24,7 +24,7 @@ import { QUERY_KEY, useNamedConfiguration } from 'hooks/useNamedConfiguration';
import type { EncodingOptions } from '@jellyfin/sdk/lib/generated-client/models/encoding-options'; import type { EncodingOptions } from '@jellyfin/sdk/lib/generated-client/models/encoding-options';
import { HardwareAccelerationType } from '@jellyfin/sdk/lib/generated-client/models/hardware-acceleration-type'; import { HardwareAccelerationType } from '@jellyfin/sdk/lib/generated-client/models/hardware-acceleration-type';
import { ServerConnections } from 'lib/jellyfin-apiclient'; import { ServerConnections } from 'lib/jellyfin-apiclient';
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
import { queryClient } from 'utils/query/queryClient'; import { queryClient } from 'utils/query/queryClient';
import { ActionData } from 'types/actionData'; import { ActionData } from 'types/actionData';
import { CODECS, HEVC_REXT_DECODING_TYPES, HEVC_VP9_HW_DECODING_TYPES } from 'apps/dashboard/features/playback/constants/codecs'; import { CODECS, HEVC_REXT_DECODING_TYPES, HEVC_VP9_HW_DECODING_TYPES } from 'apps/dashboard/features/playback/constants/codecs';
@@ -38,7 +38,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const data = await request.json() as EncodingOptions; const data = await request.json() as EncodingOptions;
await getConfigurationApi(api) await getSystemApi(api)
.updateNamedConfiguration({ key: CONFIG_KEY, body: data }); .updateNamedConfiguration({ key: CONFIG_KEY, body: data });
void queryClient.invalidateQueries({ void queryClient.invalidateQueries({
@@ -17,7 +17,7 @@ import MenuItem from '@mui/material/MenuItem';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Alert from '@mui/material/Alert'; import Alert from '@mui/material/Alert';
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
import { TrickplayScanBehavior } from '@jellyfin/sdk/lib/generated-client/models/trickplay-scan-behavior'; import { TrickplayScanBehavior } from '@jellyfin/sdk/lib/generated-client/models/trickplay-scan-behavior';
import { ProcessPriorityClass } from '@jellyfin/sdk/lib/generated-client/models/process-priority-class'; import { ProcessPriorityClass } from '@jellyfin/sdk/lib/generated-client/models/process-priority-class';
import { ActionData } from 'types/actionData'; import { ActionData } from 'types/actionData';
@@ -30,7 +30,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData(); const formData = await request.formData();
const data = Object.fromEntries(formData); const data = Object.fromEntries(formData);
const { data: config } = await getConfigurationApi(api).getConfiguration(); const { data: config } = await getSystemApi(api).getConfiguration();
const options = config.TrickplayOptions; const options = config.TrickplayOptions;
if (!options) throw new Error('Unexpected null TrickplayOptions'); if (!options) throw new Error('Unexpected null TrickplayOptions');
@@ -48,7 +48,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
options.JpegQuality = parseInt(data.JpegQuality.toString() || '90', 10); options.JpegQuality = parseInt(data.JpegQuality.toString() || '90', 10);
options.ProcessThreads = parseInt(data.TrickplayThreads.toString() || '1', 10); options.ProcessThreads = parseInt(data.TrickplayThreads.toString() || '1', 10);
await getConfigurationApi(api) await getSystemApi(api)
.updateConfiguration({ serverConfiguration: config }); .updateConfiguration({ serverConfiguration: config });
void queryClient.invalidateQueries({ void queryClient.invalidateQueries({
+3 -3
View File
@@ -22,7 +22,7 @@ import Checkbox from '@mui/material/Checkbox';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import DirectoryBrowser from 'components/directorybrowser/directorybrowser'; import DirectoryBrowser from 'components/directorybrowser/directorybrowser';
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
import { queryClient } from 'utils/query/queryClient'; import { queryClient } from 'utils/query/queryClient';
import { ActionData } from 'types/actionData'; import { ActionData } from 'types/actionData';
@@ -30,7 +30,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const api = ServerConnections.getApi(); const api = ServerConnections.getApi();
if (!api) throw new Error('No Api instance available'); if (!api) throw new Error('No Api instance available');
const { data: config } = await getConfigurationApi(api).getConfiguration(); const { data: config } = await getSystemApi(api).getConfiguration();
const formData = await request.formData(); const formData = await request.formData();
config.ServerName = formData.get('ServerName')?.toString(); config.ServerName = formData.get('ServerName')?.toString();
@@ -41,7 +41,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
config.LibraryScanFanoutConcurrency = parseInt(formData.get('LibraryScanFanoutConcurrency')?.toString() || '0', 10); config.LibraryScanFanoutConcurrency = parseInt(formData.get('LibraryScanFanoutConcurrency')?.toString() || '0', 10);
config.ParallelImageEncodingLimit = parseInt(formData.get('ParallelImageEncodingLimit')?.toString() || '0', 10); config.ParallelImageEncodingLimit = parseInt(formData.get('ParallelImageEncodingLimit')?.toString() || '0', 10);
await getConfigurationApi(api) await getSystemApi(api)
.updateConfiguration({ serverConfiguration: config }); .updateConfiguration({ serverConfiguration: config });
void queryClient.invalidateQueries({ void queryClient.invalidateQueries({
+7
View File
@@ -167,6 +167,10 @@ const UserNew = () => {
setIsErrorToastOpen(true); setIsErrorToastOpen(true);
} }
}); });
},
onError: () => {
loading.hide();
setIsErrorToastOpen(true);
} }
}); });
}; };
@@ -231,6 +235,9 @@ const UserNew = () => {
label={globalize.translate('LabelName')} label={globalize.translate('LabelName')}
required required
/> />
<div className='fieldDescription'>
{globalize.translate('LabelUsernameAllowedCharactersHelp')}
</div>
</div> </div>
<div className='inputContainer'> <div className='inputContainer'>
<Input <Input
+2 -2
View File
@@ -2,7 +2,7 @@ import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-ite
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields'; import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by'; import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by';
import { getStudiosApi } from '@jellyfin/sdk/lib/utils/api/studios-api'; import { getStudioApi } from '@jellyfin/sdk/lib/utils/api/studio-api';
import cardBuilder from 'components/cardbuilder/cardBuilder'; import cardBuilder from 'components/cardbuilder/cardBuilder';
import { getBackdropShape, getPortraitShape, getSquareShape } from 'components/cardbuilder/utils/shape'; import { getBackdropShape, getPortraitShape, getSquareShape } from 'components/cardbuilder/utils/shape';
@@ -208,7 +208,7 @@ function getFetchDataFn(section) {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }
return getStudiosApi(api) return getStudioApi(api)
.getStudios({ .getStudios({
userId, userId,
isFavorite: true, isFavorite: true,
@@ -2,7 +2,6 @@ import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-ite
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields'; import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
import { PersonKind } from '@jellyfin/sdk/lib/generated-client/models/person-kind'; import { PersonKind } from '@jellyfin/sdk/lib/generated-client/models/person-kind';
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api'; import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
import { intervalToDuration } from 'date-fns'; import { intervalToDuration } from 'date-fns';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
@@ -1088,7 +1087,7 @@ function renderMoreFromSeason(view, item, apiClient) {
cardBuilder.buildCards(result.Items, { cardBuilder.buildCards(result.Items, {
parentContainer: section, parentContainer: section,
itemsContainer: itemsContainer, itemsContainer: itemsContainer,
shape: 'autooverflow', shape: 'overflowBackdrop',
sectionTitleTagName: 'h2', sectionTitleTagName: 'h2',
scalable: true, scalable: true,
showTitle: true, showTitle: true,
@@ -2170,7 +2169,7 @@ export default function (view, params) {
return; return;
} }
getUserLibraryApi(api).getItem({ getLibraryApi(api).getItem({
userId: apiClient?.getCurrentUserId(), userId: apiClient?.getCurrentUserId(),
itemId: selectedId itemId: selectedId
}).then(function ({ data: altItem }) { }).then(function ({ data: altItem }) {
+2 -2
View File
@@ -1,4 +1,4 @@
import { getLyricsApi } from '@jellyfin/sdk/lib/utils/api/lyrics-api'; import { getLyricApi } from '@jellyfin/sdk/lib/utils/api/lyric-api';
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import { AutoScroll } from 'apps/legacy/features/lyrics/constants/autoScroll'; import { AutoScroll } from 'apps/legacy/features/lyrics/constants/autoScroll';
@@ -137,7 +137,7 @@ export default function (view) {
return; return;
} }
return getLyricsApi(api).getLyrics({ itemId }) return getLyricApi(api).getLyrics({ itemId })
.then(({ data }) => { .then(({ data }) => {
if (!data.Lyrics?.length) { if (!data.Lyrics?.length) {
throw new Error('No lyrics returned'); throw new Error('No lyrics returned');
@@ -1,6 +1,6 @@
import type { Api } from '@jellyfin/sdk/lib/api'; import type { Api } from '@jellyfin/sdk/lib/api';
import type { UserLibraryApiGetLatestMediaRequest } from '@jellyfin/sdk/lib/generated-client/api/user-library-api'; import type { LibraryApiGetLatestMediaRequest } from '@jellyfin/sdk/lib/generated-client/api/library-api';
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api'; import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
import { queryOptions, useQuery } from '@tanstack/react-query'; import { queryOptions, useQuery } from '@tanstack/react-query';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
@@ -8,17 +8,17 @@ import { useApi } from 'hooks/useApi';
const fetchLatestMedia = async ( const fetchLatestMedia = async (
api: Api, api: Api,
params?: UserLibraryApiGetLatestMediaRequest, params?: LibraryApiGetLatestMediaRequest,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getUserLibraryApi(api) const response = await getLibraryApi(api)
.getLatestMedia(params, options); .getLatestMedia(params, options);
return response.data; return response.data;
}; };
export const getLatestMediaQuery = ( export const getLatestMediaQuery = (
api?: Api, api?: Api,
params?: UserLibraryApiGetLatestMediaRequest params?: LibraryApiGetLatestMediaRequest
) => queryOptions({ ) => queryOptions({
queryKey: [ 'User', params?.userId, 'LatestMedia', params ], queryKey: [ 'User', params?.userId, 'LatestMedia', params ],
queryFn: ({ signal }) => fetchLatestMedia(api!, params, { signal }), queryFn: ({ signal }) => fetchLatestMedia(api!, params, { signal }),
@@ -26,7 +26,7 @@ export const getLatestMediaQuery = (
}); });
export const useLatestMedia = ( export const useLatestMedia = (
params?: UserLibraryApiGetLatestMediaRequest params?: LibraryApiGetLatestMediaRequest
) => { ) => {
const { api, user } = useApi(); const { api, user } = useApi();
return useQuery(getLatestMediaQuery(api, { return useQuery(getLatestMediaQuery(api, {
@@ -1,6 +1,6 @@
import type { Api } from '@jellyfin/sdk/lib/api'; import type { Api } from '@jellyfin/sdk/lib/api';
import type { TvShowsApiGetNextUpRequest } from '@jellyfin/sdk/lib/generated-client/api/tv-shows-api'; import type { ShowApiGetNextUpRequest } from '@jellyfin/sdk/lib/generated-client/api/show-api';
import { getTvShowsApi } from '@jellyfin/sdk/lib/utils/api/tv-shows-api'; import { getShowApi } from '@jellyfin/sdk/lib/utils/api/show-api';
import { queryOptions, useQuery } from '@tanstack/react-query'; import { queryOptions, useQuery } from '@tanstack/react-query';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
@@ -8,17 +8,17 @@ import { useApi } from 'hooks/useApi';
const fetchNextUp = async ( const fetchNextUp = async (
api: Api, api: Api,
params?: TvShowsApiGetNextUpRequest, params?: ShowApiGetNextUpRequest,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getTvShowsApi(api) const response = await getShowApi(api)
.getNextUp(params, options); .getNextUp(params, options);
return response.data; return response.data;
}; };
export const getNextUpQuery = ( export const getNextUpQuery = (
api?: Api, api?: Api,
params?: TvShowsApiGetNextUpRequest params?: ShowApiGetNextUpRequest
) => queryOptions({ ) => queryOptions({
queryKey: [ 'User', params?.userId, 'NextUp', params ], queryKey: [ 'User', params?.userId, 'NextUp', params ],
queryFn: ({ signal }) => fetchNextUp(api!, params, { signal }), queryFn: ({ signal }) => fetchNextUp(api!, params, { signal }),
@@ -26,7 +26,7 @@ export const getNextUpQuery = (
}); });
export const useNextUp = ( export const useNextUp = (
params?: TvShowsApiGetNextUpRequest params?: ShowApiGetNextUpRequest
) => { ) => {
const { api, user } = useApi(); const { api, user } = useApi();
return useQuery(getNextUpQuery(api, { return useQuery(getNextUpQuery(api, {
@@ -1,6 +1,6 @@
import type { Api } from '@jellyfin/sdk/lib/api'; import type { Api } from '@jellyfin/sdk/lib/api';
import type { ItemsApiGetResumeItemsRequest } from '@jellyfin/sdk/lib/generated-client/api/items-api'; import type { LibraryApiGetResumeItemsRequest } from '@jellyfin/sdk/lib/generated-client/api/library-api';
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'; import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
import { queryOptions, useQuery } from '@tanstack/react-query'; import { queryOptions, useQuery } from '@tanstack/react-query';
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
@@ -8,17 +8,17 @@ import { useApi } from 'hooks/useApi';
const fetchResumeItems = async ( const fetchResumeItems = async (
api: Api, api: Api,
params?: ItemsApiGetResumeItemsRequest, params?: LibraryApiGetResumeItemsRequest,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getItemsApi(api) const response = await getLibraryApi(api)
.getResumeItems(params, options); .getResumeItems(params, options);
return response.data; return response.data;
}; };
export const getResumeItemsQuery = ( export const getResumeItemsQuery = (
api?: Api, api?: Api,
params?: ItemsApiGetResumeItemsRequest params?: LibraryApiGetResumeItemsRequest
) => queryOptions({ ) => queryOptions({
queryKey: [ 'User', params?.userId, 'ResumeItems', params ], queryKey: [ 'User', params?.userId, 'ResumeItems', params ],
queryFn: ({ signal }) => fetchResumeItems(api!, params, { signal }), queryFn: ({ signal }) => fetchResumeItems(api!, params, { signal }),
@@ -26,7 +26,7 @@ export const getResumeItemsQuery = (
}); });
export const useResumeItems = ( export const useResumeItems = (
params?: ItemsApiGetResumeItemsRequest params?: LibraryApiGetResumeItemsRequest
) => { ) => {
const { api, user } = useApi(); const { api, user } = useApi();
return useQuery(getResumeItemsQuery(api, { return useQuery(getResumeItemsQuery(api, {
@@ -1,7 +1,7 @@
import type { Api } from '@jellyfin/sdk/lib/api'; import type { Api } from '@jellyfin/sdk/lib/api';
import type { MediaSegmentDto } from '@jellyfin/sdk/lib/generated-client/models/media-segment-dto'; import type { MediaSegmentDto } from '@jellyfin/sdk/lib/generated-client/models/media-segment-dto';
import { MediaSegmentType } from '@jellyfin/sdk/lib/generated-client/models/media-segment-type'; import { MediaSegmentType } from '@jellyfin/sdk/lib/generated-client/models/media-segment-type';
import { getMediaSegmentsApi } from '@jellyfin/sdk/lib/utils/api/media-segments-api'; import { getMediaSegmentApi } from '@jellyfin/sdk/lib/utils/api/media-segment-api';
import type { PlaybackManager } from 'components/playback/playbackmanager'; import type { PlaybackManager } from 'components/playback/playbackmanager';
import { TICKS_PER_MILLISECOND, TICKS_PER_SECOND } from 'constants/time'; import { TICKS_PER_MILLISECOND, TICKS_PER_SECOND } from 'constants/time';
@@ -25,7 +25,7 @@ class MediaSegmentManager extends PlaybackSubscriber {
private async fetchMediaSegments(api: Api, itemId: string, includeSegmentTypes: MediaSegmentType[]) { private async fetchMediaSegments(api: Api, itemId: string, includeSegmentTypes: MediaSegmentType[]) {
try { try {
const { data: mediaSegments } = await getMediaSegmentsApi(api) const { data: mediaSegments } = await getMediaSegmentApi(api)
.getItemSegments({ itemId, includeSegmentTypes }); .getItemSegments({ itemId, includeSegmentTypes });
this.mediaSegments = mediaSegments.Items || []; this.mediaSegments = mediaSegments.Items || [];
} catch (err) { } catch (err) {
@@ -21,7 +21,7 @@ export interface PlaybackSubscriber {
onPlayerChange?(e: Event, player: PlayerPlugin, target: PlayTarget, previousPlayer: PlayerPlugin): void onPlayerChange?(e: Event, player: PlayerPlugin, target: PlayTarget, previousPlayer: PlayerPlugin): void
onPromptSkip?(e: Event, mediaSegment: MediaSegmentDto): void onPromptSkip?(e: Event, mediaSegment: MediaSegmentDto): void
onPlayerError?(e: Event, error: PlayerError): void onPlayerError?(e: Event, error: PlayerError): void
onPlayerFullscreenChange?(e: Event): void onPlayerFullscreenChange?(e: Event, fullscreen: boolean): void
onPlayerItemStarted?(e: Event, item?: BaseItemDto, mediaSource?: MediaSourceInfo): void onPlayerItemStarted?(e: Event, item?: BaseItemDto, mediaSource?: MediaSourceInfo): void
onPlayerItemStopped?(e: Event, info: ManagedPlayerStopInfo): void onPlayerItemStopped?(e: Event, info: ManagedPlayerStopInfo): void
onPlayerMediaStreamsChange?(e: Event): void onPlayerMediaStreamsChange?(e: Event): void
@@ -1,16 +1,16 @@
import { Api } from '@jellyfin/sdk/lib/api'; import { Api } from '@jellyfin/sdk/lib/api';
import { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client/api/items-api'; import { LibraryApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client/api/library-api';
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'; import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
import { AxiosRequestConfig } from 'axios'; import { AxiosRequestConfig } from 'axios';
import { QUERY_OPTIONS } from '../constants/queryOptions'; import { QUERY_OPTIONS } from '../constants/queryOptions';
export const fetchItemsByType = async ( export const fetchItemsByType = async (
api: Api, api: Api,
userId?: string, userId?: string,
params?: ItemsApiGetItemsRequest, params?: LibraryApiGetItemsRequest,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getItemsApi(api).getItems( const response = await getLibraryApi(api).getItems(
{ {
...QUERY_OPTIONS, ...QUERY_OPTIONS,
userId, userId,
@@ -1,7 +1,7 @@
import { Api } from '@jellyfin/sdk'; import { Api } from '@jellyfin/sdk';
import { ArtistsApiGetArtistsRequest } from '@jellyfin/sdk/lib/generated-client/api/artists-api'; import { ArtistApiGetArtistsRequest } from '@jellyfin/sdk/lib/generated-client/api/artist-api';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { getArtistsApi } from '@jellyfin/sdk/lib/utils/api/artists-api'; import { getArtistApi } from '@jellyfin/sdk/lib/utils/api/artist-api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { AxiosRequestConfig } from 'axios'; import { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
@@ -11,10 +11,10 @@ import { isMusic } from '../utils/search';
const fetchArtists = async ( const fetchArtists = async (
api: Api, api: Api,
userId: string, userId: string,
params?: ArtistsApiGetArtistsRequest, params?: ArtistApiGetArtistsRequest,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getArtistsApi(api).getArtists( const response = await getArtistApi(api).getArtists(
{ {
...QUERY_OPTIONS, ...QUERY_OPTIONS,
userId, userId,
@@ -5,17 +5,17 @@ import { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { QUERY_OPTIONS } from '../constants/queryOptions'; import { QUERY_OPTIONS } from '../constants/queryOptions';
import { isMovies, isTVShows } from '../utils/search'; import { isMovies, isTVShows } from '../utils/search';
import { PersonsApiGetPersonsRequest } from '@jellyfin/sdk/lib/generated-client/api/persons-api'; import { PersonApiGetPersonsRequest } from '@jellyfin/sdk/lib/generated-client/api/person-api';
import { getPersonsApi } from '@jellyfin/sdk/lib/utils/api/persons-api'; import { getPersonApi } from '@jellyfin/sdk/lib/utils/api/person-api';
import { PersonKind } from '@jellyfin/sdk/lib/generated-client/models/person-kind'; import { PersonKind } from '@jellyfin/sdk/lib/generated-client/models/person-kind';
const fetchPeople = async ( const fetchPeople = async (
api: Api, api: Api,
userId: string, userId: string,
params?: PersonsApiGetPersonsRequest, params?: PersonApiGetPersonsRequest,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getPersonsApi(api).getPersons( const response = await getPersonApi(api).getPersons(
{ {
...QUERY_OPTIONS, ...QUERY_OPTIONS,
userId, userId,
@@ -4,13 +4,13 @@ import { useQuery } from '@tanstack/react-query';
import { AxiosRequestConfig } from 'axios'; import { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client/api/items-api'; import { LibraryApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client/api/library-api';
import { fetchItemsByType } from './fetchItemsByType'; import { fetchItemsByType } from './fetchItemsByType';
const fetchPrograms = async ( const fetchPrograms = async (
api: Api, api: Api,
userId: string, userId: string,
params?: ItemsApiGetItemsRequest, params?: LibraryApiGetItemsRequest,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await fetchItemsByType( const response = await fetchItemsByType(
@@ -2,7 +2,7 @@ import type { AxiosRequestConfig } from 'axios';
import type { Api } from '@jellyfin/sdk'; import type { Api } from '@jellyfin/sdk';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by'; import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by';
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'; import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
@@ -13,7 +13,7 @@ const fetchGetItems = async (
parentId?: string, parentId?: string,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getItemsApi(api).getItems( const response = await getLibraryApi(api).getItems(
{ {
userId, userId,
sortBy: [ItemSortBy.IsFavoriteOrLiked, ItemSortBy.Random], sortBy: [ItemSortBy.IsFavoriteOrLiked, ItemSortBy.Random],
@@ -1,7 +1,7 @@
import { Api } from '@jellyfin/sdk'; import { Api } from '@jellyfin/sdk';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { StudiosApiGetStudiosRequest } from '@jellyfin/sdk/lib/generated-client/api/studios-api'; import { StudioApiGetStudiosRequest } from '@jellyfin/sdk/lib/generated-client/api/studio-api';
import { getStudiosApi } from '@jellyfin/sdk/lib/utils/api/studios-api'; import { getStudioApi } from '@jellyfin/sdk/lib/utils/api/studio-api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { AxiosRequestConfig } from 'axios'; import { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
@@ -11,10 +11,10 @@ import { isMovies, isTVShows } from '../utils/search';
const fetchStudios = async ( const fetchStudios = async (
api: Api, api: Api,
userId: string, userId: string,
params?: StudiosApiGetStudiosRequest, params?: StudioApiGetStudiosRequest,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getStudiosApi(api).getStudios( const response = await getStudioApi(api).getStudios(
{ {
...QUERY_OPTIONS, ...QUERY_OPTIONS,
userId, userId,
@@ -4,18 +4,18 @@ import { useQuery } from '@tanstack/react-query';
import { AxiosRequestConfig } from 'axios'; import { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { QUERY_OPTIONS } from '../constants/queryOptions'; import { QUERY_OPTIONS } from '../constants/queryOptions';
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'; import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type'; import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client/api/items-api'; import { LibraryApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client/api/library-api';
const fetchVideos = async ( const fetchVideos = async (
api: Api, api: Api,
userId: string, userId: string,
params?: ItemsApiGetItemsRequest, params?: LibraryApiGetItemsRequest,
options?: AxiosRequestConfig options?: AxiosRequestConfig
) => { ) => {
const response = await getItemsApi(api).getItems( const response = await getLibraryApi(api).getItems(
{ {
...QUERY_OPTIONS, ...QUERY_OPTIONS,
userId, userId,
+1 -1
View File
@@ -1,4 +1,4 @@
import { AsyncRoute } from '../../../../components/router/AsyncRoute'; import { AsyncRoute } from 'components/router/AsyncRoute';
export const ASYNC_PUBLIC_ROUTES: AsyncRoute[] = [ export const ASYNC_PUBLIC_ROUTES: AsyncRoute[] = [
{ path: 'forgotpassword', page: 'session/forgotPassword' } { path: 'forgotpassword', page: 'session/forgotPassword' }
@@ -1,4 +1,4 @@
import { getQuickConnectApi } from '@jellyfin/sdk/lib/utils/api/quick-connect-api'; import { getAuthenticationApi } from '@jellyfin/sdk/lib/utils/api/authentication-api';
import React, { FC, FormEvent, useCallback, useState } from 'react'; import React, { FC, FormEvent, useCallback, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom'; import { Link, useSearchParams } from 'react-router-dom';
@@ -41,7 +41,7 @@ const QuickConnectPage: FC = () => {
const normalizedCode = code.replace(/\s/g, ''); const normalizedCode = code.replace(/\s/g, '');
console.log('[QuickConnect] authorizing code %s as user %s', normalizedCode, userId); console.log('[QuickConnect] authorizing code %s as user %s', normalizedCode, userId);
getQuickConnectApi(api) getAuthenticationApi(api)
.authorizeQuickConnect({ .authorizeQuickConnect({
code: normalizedCode, code: normalizedCode,
userId userId
+1 -1
View File
@@ -42,7 +42,7 @@ export const Component = () => {
}} }}
> >
<StrictMode> <StrictMode>
<OffsetAppBar dense elevation={4}> <OffsetAppBar dense>
<AppToolbar <AppToolbar
isDrawerAvailable={!isMediumScreen && isDrawerAvailable} isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
isDrawerOpen={isDrawerOpen} isDrawerOpen={isDrawerOpen}
+6
View File
@@ -24,6 +24,12 @@ $mui-bp-xl: 1536px;
} }
.layout-mobile { .layout-mobile {
.itemDetailPage {
// Fix horizontal panning on mobile item details page
overflow-x: hidden;
overflow-x: clip;
}
.itemBackdrop { .itemBackdrop {
// Fix backdrop position on mobile item details page // Fix backdrop position on mobile item details page
margin-top: -48px !important; margin-top: -48px !important;
@@ -47,6 +47,7 @@ const AppToolbar: FC<AppToolbarProps> = ({
onDrawerButtonClick={onDrawerButtonClick} onDrawerButtonClick={onDrawerButtonClick}
isBackButtonAvailable={isBackButtonAvailable} isBackButtonAvailable={isBackButtonAvailable}
isUserMenuAvailable={!isPublicPath} isUserMenuAvailable={!isPublicPath}
className='padded-left padded-right'
> >
{!isDrawerAvailable && ( {!isDrawerAvailable && (
<Stack <Stack
@@ -1,3 +1,4 @@
import { getDisplayVersion } from '@jellyfin/sdk/lib/utils/versioning';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText'; import ListItemText from '@mui/material/ListItemText';
@@ -5,7 +6,6 @@ import React from 'react';
import { useSystemInfo } from 'hooks/useSystemInfo'; import { useSystemInfo } from 'hooks/useSystemInfo';
import ListItemLink from 'components/ListItemLink'; import ListItemLink from 'components/ListItemLink';
import { getDisplayVersion } from 'utils/versions';
import appIcon from '@jellyfin/ux-web/icon-transparent.png'; import appIcon from '@jellyfin/ux-web/icon-transparent.png';
@@ -1,5 +1,5 @@
import type { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api'; import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api'; import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { type JellyfinApiContext, useApi } from 'hooks/useApi'; import { type JellyfinApiContext, useApi } from 'hooks/useApi';
@@ -33,7 +33,7 @@ const getItemByType = async (
); );
break; break;
default: { default: {
response = await getUserLibraryApi(api).getItem( response = await getLibraryApi(api).getItem(
{ userId: user.Id, itemId }, { userId: user.Id, itemId },
options options
); );
@@ -57,7 +57,7 @@ const ItemsView: FC = () => {
listOptions.showArtist = true; listOptions.showArtist = true;
listOptions.addToListButton = true; listOptions.addToListButton = true;
} else if (viewType === LibraryTab.Albums) { } else if (viewType === LibraryTab.Albums) {
listOptions.sortBy = libraryViewSettings.SortBy; listOptions.sortBy = libraryViewSettings.SortBy[0];
listOptions.addToListButton = true; listOptions.addToListButton = true;
} else if (viewType === LibraryTab.Episodes) { } else if (viewType === LibraryTab.Episodes) {
listOptions.showParentTitle = true; listOptions.showParentTitle = true;
@@ -171,7 +171,7 @@ const ItemsView: FC = () => {
noItemsMessage noItemsMessage
]); ]);
const hasSortName = libraryViewSettings.SortBy !== ItemSortBy.Random; const hasSortName = !libraryViewSettings.SortBy.includes(ItemSortBy.Random);
const itemsContainerClass = classNames( const itemsContainerClass = classNames(
'padded-left padded-right', 'padded-left padded-right',
@@ -104,61 +104,21 @@ const LibraryToolbar: FC = () => {
> >
<LibraryViewMenu /> <LibraryViewMenu />
<Box {!MENU_ONLY_VIEWS.includes(viewType) && (
sx={{ <Box
display: 'flex', sx={{
flexGrow: { display: 'flex',
xs: 1, flexGrow: {
sm: 0 xs: 1,
}, sm: 0
justifyContent: 'flex-end', },
marginLeft: 1 justifyContent: 'flex-end',
}} marginLeft: 1
> }}
<ButtonGroup
variant='contained'
> >
{isBtnPlayAllEnabled && ( <Chip label={itemCountDisplay} />
<PlayAllButton </Box>
item={item} )}
items={items}
viewType={viewType}
collectionType={collectionType}
hasFilters={hasFilters}
isTextVisible={isSmallScreen}
libraryViewSettings={libraryViewSettings}
/>
)}
{isBtnShuffleEnabled && (
<ShuffleButton
item={item}
items={items}
viewType={viewType}
collectionType={collectionType}
hasFilters={hasFilters}
isTextVisible={isSmallScreen && !isBtnPlayAllEnabled}
libraryViewSettings={libraryViewSettings}
/>
)}
{isBtnQueueEnabled && item && playbackManager.canQueue(item) && (
<QueueButton
item={item}
items={items}
hasFilters={hasFilters}
isTextVisible={isSmallScreen && !isBtnPlayAllEnabled && !isBtnShuffleEnabled}
/>
)}
</ButtonGroup>
{isBtnNewCollectionEnabled && canCreateCollections && (
<NewCollectionButton isTextVisible={isSmallScreen} queryKey={allItemsQueryKey} />
)}
{isBtnNewPlaylistEnabled && (
<NewPlaylistButton isTextVisible={isSmallScreen} queryKey={allItemsQueryKey} />
)}
</Box>
{!MENU_ONLY_VIEWS.includes(viewType) && ( {!MENU_ONLY_VIEWS.includes(viewType) && (
<Stack <Stack
@@ -174,7 +134,10 @@ const LibraryToolbar: FC = () => {
sm: 'auto' sm: 'auto'
}, },
flexGrow: 1, flexGrow: 1,
marginTop: 0.5, marginTop: {
xs: 1,
sm: 0.5
},
marginBottom: 0.5 marginBottom: 0.5
}} }}
> >
@@ -188,7 +151,49 @@ const LibraryToolbar: FC = () => {
} }
}} }}
> >
<Chip label={itemCountDisplay} /> <ButtonGroup
variant='contained'
>
{isBtnPlayAllEnabled && (
<PlayAllButton
item={item}
items={items}
viewType={viewType}
collectionType={collectionType}
hasFilters={hasFilters}
isTextVisible={isSmallScreen}
libraryViewSettings={libraryViewSettings}
/>
)}
{isBtnShuffleEnabled && (
<ShuffleButton
item={item}
items={items}
viewType={viewType}
collectionType={collectionType}
hasFilters={hasFilters}
isTextVisible={isSmallScreen && !isBtnPlayAllEnabled}
libraryViewSettings={libraryViewSettings}
/>
)}
{isBtnQueueEnabled && item && playbackManager.canQueue(item) && (
<QueueButton
item={item}
items={items}
hasFilters={hasFilters}
isTextVisible={isSmallScreen && !isBtnPlayAllEnabled && !isBtnShuffleEnabled}
/>
)}
</ButtonGroup>
{isBtnNewCollectionEnabled && canCreateCollections && (
<NewCollectionButton isTextVisible={isSmallScreen} queryKey={allItemsQueryKey} />
)}
{isBtnNewPlaylistEnabled && (
<NewPlaylistButton isTextVisible={isSmallScreen} queryKey={allItemsQueryKey} />
)}
</Box> </Box>
<ButtonGroup <ButtonGroup
@@ -34,7 +34,6 @@ const PageTabContent: FC<PageTabContentProps> = ({ parentId, currentTab }) => {
if (currentTab.viewType === LibraryTab.Programs || currentTab.viewType === LibraryTab.Recordings || currentTab.viewType === LibraryTab.Schedule) { if (currentTab.viewType === LibraryTab.Programs || currentTab.viewType === LibraryTab.Recordings || currentTab.viewType === LibraryTab.Schedule) {
return ( return (
<ProgramsSectionView <ProgramsSectionView
parentId={parentId}
sectionType={ sectionType={
currentTab.sectionsView?.programSections ?? [] currentTab.sectionsView?.programSections ?? []
} }
@@ -43,7 +43,7 @@ const PlayAllButton: FC<PlayAllButtonProps> = ({
items: [item], items: [item],
autoplay: true, autoplay: true,
queryOptions: { queryOptions: {
SortBy: [libraryViewSettings.SortBy], SortBy: libraryViewSettings.SortBy,
SortOrder: [libraryViewSettings.SortOrder] SortOrder: [libraryViewSettings.SortOrder]
} }
}).catch(err => { }).catch(err => {
@@ -56,7 +56,7 @@ const PlayAllButton: FC<PlayAllButtonProps> = ({
queryOptions: { queryOptions: {
ParentId: item?.Id ?? undefined, ParentId: item?.Id ?? undefined,
...getFiltersQuery(viewType, libraryViewSettings), ...getFiltersQuery(viewType, libraryViewSettings),
SortBy: [libraryViewSettings.SortBy], SortBy: libraryViewSettings.SortBy,
SortOrder: [libraryViewSettings.SortOrder] SortOrder: [libraryViewSettings.SortOrder]
} }
}).catch(err => { }).catch(err => {
@@ -9,22 +9,19 @@ import { ItemAction } from 'constants/itemAction';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { useGetProgramsSectionsWithItems, useGetTimers } from 'hooks/useFetchItems'; import { useGetProgramsSectionsWithItems, useGetTimers } from 'hooks/useFetchItems';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
import type { ParentId } from 'types/library';
import type { Section, SectionType } from 'types/sections'; import type { Section, SectionType } from 'types/sections';
interface ProgramsSectionViewProps { interface ProgramsSectionViewProps {
parentId: ParentId;
sectionType: SectionType[]; sectionType: SectionType[];
isUpcomingRecordingsEnabled: boolean | undefined isUpcomingRecordingsEnabled: boolean | undefined
} }
const ProgramsSectionView: FC<ProgramsSectionViewProps> = ({ const ProgramsSectionView: FC<ProgramsSectionViewProps> = ({
parentId,
sectionType, sectionType,
isUpcomingRecordingsEnabled = false isUpcomingRecordingsEnabled = false
}) => { }) => {
const { __legacyApiClient__ } = useApi(); const { __legacyApiClient__ } = useApi();
const { isLoading, data: sectionsWithItems, refetch } = useGetProgramsSectionsWithItems(parentId, sectionType); const { isLoading, data: sectionsWithItems, refetch } = useGetProgramsSectionsWithItems(sectionType);
const { const {
isLoading: isUpcomingRecordingsLoading, isLoading: isUpcomingRecordingsLoading,
data: upcomingRecordings data: upcomingRecordings
@@ -14,52 +14,53 @@ import Popover from '@mui/material/Popover';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
import { LibraryViewSettings } from 'types/library'; import { LibraryViewSettings } from 'types/library';
import { LibraryTab } from 'types/libraryTab'; import { LibraryTab } from 'types/libraryTab';
import isEqual from 'lodash-es/isEqual';
type SortOption = { type SortOption = {
label: string; label: string;
value: ItemSortBy; value: ItemSortBy[];
}; };
type SortOptionsMapping = Record<string, SortOption[]>; type SortOptionsMapping = Record<string, SortOption[]>;
const collectionMovieOptions: SortOption[] = [ const collectionMovieOptions: SortOption[] = [
{ label: 'Name', value: ItemSortBy.SortName }, { label: 'Name', value: [ItemSortBy.SortName] },
{ label: 'OptionRandom', value: ItemSortBy.Random }, { label: 'OptionRandom', value: [ItemSortBy.Random] },
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating }, { label: 'OptionCommunityRating', value: [ItemSortBy.CommunityRating, ItemSortBy.SortName] },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated }, { label: 'OptionDateAdded', value: [ItemSortBy.DateCreated, ItemSortBy.SortName] },
{ label: 'OptionParentalRating', value: ItemSortBy.OfficialRating }, { label: 'OptionParentalRating', value: [ItemSortBy.OfficialRating, ItemSortBy.SortName] },
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate } { label: 'OptionReleaseDate', value: [ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName] }
]; ];
const movieOrFavoriteOptions = [ const movieOrFavoriteOptions = [
{ label: 'Name', value: ItemSortBy.SortName }, { label: 'Name', value: [ItemSortBy.SortName] },
{ label: 'OptionRandom', value: ItemSortBy.Random }, { label: 'OptionRandom', value: [ItemSortBy.Random] },
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating }, { label: 'OptionCommunityRating', value: [ItemSortBy.CommunityRating, ItemSortBy.SortName] },
{ label: 'OptionCriticRating', value: ItemSortBy.CriticRating }, { label: 'OptionCriticRating', value: [ItemSortBy.CriticRating, ItemSortBy.SortName] },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated }, { label: 'OptionDateAdded', value: [ItemSortBy.DateCreated, ItemSortBy.SortName] },
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed }, { label: 'OptionDatePlayed', value: [ItemSortBy.DatePlayed, ItemSortBy.SortName] },
{ label: 'OptionParentalRating', value: ItemSortBy.OfficialRating }, { label: 'OptionParentalRating', value: [ItemSortBy.OfficialRating, ItemSortBy.SortName] },
{ label: 'OptionPlayCount', value: ItemSortBy.PlayCount }, { label: 'OptionPlayCount', value: [ItemSortBy.PlayCount, ItemSortBy.SortName] },
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate }, { label: 'OptionReleaseDate', value: [ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName] },
{ label: 'Runtime', value: ItemSortBy.Runtime } { label: 'Runtime', value: [ItemSortBy.Runtime, ItemSortBy.SortName] }
]; ];
const photosOrPhotoAlbumsOptions = [ const photosOrPhotoAlbumsOptions = [
{ label: 'Name', value: ItemSortBy.SortName }, { label: 'Name', value: [ItemSortBy.SortName] },
{ label: 'OptionRandom', value: ItemSortBy.Random }, { label: 'OptionRandom', value: [ItemSortBy.Random] },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated } { label: 'OptionDateAdded', value: [ItemSortBy.DateCreated, ItemSortBy.SortName] }
]; ];
const videoOrMusicVideoOptions = [ const videoOrMusicVideoOptions = [
{ label: 'Name', value: ItemSortBy.SortName }, { label: 'Name', value: [ItemSortBy.SortName] },
{ label: 'OptionRandom', value: ItemSortBy.Random }, { label: 'OptionRandom', value: [ItemSortBy.Random] },
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating }, { label: 'OptionCommunityRating', value: [ItemSortBy.CommunityRating, ItemSortBy.SortName] },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated }, { label: 'OptionDateAdded', value: [ItemSortBy.DateCreated, ItemSortBy.SortName] },
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed }, { label: 'OptionDatePlayed', value: [ItemSortBy.DatePlayed, ItemSortBy.SortName] },
{ label: 'OptionParentalRating', value: ItemSortBy.OfficialRating }, { label: 'OptionParentalRating', value: [ItemSortBy.OfficialRating, ItemSortBy.SortName] },
{ label: 'OptionPlayCount', value: ItemSortBy.PlayCount }, { label: 'OptionPlayCount', value: [ItemSortBy.PlayCount, ItemSortBy.SortName] },
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate }, { label: 'OptionReleaseDate', value: [ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName] },
{ label: 'Runtime', value: ItemSortBy.Runtime } { label: 'Runtime', value: [ItemSortBy.Runtime, ItemSortBy.SortName] }
]; ];
const sortOptionsMapping: SortOptionsMapping = { const sortOptionsMapping: SortOptionsMapping = {
@@ -67,91 +68,91 @@ const sortOptionsMapping: SortOptionsMapping = {
[LibraryTab.Collections]: collectionMovieOptions, [LibraryTab.Collections]: collectionMovieOptions,
[LibraryTab.Favorites]: movieOrFavoriteOptions, [LibraryTab.Favorites]: movieOrFavoriteOptions,
[LibraryTab.Series]: [ [LibraryTab.Series]: [
{ label: 'Name', value: ItemSortBy.SortName }, { label: 'Name', value: [ItemSortBy.SortName] },
{ label: 'OptionRandom', value: ItemSortBy.Random }, { label: 'OptionRandom', value: [ItemSortBy.Random] },
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating }, { label: 'OptionCommunityRating', value: [ItemSortBy.CommunityRating, ItemSortBy.SortName] },
{ label: 'OptionDateShowAdded', value: ItemSortBy.DateCreated }, { label: 'OptionDateShowAdded', value: [ItemSortBy.DateCreated, ItemSortBy.SortName] },
{ label: 'OptionDateEpisodeAdded', value: ItemSortBy.DateLastContentAdded }, { label: 'OptionDateEpisodeAdded', value: [ItemSortBy.DateLastContentAdded, ItemSortBy.SortName] },
{ label: 'OptionDatePlayed', value: ItemSortBy.SeriesDatePlayed }, { label: 'OptionDatePlayed', value: [ItemSortBy.SeriesDatePlayed, ItemSortBy.SortName] },
{ label: 'OptionParentalRating', value: ItemSortBy.OfficialRating }, { label: 'OptionParentalRating', value: [ItemSortBy.OfficialRating, ItemSortBy.SortName] },
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate } { label: 'OptionReleaseDate', value: [ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName] }
], ],
[LibraryTab.Episodes]: [ [LibraryTab.Episodes]: [
{ label: 'Name', value: ItemSortBy.SeriesSortName }, { label: 'Name', value: [ItemSortBy.SeriesSortName] },
{ label: 'OptionRandom', value: ItemSortBy.Random }, { label: 'OptionRandom', value: [ItemSortBy.Random] },
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating }, { label: 'OptionCommunityRating', value: [ItemSortBy.CommunityRating, ItemSortBy.SortName] },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated }, { label: 'OptionDateAdded', value: [ItemSortBy.DateCreated, ItemSortBy.SortName] },
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate }, { label: 'OptionReleaseDate', value: [ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName] },
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed }, { label: 'OptionDatePlayed', value: [ItemSortBy.DatePlayed, ItemSortBy.SortName] },
{ label: 'OptionParentalRating', value: ItemSortBy.OfficialRating }, { label: 'OptionParentalRating', value: [ItemSortBy.OfficialRating, ItemSortBy.SortName] },
{ label: 'OptionPlayCount', value: ItemSortBy.PlayCount }, { label: 'OptionPlayCount', value: [ItemSortBy.PlayCount, ItemSortBy.SortName] },
{ label: 'Runtime', value: ItemSortBy.Runtime } { label: 'Runtime', value: [ItemSortBy.Runtime, ItemSortBy.SortName] }
], ],
[LibraryTab.Albums]: [ [LibraryTab.Albums]: [
{ label: 'Name', value: ItemSortBy.SortName }, { label: 'Name', value: [ItemSortBy.SortName] },
{ label: 'OptionRandom', value: ItemSortBy.Random }, { label: 'OptionRandom', value: [ItemSortBy.Random] },
{ label: 'AlbumArtist', value: ItemSortBy.AlbumArtist }, { label: 'AlbumArtist', value: [ItemSortBy.AlbumArtist, ItemSortBy.SortName] },
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating }, { label: 'OptionCommunityRating', value: [ItemSortBy.CommunityRating, ItemSortBy.SortName] },
{ label: 'OptionCriticRating', value: ItemSortBy.CriticRating }, { label: 'OptionCriticRating', value: [ItemSortBy.CriticRating, ItemSortBy.SortName] },
{ label: 'OptionReleaseDate', value: ItemSortBy.ProductionYear }, { label: 'OptionReleaseDate', value: [ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName] },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated } { label: 'OptionDateAdded', value: [ItemSortBy.DateCreated, ItemSortBy.SortName] }
], ],
[LibraryTab.Books]: [ [LibraryTab.Books]: [
{ label: 'Name', value: ItemSortBy.SortName }, { label: 'Name', value: [ItemSortBy.SortName] },
{ label: 'OptionRandom', value: ItemSortBy.Random }, { label: 'OptionRandom', value: [ItemSortBy.Random] },
{ label: 'OptionReleaseDate', value: ItemSortBy.ProductionYear }, { label: 'OptionReleaseDate', value: [ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName] },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated }, { label: 'OptionDateAdded', value: [ItemSortBy.DateCreated, ItemSortBy.SortName] },
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed }, { label: 'OptionDatePlayed', value: [ItemSortBy.DatePlayed, ItemSortBy.SortName] },
{ label: 'IndexNumber', value: ItemSortBy.IndexNumber } { label: 'IndexNumber', value: [ItemSortBy.IndexNumber, ItemSortBy.SortName] }
], ],
[LibraryTab.Songs]: [ [LibraryTab.Songs]: [
{ label: 'Name', value: ItemSortBy.SortName }, { label: 'OptionTrackName', value: [ItemSortBy.Name] },
{ label: 'OptionRandom', value: ItemSortBy.Random }, { label: 'OptionRandom', value: [ItemSortBy.Random] },
{ label: 'Album', value: ItemSortBy.Album }, { label: 'Album', value: [ItemSortBy.Album, ItemSortBy.AlbumArtist, ItemSortBy.SortName] },
{ label: 'AlbumArtist', value: ItemSortBy.AlbumArtist }, { label: 'AlbumArtist', value: [ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName] },
{ label: 'Artist', value: ItemSortBy.Artist }, { label: 'Artist', value: [ItemSortBy.Artist, ItemSortBy.Album, ItemSortBy.SortName] },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated }, { label: 'OptionDateAdded', value: [ItemSortBy.DateCreated, ItemSortBy.SortName] },
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed }, { label: 'OptionDatePlayed', value: [ItemSortBy.DatePlayed, ItemSortBy.SortName] },
{ label: 'OptionPlayCount', value: ItemSortBy.PlayCount }, { label: 'OptionPlayCount', value: [ItemSortBy.PlayCount, ItemSortBy.SortName] },
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate }, { label: 'OptionReleaseDate', value: [ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName] },
{ label: 'Runtime', value: ItemSortBy.Runtime } { label: 'Runtime', value: [ItemSortBy.Runtime, ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName] }
], ],
[LibraryTab.Playlists]: [ [LibraryTab.Playlists]: [
{ label: 'Name', value: ItemSortBy.SortName }, { label: 'Name', value: [ItemSortBy.SortName] },
{ label: 'OptionRandom', value: ItemSortBy.Random }, { label: 'OptionRandom', value: [ItemSortBy.Random] },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated }, { label: 'OptionDateAdded', value: [ItemSortBy.DateCreated, ItemSortBy.SortName] },
{ label: 'OptionDatePlaylistUpdated', value: ItemSortBy.DateLastContentAdded }, { label: 'OptionDatePlaylistUpdated', value: [ItemSortBy.DateLastContentAdded, ItemSortBy.SortName] },
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate }, { label: 'OptionReleaseDate', value: [ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName] },
{ label: 'Runtime', value: ItemSortBy.Runtime } { label: 'Runtime', value: [ItemSortBy.Runtime, ItemSortBy.SortName] }
], ],
[LibraryTab.PhotoAlbums]: photosOrPhotoAlbumsOptions, [LibraryTab.PhotoAlbums]: photosOrPhotoAlbumsOptions,
[LibraryTab.Photos]: photosOrPhotoAlbumsOptions, [LibraryTab.Photos]: photosOrPhotoAlbumsOptions,
[LibraryTab.Videos]:videoOrMusicVideoOptions, [LibraryTab.Videos]:videoOrMusicVideoOptions,
[LibraryTab.MusicVideos]:videoOrMusicVideoOptions, [LibraryTab.MusicVideos]:videoOrMusicVideoOptions,
[LibraryTab.Folders]: [ [LibraryTab.Folders]: [
{ label: 'Name', value: ItemSortBy.SortName }, { label: 'Name', value: [ItemSortBy.SortName] },
{ label: 'OptionRandom', value: ItemSortBy.Random }, { label: 'OptionRandom', value: [ItemSortBy.Random] },
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating }, { label: 'OptionCommunityRating', value: [ItemSortBy.CommunityRating, ItemSortBy.SortName] },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated }, { label: 'OptionDateAdded', value: [ItemSortBy.DateCreated, ItemSortBy.SortName] },
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed }, { label: 'OptionDatePlayed', value: [ItemSortBy.DatePlayed, ItemSortBy.SortName] },
{ label: 'Folders', value: ItemSortBy.IsFolder }, { label: 'Folders', value: [ItemSortBy.IsFolder, ItemSortBy.SortName] },
{ label: 'OptionParentalRating', value: ItemSortBy.OfficialRating }, { label: 'OptionParentalRating', value: [ItemSortBy.OfficialRating, ItemSortBy.SortName] },
{ label: 'OptionPlayCount', value: ItemSortBy.PlayCount }, { label: 'OptionPlayCount', value: [ItemSortBy.PlayCount, ItemSortBy.SortName] },
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate }, { label: 'OptionReleaseDate', value: [ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName] },
{ label: 'Runtime', value: ItemSortBy.Runtime } { label: 'Runtime', value: [ItemSortBy.Runtime, ItemSortBy.SortName] }
], ],
[LibraryTab.Mixed]: [ [LibraryTab.Mixed]: [
{ label: 'Name', value: ItemSortBy.SortName }, { label: 'Name', value: [ItemSortBy.SortName] },
{ label: 'OptionRandom', value: ItemSortBy.Random }, { label: 'OptionRandom', value: [ItemSortBy.Random] },
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating }, { label: 'OptionCommunityRating', value: [ItemSortBy.CommunityRating, ItemSortBy.SortName] },
{ label: 'OptionCriticRating', value: ItemSortBy.CriticRating }, { label: 'OptionCriticRating', value: [ItemSortBy.CriticRating, ItemSortBy.SortName] },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated }, { label: 'OptionDateAdded', value: [ItemSortBy.DateCreated, ItemSortBy.SortName] },
{ label: 'OptionDateEpisodeAdded', value: ItemSortBy.DateLastContentAdded }, { label: 'OptionDateEpisodeAdded', value: [ItemSortBy.DateLastContentAdded, ItemSortBy.SortName] },
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed }, { label: 'OptionDatePlayed', value: [ItemSortBy.DatePlayed, ItemSortBy.SortName] },
{ label: 'OptionParentalRating', value: ItemSortBy.OfficialRating }, { label: 'OptionParentalRating', value: [ItemSortBy.OfficialRating, ItemSortBy.SortName] },
{ label: 'OptionPlayCount', value: ItemSortBy.PlayCount }, { label: 'OptionPlayCount', value: [ItemSortBy.PlayCount, ItemSortBy.SortName] },
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate }, { label: 'OptionReleaseDate', value: [ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName] },
{ label: 'Runtime', value: ItemSortBy.Runtime } { label: 'Runtime', value: [ItemSortBy.Runtime, ItemSortBy.SortName] }
] ]
}; };
@@ -186,11 +187,11 @@ const SortButton: FC<SortButtonProps> = ({
}, []); }, []);
const onMenuItemClick = useCallback( const onMenuItemClick = useCallback(
(sortBy: ItemSortBy) => { (sortBy: ItemSortBy[]) => {
setLibraryViewSettings((prevState) => { setLibraryViewSettings((prevState) => {
let sortOrder: SortOrder = SortOrder.Ascending; let sortOrder: SortOrder = SortOrder.Ascending;
// If the user clicks the currently selected sort option, toggle the sort order // If the user clicks the currently selected sort option, toggle the sort order
if (prevState.SortBy === sortBy) { if (isEqual(prevState.SortBy, sortBy)) {
sortOrder = prevState.SortOrder === SortOrder.Ascending ? SortOrder.Descending : SortOrder.Ascending; sortOrder = prevState.SortOrder === SortOrder.Ascending ? SortOrder.Descending : SortOrder.Ascending;
} }
@@ -235,7 +236,7 @@ const SortButton: FC<SortButtonProps> = ({
{sortMenuOptions {sortMenuOptions
.map((option) => ( .map((option) => (
<MenuItem <MenuItem
key={option.value} key={option.value.join(',')}
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => onMenuItemClick(option.value)} onClick={() => onMenuItemClick(option.value)}
> >
@@ -243,7 +244,7 @@ const SortButton: FC<SortButtonProps> = ({
{globalize.translate(option.label)} {globalize.translate(option.label)}
</ListItemText> </ListItemText>
<ListItemIcon sx={{ justifyContent: 'flex-end' }}> <ListItemIcon sx={{ justifyContent: 'flex-end' }}>
{libraryViewSettings.SortBy === option.value && ( {isEqual(libraryViewSettings.SortBy, option.value) && (
libraryViewSettings.SortOrder === SortOrder.Ascending ? ( libraryViewSettings.SortOrder === SortOrder.Ascending ? (
<ArrowUpward fontSize='small' /> <ArrowUpward fontSize='small' />
) : ( ) : (
@@ -1,13 +1,11 @@
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
import type { RecommendationDto } from '@jellyfin/sdk/lib/generated-client/models/recommendation-dto'; import type { RecommendationDto } from '@jellyfin/sdk/lib/generated-client/models/recommendation-dto';
import { RecommendationType } from '@jellyfin/sdk/lib/generated-client/models/recommendation-type'; import { RecommendationType } from '@jellyfin/sdk/lib/generated-client/models/recommendation-type';
import React, { type FC } from 'react'; import React, { type FC } from 'react';
import { CardShape } from 'components/cardbuilder/utils/shape'; import { CardShape } from 'components/cardbuilder/utils/shape';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { import { useGetSuggestionSectionsWithItems } from 'hooks/useFetchItems';
useGetMovieRecommendations,
useGetSuggestionSectionsWithItems
} from 'hooks/useFetchItems';
import { appRouter } from 'components/router/appRouter'; import { appRouter } from 'components/router/appRouter';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
import Loading from 'components/loading/LoadingComponent'; import Loading from 'components/loading/LoadingComponent';
@@ -17,6 +15,8 @@ import type { ParentId } from 'types/library';
import type { Section, SectionType } from 'types/sections'; import type { Section, SectionType } from 'types/sections';
import type { ItemDto } from 'types/base/models/item-dto'; import type { ItemDto } from 'types/base/models/item-dto';
import { useMovieRecommendations } from '../hooks/api/useMovieRecommendations';
interface SuggestionsSectionViewProps { interface SuggestionsSectionViewProps {
parentId: ParentId; parentId: ParentId;
sectionType: SectionType[]; sectionType: SectionType[];
@@ -35,7 +35,15 @@ const SuggestionsSectionView: FC<SuggestionsSectionViewProps> = ({
const { const {
isLoading: isRecommendationsLoading, isLoading: isRecommendationsLoading,
data: movieRecommendationsItems data: movieRecommendationsItems
} = useGetMovieRecommendations(isMovieRecommendationEnabled, parentId); } = useMovieRecommendations({
parentId: parentId || undefined,
fields: [
ItemFields.PrimaryImageAspectRatio,
ItemFields.MediaSourceCount
],
categoryLimit: 6,
itemLimit: 20
}, isMovieRecommendationEnabled);
if (isLoading || isRecommendationsLoading) { if (isLoading || isRecommendationsLoading) {
return <Loading />; return <Loading />;
@@ -15,9 +15,10 @@ import Badge from '@mui/material/Badge';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { useGetQueryFiltersLegacy, useGetStudios } from 'hooks/useFetchItems'; import { useGetQueryFilters, useGetQueryFiltersLegacy, useGetStudios } from 'hooks/useFetchItems';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
import FiltersAudioLanguages from './FiltersAudioLanguages';
import FiltersFeatures from './FiltersFeatures'; import FiltersFeatures from './FiltersFeatures';
import FiltersGenres from './FiltersGenres'; import FiltersGenres from './FiltersGenres';
import FiltersOfficialRatings from './FiltersOfficialRatings'; import FiltersOfficialRatings from './FiltersOfficialRatings';
@@ -25,6 +26,7 @@ import FiltersEpisodesStatus from './FiltersEpisodesStatus';
import FiltersSeriesStatus from './FiltersSeriesStatus'; import FiltersSeriesStatus from './FiltersSeriesStatus';
import FiltersStatus from './FiltersStatus'; import FiltersStatus from './FiltersStatus';
import FiltersStudios from './FiltersStudios'; import FiltersStudios from './FiltersStudios';
import FiltersSubtitleLanguages from './FiltersSubtitleLanguages';
import FiltersTags from './FiltersTags'; import FiltersTags from './FiltersTags';
import FiltersVideoTypes from './FiltersVideoTypes'; import FiltersVideoTypes from './FiltersVideoTypes';
import FiltersYears from './FiltersYears'; import FiltersYears from './FiltersYears';
@@ -99,7 +101,8 @@ const FilterButton: FC<FilterButtonProps> = ({
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const id = open ? 'filter-popover' : undefined; const id = open ? 'filter-popover' : undefined;
const { data } = useGetQueryFiltersLegacy(parentId, itemType); const { data: filtersLegacy } = useGetQueryFiltersLegacy(parentId, itemType);
const { data: filters } = useGetQueryFilters(parentId, itemType);
const { data: studios } = useGetStudios(parentId, itemType); const { data: studios } = useGetStudios(parentId, itemType);
const handleChange = const handleChange =
@@ -173,6 +176,13 @@ const FilterButton: FC<FilterButtonProps> = ({
return viewType === LibraryTab.Episodes; return viewType === LibraryTab.Episodes;
}; };
const isFiltersLanguagesEnabled = () => {
return viewType === LibraryTab.Movies
|| viewType === LibraryTab.Series
|| viewType === LibraryTab.Episodes
|| viewType === LibraryTab.Mixed;
};
return ( return (
<> <>
<Button <Button
@@ -324,7 +334,7 @@ const FilterButton: FC<FilterButtonProps> = ({
{isFiltersLegacyEnabled() && ( {isFiltersLegacyEnabled() && (
<> <>
{data?.Genres && data?.Genres?.length > 0 && ( {filtersLegacy?.Genres && filtersLegacy?.Genres?.length > 0 && (
<Accordion <Accordion
expanded={expanded === 'filtersGenres'} expanded={expanded === 'filtersGenres'}
onChange={handleChange('filtersGenres')} onChange={handleChange('filtersGenres')}
@@ -339,7 +349,7 @@ const FilterButton: FC<FilterButtonProps> = ({
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<FiltersGenres <FiltersGenres
genresOptions={data.Genres} genresOptions={filtersLegacy.Genres}
libraryViewSettings={ libraryViewSettings={
libraryViewSettings libraryViewSettings
} }
@@ -351,8 +361,8 @@ const FilterButton: FC<FilterButtonProps> = ({
</Accordion> </Accordion>
)} )}
{data?.OfficialRatings {filtersLegacy?.OfficialRatings
&& data?.OfficialRatings?.length > 0 && ( && filtersLegacy?.OfficialRatings?.length > 0 && (
<Accordion <Accordion
expanded={ expanded={
expanded === 'filtersOfficialRatings' expanded === 'filtersOfficialRatings'
@@ -373,7 +383,7 @@ const FilterButton: FC<FilterButtonProps> = ({
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<FiltersOfficialRatings <FiltersOfficialRatings
OfficialRatingsOptions={data.OfficialRatings} OfficialRatingsOptions={filtersLegacy.OfficialRatings}
libraryViewSettings={ libraryViewSettings={
libraryViewSettings libraryViewSettings
} }
@@ -385,7 +395,7 @@ const FilterButton: FC<FilterButtonProps> = ({
</Accordion> </Accordion>
)} )}
{data?.Tags && data?.Tags.length > 0 && ( {filtersLegacy?.Tags && filtersLegacy?.Tags.length > 0 && (
<Accordion <Accordion
expanded={expanded === 'filtersTags'} expanded={expanded === 'filtersTags'}
onChange={handleChange('filtersTags')} onChange={handleChange('filtersTags')}
@@ -400,7 +410,7 @@ const FilterButton: FC<FilterButtonProps> = ({
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<FiltersTags <FiltersTags
tagsOptions={data.Tags} tagsOptions={filtersLegacy.Tags}
libraryViewSettings={ libraryViewSettings={
libraryViewSettings libraryViewSettings
} }
@@ -412,7 +422,7 @@ const FilterButton: FC<FilterButtonProps> = ({
</Accordion> </Accordion>
)} )}
{data?.Years && data?.Years?.length > 0 && ( {filtersLegacy?.Years && filtersLegacy?.Years?.length > 0 && (
<Accordion <Accordion
expanded={expanded === 'filtersYears'} expanded={expanded === 'filtersYears'}
onChange={handleChange('filtersYears')} onChange={handleChange('filtersYears')}
@@ -427,7 +437,7 @@ const FilterButton: FC<FilterButtonProps> = ({
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<FiltersYears <FiltersYears
yearsOptions={data.Years} yearsOptions={filtersLegacy.Years}
libraryViewSettings={ libraryViewSettings={
libraryViewSettings libraryViewSettings
} }
@@ -464,6 +474,50 @@ const FilterButton: FC<FilterButtonProps> = ({
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
)} )}
{isFiltersLanguagesEnabled() && !!filters?.AudioLanguages?.length && (
<Accordion
expanded={expanded === 'filtersAudioLanguages'}
onChange={handleChange('filtersAudioLanguages')}
>
<AccordionSummary
aria-controls='audioLanguages-content'
id='filtersAudioLanguages-header'
>
<Typography>
{globalize.translate('AudioLanguages')}
</Typography>
</AccordionSummary>
<AccordionDetails>
<FiltersAudioLanguages
options={filters.AudioLanguages}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={setLibraryViewSettings}
/>
</AccordionDetails>
</Accordion>
)}
{isFiltersLanguagesEnabled() && !!filters?.SubtitleLanguages?.length && (
<Accordion
expanded={expanded === 'filtersSubtitleLanguages'}
onChange={handleChange('filtersSubtitleLanguages')}
>
<AccordionSummary
aria-controls='subtitleLanguages-content'
id='filtersSubtitleLanguages-header'
>
<Typography>
{globalize.translate('SubtitleLanguages')}
</Typography>
</AccordionSummary>
<AccordionDetails>
<FiltersSubtitleLanguages
options={filters.SubtitleLanguages}
libraryViewSettings={libraryViewSettings}
setLibraryViewSettings={setLibraryViewSettings}
/>
</AccordionDetails>
</Accordion>
)}
<Button <Button
disabled={!hasFilters} disabled={!hasFilters}
title={globalize.translate('ResetFilters')} title={globalize.translate('ResetFilters')}
@@ -0,0 +1,59 @@
import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormGroup from '@mui/material/FormGroup';
import React, { FC, useCallback } from 'react';
import { LibraryViewSettings } from 'types/library';
interface FiltersAudioLanguagesProps {
options: { Name?: string | null, Value?: string | null }[];
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
}
const FiltersAudioLanguages: FC<FiltersAudioLanguagesProps> = ({
options,
libraryViewSettings,
setLibraryViewSettings
}) => {
const onFiltersChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const value = event.target.value;
const existingValues = libraryViewSettings?.Filters?.AudioLanguages ?? [];
const updatedValues = existingValues.includes(value) ?
existingValues.filter((filter) => filter !== value) :
[...existingValues, value];
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: {
...prevState.Filters,
AudioLanguages: updatedValues.length ? updatedValues : undefined
}
}));
},
[setLibraryViewSettings, libraryViewSettings?.Filters?.AudioLanguages]
);
return (
<FormGroup>
{options.filter((filter) => filter?.Value != null).map((filter) => (
<FormControlLabel
key={filter.Value}
control={
<Checkbox
checked={!!libraryViewSettings?.Filters?.AudioLanguages?.includes(filter.Value!)}
onChange={onFiltersChange}
value={filter.Value}
/>
}
label={filter.Name}
/>
))}
</FormGroup>
);
};
export default FiltersAudioLanguages;
@@ -0,0 +1,59 @@
import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormGroup from '@mui/material/FormGroup';
import React, { FC, useCallback } from 'react';
import { LibraryViewSettings } from 'types/library';
interface FiltersSubtitleLanguagesProps {
options: { Name?: string | null, Value?: string | null }[];
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
}
const FiltersSubtitleLanguages: FC<FiltersSubtitleLanguagesProps> = ({
options,
libraryViewSettings,
setLibraryViewSettings
}) => {
const onFiltersChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const value = event.target.value;
const existingValues = libraryViewSettings?.Filters?.SubtitleLanguages ?? [];
const updatedValues = existingValues.includes(value) ?
existingValues.filter((filter) => filter !== value) :
[...existingValues, value];
setLibraryViewSettings((prevState) => ({
...prevState,
StartIndex: 0,
Filters: {
...prevState.Filters,
SubtitleLanguages: updatedValues.length ? updatedValues : undefined
}
}));
},
[setLibraryViewSettings, libraryViewSettings?.Filters?.SubtitleLanguages]
);
return (
<FormGroup>
{options.filter((filter) => filter?.Value != null).map((filter) => (
<FormControlLabel
key={filter.Value}
control={
<Checkbox
checked={!!libraryViewSettings?.Filters?.SubtitleLanguages?.includes(filter.Value!)}
onChange={onFiltersChange}
value={filter.Value}
/>
}
label={filter.Name}
/>
))}
</FormGroup>
);
};
export default FiltersSubtitleLanguages;
@@ -0,0 +1,43 @@
import type { Api } from '@jellyfin/sdk/lib/api';
import type { MovieApiGetMovieRecommendationsRequest } from '@jellyfin/sdk/lib/generated-client/api/movie-api';
import { getMovieApi } from '@jellyfin/sdk/lib/utils/api/movie-api';
import { queryOptions, useQuery } from '@tanstack/react-query';
import type { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
const fetchGetMovieRecommendations = async (
api: Api,
params: MovieApiGetMovieRecommendationsRequest,
options?: AxiosRequestConfig
) => {
const response = await getMovieApi(api).getMovieRecommendations(params, options);
return response.data;
};
/** Query options for fetching movie recommendations. */
export const getMovieRecommendationsQuery = (
api?: Api,
params: MovieApiGetMovieRecommendationsRequest = {},
enabled = true
) => queryOptions({
queryKey: ['MovieRecommendations', params?.parentId],
queryFn: ({ signal }) => fetchGetMovieRecommendations(api!, params, { signal }),
enabled: !!api && enabled
});
/** Hook for fetching movie recommendations. */
export const useMovieRecommendations = (
params?: MovieApiGetMovieRecommendationsRequest,
enabled?: boolean
) => {
const { api, user } = useApi();
return useQuery(getMovieRecommendationsQuery(
api,
{
...params,
userId: params?.userId || user?.Id
},
enabled
));
};
@@ -35,7 +35,7 @@ export const useLibrary = () => useContext(LibraryContext);
export const LibraryProvider: FC<PropsWithChildren<unknown>> = ({ children }) => { export const LibraryProvider: FC<PropsWithChildren<unknown>> = ({ children }) => {
const { pathname } = useLocation(); const { pathname } = useLocation();
const { libraryId, activeTab } = useCurrentTab(); const { libraryId, activeTab, settingsKey } = useCurrentTab();
const route = useMemo(() => LibraryRoutes.find(({ path }) => path === pathname), [pathname]); const route = useMemo(() => LibraryRoutes.find(({ path }) => path === pathname), [pathname]);
const collectionType = route?.type; const collectionType = route?.type;
@@ -48,7 +48,7 @@ export const LibraryProvider: FC<PropsWithChildren<unknown>> = ({ children }) =>
// Local storage requires the view type to be known upfront so default to movies if unknown // Local storage requires the view type to be known upfront so default to movies if unknown
const settingsViewType = viewType ?? LibraryTab.Movies; const settingsViewType = viewType ?? LibraryTab.Movies;
const [viewSettings, setViewSettings] = useLocalStorage<LibraryViewSettings>( const [viewSettings, setViewSettings] = useLocalStorage<LibraryViewSettings>(
getSettingsKey(settingsViewType, libraryId), getSettingsKey(settingsViewType, settingsKey),
getDefaultLibraryViewSettings(settingsViewType) getDefaultLibraryViewSettings(settingsViewType)
); );
@@ -6,12 +6,12 @@ import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'
import { type ParentId, ViewMode, type LibraryViewSettings } from 'types/library'; import { type ParentId, ViewMode, type LibraryViewSettings } from 'types/library';
import { LibraryTab } from 'types/libraryTab'; import { LibraryTab } from 'types/libraryTab';
export const getDefaultSortBy = (viewType: LibraryTab) => { export const getDefaultSortBy = (viewType: LibraryTab): ItemSortBy[] => {
if (viewType === LibraryTab.Episodes) { if (viewType === LibraryTab.Episodes) {
return ItemSortBy.SeriesSortName; return [ItemSortBy.SeriesSortName];
} }
return ItemSortBy.SortName; return [ItemSortBy.SortName];
}; };
export const getDefaultLibraryViewSettings = (viewType: LibraryTab): LibraryViewSettings => { export const getDefaultLibraryViewSettings = (viewType: LibraryTab): LibraryViewSettings => {
@@ -1 +1,2 @@
export * from './public';
export * from './user'; export * from './user';
@@ -0,0 +1,5 @@
import { AsyncRoute } from 'components/router/AsyncRoute';
export const ASYNC_PUBLIC_ROUTES: AsyncRoute[] = [
{ path: 'forgotpassword', page: 'session/forgotPassword' }
];
@@ -22,13 +22,6 @@ export const LEGACY_PUBLIC_ROUTES: LegacyRoute[] = [
view: 'session/login/index.html' view: 'session/login/index.html'
} }
}, },
{
path: 'forgotpassword',
pageProps: {
controller: 'session/forgotPassword/index',
view: 'session/forgotPassword/index.html'
}
},
{ {
path: 'forgotpasswordpin', path: 'forgotpasswordpin',
pageProps: { pageProps: {
+2 -1
View File
@@ -7,7 +7,7 @@ import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
import ErrorBoundary from 'components/router/ErrorBoundary'; import ErrorBoundary from 'components/router/ErrorBoundary';
import FallbackRoute from 'components/router/FallbackRoute'; import FallbackRoute from 'components/router/FallbackRoute';
import { ASYNC_USER_ROUTES } from './asyncRoutes'; import { ASYNC_PUBLIC_ROUTES, ASYNC_USER_ROUTES } from './asyncRoutes';
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes'; import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
import VideoPage from './video'; import VideoPage from './video';
@@ -38,6 +38,7 @@ export const APP_ROUTES: RouteObject[] = [
/* Public routes */ /* Public routes */
element: <ConnectionRequired level='public' />, element: <ConnectionRequired level='public' />,
children: [ children: [
...ASYNC_PUBLIC_ROUTES.map(toAsyncPageRoute),
...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute), ...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute),
/* Fallback route for invalid paths */ /* Fallback route for invalid paths */
+1
View File
@@ -70,6 +70,7 @@ const VideoPage: FC = () => {
<RemotePlayButton /> <RemotePlayButton />
</> </>
} }
className='padded-left padded-right'
> >
<Typography>{videoTitle}</Typography> <Typography>{videoTitle}</Typography>
</AppToolbar> </AppToolbar>
@@ -1,4 +1,4 @@
import React, { type FC } from 'react'; import React, { type FC, type PropsWithChildren } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import useCardText from './useCardText'; import useCardText from './useCardText';
import layoutManager from 'components/layoutManager'; import layoutManager from 'components/layoutManager';
@@ -21,6 +21,28 @@ const shouldShowDetailsMenu = (
); );
}; };
interface CardFooterWrapperProps {
cardOptions: CardOptions;
footerClass?: string;
isOuterFooter: boolean;
logoUrl?: string;
}
/** Component that conditionally wraps the footer content based on the card options and presence of a logo. */
const CardFooterWrapper: FC<PropsWithChildren<CardFooterWrapperProps>> = ({
cardOptions,
footerClass,
isOuterFooter,
logoUrl,
children
}) => (
(!isOuterFooter || logoUrl || cardOptions.cardLayout) ? (
<Box className={footerClass}>
{children}
</Box>
) : children
);
interface LogoComponentProps { interface LogoComponentProps {
logoUrl: string; logoUrl: string;
} }
@@ -72,7 +94,12 @@ const CardFooterText: FC<CardFooterTextProps> = ({
}); });
return ( return (
<Box className={footerClass}> <CardFooterWrapper
cardOptions={cardOptions}
footerClass={footerClass}
isOuterFooter={isOuterFooter}
logoUrl={logoUrl}
>
{logoUrl && <LogoComponent logoUrl={logoUrl} />} {logoUrl && <LogoComponent logoUrl={logoUrl} />}
{shouldShowDetailsMenu(cardOptions, isOuterFooter) && ( {shouldShowDetailsMenu(cardOptions, isOuterFooter) && (
<MoreVertIconButton className='itemAction btnCardOptions' /> <MoreVertIconButton className='itemAction btnCardOptions' />
@@ -81,7 +108,7 @@ const CardFooterText: FC<CardFooterTextProps> = ({
{cardTextLines} {cardTextLines}
{progressBar} {progressBar}
</Box> </CardFooterWrapper>
); );
}; };
+6 -1
View File
@@ -299,7 +299,7 @@ button::-moz-focus-inner {
} }
.cardText { .cardText {
padding: 0.06em 0.5em; padding: 0.06em 2px; // 2px horizontal padding to avoid touching multiselect border
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -448,6 +448,11 @@ button::-moz-focus-inner {
z-index: 1; z-index: 1;
padding: 0.75em; padding: 0.75em;
font-size: 88%; font-size: 88%;
/* The "modern" layout needs additional specificity to prevent MUI styles from overriding */
&.paper-icon-button-light {
color: rgba(255, 255, 255, 0.76);
}
} }
.cardOverlayButton-br { .cardOverlayButton-br {

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