mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2026-09-02 19:43:13 +03:00
Merge remote-tracking branch 'upstream/master' into libbitsub
This commit is contained in:
@@ -11,7 +11,7 @@ jobs:
|
||||
name: Merge conflict labeling 🏷️
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
|
||||
- uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0
|
||||
with:
|
||||
dirtyLabel: 'merge conflict'
|
||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
||||
|
||||
@@ -20,21 +20,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository ⬇️
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ inputs.commit }}
|
||||
show-progress: false
|
||||
|
||||
- name: Initialize CodeQL 🛠️
|
||||
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
queries: security-and-quality
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild 📦
|
||||
uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
|
||||
- name: Perform CodeQL Analysis 🧪
|
||||
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
name: CloudFlare Pages 📃
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: ${{ inputs.branch == 'jellyfin/jellyfin-web/master' && 'Production' || 'Preview' }}
|
||||
name: ${{ inputs.branch == github.event.repository.default_branch && 'Production' || 'Preview' }}
|
||||
url: ${{ steps.cf.outputs.deployment-url }}
|
||||
outputs:
|
||||
url: ${{ steps.cf.outputs.deployment-url }}
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Publish to Cloudflare Pages 📃
|
||||
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
|
||||
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
|
||||
id: cf
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
@@ -14,12 +14,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ inputs.commit || github.sha }}
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
mv dist/config.tmp.json dist/config.json
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: frontend
|
||||
path: dist
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ inputs.commit }}
|
||||
show-progress: false
|
||||
@@ -42,13 +42,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout ⬇️
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ inputs.commit }}
|
||||
show-progress: false
|
||||
|
||||
- name: Setup node environment ⚙️
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
|
||||
@@ -46,12 +46,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
@@ -61,6 +61,6 @@ jobs:
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run eslint
|
||||
uses: CatChen/eslint-suggestion-action@efbe3942e76df120000621539d93ce7e0b54f7a7 # v4.1.27
|
||||
uses: CatChen/eslint-suggestion-action@2b07b898c0dd5a016586d22b54282199b79cd358 # v4.1.33
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
with:
|
||||
branch: ${{ format('{0}/{1}', github.repository, github.ref_name) }}
|
||||
branch: ${{ github.ref_name }}
|
||||
commit: ${{ github.sha }}
|
||||
workflow_run_id: ${{ github.run_id }}
|
||||
comment: false
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
operations-per-run: 75
|
||||
|
||||
+11
-10
@@ -12,6 +12,7 @@ If you have any questions, please join one of our [development chat rooms](https
|
||||
|
||||
* New files **MUST** be written in TypeScript.
|
||||
* API interactions **MUST** be made using the Jellyfin TypeScript SDK.
|
||||
* New app pages **MUST** be written using React components using `src/components/Page.tsx` as the base component.
|
||||
* **SHOULD** be covered by unit tests when possible (legacy component/view code is deemed untestable).
|
||||
* **SHOULD** avoid whitespace only changes in unchanged sections of code.
|
||||
* **SHOULD NOT** overuse dynamic imports. We use dynamic imports at the page level; otherwise we should let our build tooling deal with code-splitting for the best bundle sizes.
|
||||
@@ -30,6 +31,7 @@ If you have any questions, please join one of our [development chat rooms](https
|
||||
* **SHOULD NOT** rebase once reviews are in progress.
|
||||
* **MUST** follow the [LLM development policy](https://jellyfin.org/docs/general/contributing/llm-policies).
|
||||
* **MUST** test that the change works as expected before marking a PR as ready for review.
|
||||
* **MUST** fully complete the PR template. Failing to do so will result in the PR being closed as invalid without review.
|
||||
* **SHOULD** represent a singular focus (i.e. a PR to fix a bug should not include unrelated refactoring).
|
||||
* **SHOULD NOT** update from `master` needlessly once opened (only update if conflicts exist).
|
||||
|
||||
@@ -51,7 +53,7 @@ You may be asked to update your Pull Request to target a release branch as part
|
||||
* [React](https://react.dev/reference/react) — User Interface library
|
||||
* [React Router](https://reactrouter.com/) — Routing library
|
||||
* [TanStack Query](https://tanstack.com/query/latest/docs/framework/react/overview) — State management library for server data
|
||||
* [MUI](https://mui.com/material-ui/getting-started/) components (in Dashboard and Experimental layouts) — UI component library
|
||||
* [MUI](https://mui.com/material-ui/getting-started/) components (in Dashboard and Modern layouts) — UI component library
|
||||
* [Webpack](https://webpack.js.org/concepts/) — Bundler / build tooling
|
||||
* [Vitest](https://vitest.dev/api/) — Test library
|
||||
|
||||
@@ -59,7 +61,7 @@ You may be asked to update your Pull Request to target a release branch as part
|
||||
|
||||
| Library | Replacement |
|
||||
| --- | --- |
|
||||
| [Emby WebComponents](./src/elements) | MUI components (Dashboard + Experimental apps ONLY; Untested on TVs) |
|
||||
| [Emby WebComponents](./src/elements) | MUI components (Dashboard + Modern apps ONLY; Untested on TVs) |
|
||||
| [App Router](./src/components/router/appRouter.js) | React Router |
|
||||
| [View Manager](./src/components/viewManager) | React Router |
|
||||
| [Jellyfin ApiClient](https://github.com/jellyfin-archive/jellyfin-apiclient-javascript) | Jellyfin TypeScript SDK |
|
||||
@@ -73,13 +75,13 @@ The official list of supported browser versions can be found in the `browserlist
|
||||
|
||||
## Application Components
|
||||
|
||||
* Stable App `src/apps/stable`
|
||||
* Modern App `src/apps/modern`
|
||||
* Ongoing rewrite of the main user interface using MUI components
|
||||
* Currently reuses content from the Legacy App to maintain parity
|
||||
* Does not currently support TV layout!
|
||||
* Legacy App `src/apps/legacy`
|
||||
* Main user application
|
||||
* Supports TV layout
|
||||
* Experimental App `src/apps/experimental`
|
||||
* Ongoing rewrite of the Stable App using MUI components
|
||||
* Currently reuses (most) pages from the Stable App to maintain parity
|
||||
* Does not currently support TV layout!
|
||||
* Dashboard App `src/apps/dashboard`
|
||||
* Admin dashboard and metadata editor pages
|
||||
* Almost completely rewritten using MUI components
|
||||
@@ -103,13 +105,12 @@ The official list of supported browser versions can be found in the `browserlist
|
||||
└── src
|
||||
├── apps
|
||||
│ ├── dashboard # Admin dashboard app
|
||||
│ ├── experimental # New experimental app
|
||||
│ ├── stable # Classic (stable) app
|
||||
│ ├── legacy # Legacy app
|
||||
│ ├── modern # New modern (React based) app
|
||||
│ └── wizard # Startup wizard app
|
||||
├── assets # Static assets
|
||||
├── components # Higher order visual components and React components
|
||||
├── constants # Common constant values
|
||||
├── controllers # Legacy page views and controllers 🧹 ❌
|
||||
├── elements # Basic webcomponents and React equivalents 🧹
|
||||
├── hooks # Custom React hooks
|
||||
├── lib # Reusable libraries
|
||||
|
||||
@@ -98,13 +98,17 @@
|
||||
- [Jxiced](https://github.com/Jxiced)
|
||||
- [Derek Huber](https://github.com/Derek4aty1)
|
||||
- [StableCrimson](https://github.com/StableCrimson)
|
||||
- [hazemKrimi](https://github.com/hazemKrimi)
|
||||
- [diegoeche](https://github.com/diegoeche)
|
||||
- [Free O'Toole](https://github.com/freeotoole)
|
||||
- [TheBosZ](https://github.com/thebosz)
|
||||
- [qm3jp](https://github.com/qm3jp)
|
||||
- [johnnyg](https://github.com/johnnyg)
|
||||
- [Lampan-git](https://github.com/Lampan-git)
|
||||
- [lmaotrigine](https://github.com/lmaotrigine)
|
||||
- [bjorntp](https://github.com/bjorntp)
|
||||
- [austinhardaway](https://github.com/austinhardaway)
|
||||
- [shindouj](https://github.com/shindouj)
|
||||
|
||||
## Emby Contributors
|
||||
|
||||
|
||||
+9
-2
@@ -59,8 +59,7 @@ export default tseslint.config(
|
||||
'new-cap': [
|
||||
'error',
|
||||
{
|
||||
'capIsNewExceptions': ['jQuery.Deferred'],
|
||||
'newIsCapExceptionPattern': '\\.default$'
|
||||
'newIsCapExceptionPattern': String.raw`\.default$`
|
||||
}
|
||||
],
|
||||
'no-duplicate-imports': 'error',
|
||||
@@ -71,6 +70,13 @@ export default tseslint.config(
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': ['error', { builtinGlobals: false }],
|
||||
'no-restricted-globals': ['error'].concat(restrictedGlobals),
|
||||
'no-restricted-properties': [
|
||||
'error',
|
||||
{
|
||||
property: 'replaceChildren',
|
||||
message: 'replaceChildren is not supported in all target browsers'
|
||||
}
|
||||
],
|
||||
'no-return-assign': 'error',
|
||||
'no-return-await': 'error',
|
||||
'no-sequences': ['error', { 'allowInParentheses': false }],
|
||||
@@ -103,6 +109,7 @@ export default tseslint.config(
|
||||
'sonarjs/no-inverted-boolean-check': 'error',
|
||||
'sonarjs/no-selector-parameter': 'off',
|
||||
'sonarjs/pseudo-random': 'warn',
|
||||
'sonarjs/aws-restricted-ip-admin-access': 'off',
|
||||
// TODO: Enable the following sonarjs rules and fix issues
|
||||
'sonarjs/no-duplicate-string': 'off',
|
||||
'sonarjs/no-nested-functions': 'warn',
|
||||
|
||||
Generated
+1649
-1365
File diff suppressed because it is too large
Load Diff
+40
-38
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "jellyfin-web",
|
||||
"version": "10.12.0",
|
||||
"version": "12.0.0",
|
||||
"description": "Web interface for Jellyfin",
|
||||
"repository": "https://github.com/jellyfin/jellyfin-web",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/plugin-transform-modules-umd": "7.27.1",
|
||||
"@babel/preset-env": "7.29.2",
|
||||
"@babel/preset-react": "7.28.5",
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "4.7.1",
|
||||
"@babel/core": "7.29.7",
|
||||
"@babel/plugin-transform-modules-umd": "7.29.7",
|
||||
"@babel/preset-env": "7.29.7",
|
||||
"@babel/preset-react": "7.29.7",
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "4.7.2",
|
||||
"@eslint/js": "9.39.4",
|
||||
"@stylistic/eslint-plugin": "4.4.1",
|
||||
"@stylistic/stylelint-plugin": "3.1.3",
|
||||
@@ -18,23 +18,23 @@
|
||||
"@types/loadable__component": "5.13.10",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/markdown-it": "14.1.2",
|
||||
"@types/react": "18.3.28",
|
||||
"@types/react": "18.3.31",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/react-lazy-load-image-component": "1.6.5",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@typescript-eslint/parser": "8.57.0",
|
||||
"@typescript-eslint/parser": "8.62.0",
|
||||
"@uupaa/dynamic-import-polyfill": "1.0.2",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"autoprefixer": "10.4.27",
|
||||
"@vitest/coverage-v8": "3.2.6",
|
||||
"autoprefixer": "10.5.2",
|
||||
"babel-loader": "10.1.1",
|
||||
"caniuse-lite": "1.0.30001781",
|
||||
"caniuse-lite": "1.0.30001799",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"confusing-browser-globals": "1.0.11",
|
||||
"copy-webpack-plugin": "14.0.0",
|
||||
"cross-env": "10.1.0",
|
||||
"css-loader": "7.1.4",
|
||||
"cssnano": "7.1.3",
|
||||
"es-check": "9.6.2",
|
||||
"cssnano": "7.1.9",
|
||||
"es-check": "9.6.4",
|
||||
"eslint": "9.39.4",
|
||||
"eslint-plugin-compat": "6.2.1",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
@@ -45,34 +45,34 @@
|
||||
"expose-loader": "5.0.1",
|
||||
"fast-glob": "3.3.3",
|
||||
"fork-ts-checker-webpack-plugin": "9.1.0",
|
||||
"globals": "17.4.0",
|
||||
"globals": "17.7.0",
|
||||
"html-loader": "5.1.0",
|
||||
"html-webpack-plugin": "5.6.6",
|
||||
"jsdom": "28.1.0",
|
||||
"mini-css-extract-plugin": "2.10.1",
|
||||
"postcss": "8.5.8",
|
||||
"html-webpack-plugin": "5.6.7",
|
||||
"jsdom": "29.1.1",
|
||||
"mini-css-extract-plugin": "2.10.2",
|
||||
"postcss": "8.5.15",
|
||||
"postcss-loader": "8.2.1",
|
||||
"postcss-preset-env": "10.6.1",
|
||||
"postcss-scss": "4.0.9",
|
||||
"sass": "1.98.0",
|
||||
"sass-loader": "16.0.7",
|
||||
"sass": "1.101.0",
|
||||
"sass-loader": "16.0.8",
|
||||
"source-map-loader": "5.0.0",
|
||||
"speed-measure-webpack-plugin": "1.5.0",
|
||||
"speed-measure-webpack-plugin": "1.6.0",
|
||||
"style-loader": "4.0.0",
|
||||
"stylelint": "16.26.1",
|
||||
"stylelint-config-rational-order": "0.1.2",
|
||||
"stylelint-no-browser-hacks": "2.0.0",
|
||||
"stylelint-no-browser-hacks": "2.0.3",
|
||||
"stylelint-order": "7.0.1",
|
||||
"stylelint-scss": "6.14.0",
|
||||
"ts-loader": "9.5.4",
|
||||
"ts-loader": "9.6.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.57.0",
|
||||
"typescript-eslint": "8.62.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "3.2.4",
|
||||
"webpack": "5.105.4",
|
||||
"webpack-bundle-analyzer": "5.2.0",
|
||||
"webpack-cli": "7.0.2",
|
||||
"webpack-dev-server": "5.2.3",
|
||||
"vitest": "3.2.6",
|
||||
"webpack": "5.107.2",
|
||||
"webpack-bundle-analyzer": "5.3.0",
|
||||
"webpack-cli": "7.0.3",
|
||||
"webpack-dev-server": "5.2.5",
|
||||
"webpack-merge": "6.0.1",
|
||||
"worker-loader": "3.0.8"
|
||||
},
|
||||
@@ -86,16 +86,17 @@
|
||||
"@fontsource/noto-sans-sc": "5.2.9",
|
||||
"@fontsource/noto-sans-tc": "5.2.9",
|
||||
"@jellyfin/libass-wasm": "4.2.4",
|
||||
"@jellyfin/sdk": "0.0.0-unstable.202603300541",
|
||||
"@jellyfin/sdk": "0.0.0-unstable.202607010629",
|
||||
"@jellyfin/ux-web": "1.0.0",
|
||||
"@mui/icons-material": "6.5.0",
|
||||
"@mui/material": "6.5.0",
|
||||
"@mui/x-date-pickers": "7.29.4",
|
||||
"@react-hook/resize-observer": "2.0.2",
|
||||
"@tanstack/react-query": "5.91.2",
|
||||
"@tanstack/react-query-devtools": "5.91.3",
|
||||
"@tanstack/react-query": "5.101.1",
|
||||
"@tanstack/react-query-devtools": "5.101.1",
|
||||
"@tanstack/react-query-persist-client": "5.101.1",
|
||||
"abortcontroller-polyfill": "1.7.8",
|
||||
"axios": "1.13.6",
|
||||
"axios": "1.18.1",
|
||||
"blurhash": "2.0.5",
|
||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||
"classnames": "2.5.1",
|
||||
@@ -109,7 +110,8 @@
|
||||
"flv.js": "1.6.2",
|
||||
"headroom.js": "0.12.0",
|
||||
"history": "5.3.0",
|
||||
"hls.js": "1.6.15",
|
||||
"hls.js": "1.6.16",
|
||||
"idb-keyval": "6.2.5",
|
||||
"intersection-observer": "0.12.2",
|
||||
"jellyfin-apiclient": "1.11.0",
|
||||
"jquery": "3.7.1",
|
||||
@@ -117,7 +119,7 @@
|
||||
"libarchive.js": "2.0.2",
|
||||
"libbitsub": "1.10.1",
|
||||
"lodash-es": "4.18.1",
|
||||
"markdown-it": "14.1.1",
|
||||
"markdown-it": "14.2.0",
|
||||
"material-design-icons-iconfont": "6.7.0",
|
||||
"material-react-table": "3.2.1",
|
||||
"native-promise-only": "0.8.1",
|
||||
@@ -127,17 +129,17 @@
|
||||
"react-blurhash": "0.3.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-lazy-load-image-component": "1.6.3",
|
||||
"react-router-dom": "6.30.3",
|
||||
"react-router-dom": "6.30.4",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"screenfull": "6.0.2",
|
||||
"sortablejs": "1.15.7",
|
||||
"swiper": "12.1.2",
|
||||
"swiper": "12.2.0",
|
||||
"usehooks-ts": "3.1.1",
|
||||
"webcomponents.js": "0.7.24",
|
||||
"whatwg-fetch": "3.6.20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sass-embedded": "1.98.0"
|
||||
"sass-embedded": "1.100.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 Firefox versions",
|
||||
|
||||
+12
-4
@@ -1,12 +1,13 @@
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
|
||||
import React from 'react';
|
||||
|
||||
import QueryClientEventHandler from 'components/QueryClientEventHandler';
|
||||
import { ApiProvider } from 'hooks/useApi';
|
||||
import { UserSettingsProvider } from 'hooks/useUserSettings';
|
||||
import { WebConfigProvider } from 'hooks/useWebConfig';
|
||||
import browser from 'scripts/browser';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import { persister, queryClient } from 'utils/query/queryClient';
|
||||
|
||||
import RootAppRouter from 'RootAppRouter';
|
||||
|
||||
@@ -14,10 +15,17 @@ const useReactQueryDevtools = window.Proxy // '@tanstack/query-devtools' require
|
||||
&& !browser.tv; // Don't use devtools on the TV as the navigation is weird
|
||||
|
||||
const RootApp = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PersistQueryClientProvider
|
||||
client={queryClient}
|
||||
persistOptions={{
|
||||
buster: __JF_BUILD_VERSION__,
|
||||
persister
|
||||
}}
|
||||
>
|
||||
<ApiProvider>
|
||||
<UserSettingsProvider>
|
||||
<WebConfigProvider>
|
||||
<QueryClientEventHandler />
|
||||
<RootAppRouter />
|
||||
</WebConfigProvider>
|
||||
</UserSettingsProvider>
|
||||
@@ -25,7 +33,7 @@ const RootApp = () => (
|
||||
{useReactQueryDevtools && (
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
</PersistQueryClientProvider>
|
||||
);
|
||||
|
||||
export default RootApp;
|
||||
|
||||
+5
-10
@@ -8,27 +8,22 @@ import {
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { DASHBOARD_APP_PATHS, DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
||||
import { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/routes/routes';
|
||||
import { STABLE_APP_ROUTES } from 'apps/stable/routes/routes';
|
||||
import { APP_ROUTES as MODERN_APP_ROUTES } from 'apps/modern/routes/routes';
|
||||
import { APP_ROUTES as LEGACY_APP_ROUTES } from 'apps/legacy/routes/routes';
|
||||
import { WIZARD_APP_ROUTES } from 'apps/wizard/routes/routes';
|
||||
import AppHeader from 'components/AppHeader';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
import { SETTING_KEY as LAYOUT_SETTING_KEY } from 'components/layoutManager';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import BangRedirect from 'components/router/BangRedirect';
|
||||
import { createRouterHistory } from 'components/router/routerHistory';
|
||||
import { LayoutMode } from 'constants/layoutMode';
|
||||
import browser from 'scripts/browser';
|
||||
import appTheme from 'themes';
|
||||
import { ThemeStorageManager } from 'themes/themeStorageManager';
|
||||
|
||||
const layoutMode = browser.tv ? LayoutMode.Tv : localStorage.getItem(LAYOUT_SETTING_KEY);
|
||||
const isExperimentalLayout = !layoutMode || layoutMode === LayoutMode.Experimental;
|
||||
|
||||
const router = createHashRouter([
|
||||
{
|
||||
element: <RootAppLayout />,
|
||||
children: [
|
||||
...(isExperimentalLayout ? EXPERIMENTAL_APP_ROUTES : STABLE_APP_ROUTES),
|
||||
...(layoutManager.modern ? MODERN_APP_ROUTES : LEGACY_APP_ROUTES),
|
||||
...DASHBOARD_APP_ROUTES,
|
||||
...WIZARD_APP_ROUTES,
|
||||
{
|
||||
@@ -61,7 +56,7 @@ function RootAppLayout() {
|
||||
storageManager={ThemeStorageManager}
|
||||
>
|
||||
<Backdrop />
|
||||
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
|
||||
<AppHeader isHidden={layoutManager.modern || isNewLayoutPath} />
|
||||
|
||||
<Outlet />
|
||||
</ThemeProvider>
|
||||
|
||||
Vendored
-3
@@ -166,7 +166,6 @@ declare module 'jellyfin-apiclient' {
|
||||
getMusicGenres(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getNamedConfiguration(name: string): Promise<any>;
|
||||
getNetworkDevices(): Promise<any>;
|
||||
getNetworkShares(path: string): Promise<FileSystemEntryInfo[]>;
|
||||
getNewLiveTvTimerDefaults(options?: any): Promise<SeriesTimerInfoDto>;
|
||||
getNextUpEpisodes(options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getNotificationSummary(userId: string): Promise<NotificationsSummaryDto>;
|
||||
@@ -256,7 +255,6 @@ declare module 'jellyfin-apiclient' {
|
||||
requestSyncPlaySetRepeatMode(options?: SetRepeatModeRequestDto): Promise<void>;
|
||||
requestSyncPlaySetShuffleMode(options?: SetShuffleModeRequestDto): Promise<void>;
|
||||
requestSyncPlayUnpause(): Promise<void>;
|
||||
resetEasyPassword(userId: string): Promise<void>;
|
||||
resetLiveTvTuner(id: string): Promise<void>;
|
||||
resetUserPassword(userId: string): Promise<void>;
|
||||
restartServer(): Promise<void>;
|
||||
@@ -281,7 +279,6 @@ declare module 'jellyfin-apiclient' {
|
||||
uninstallPluginByVersion(id: string, version: string): Promise<void>;
|
||||
uninstallPlugin(id: string): Promise<void>;
|
||||
updateDisplayPreferences(id: string, obj: DisplayPreferencesDto, userId: string, app: string): Promise<void>;
|
||||
updateEasyPassword(userId: string, newPassword: string): Promise<void>;
|
||||
updateFavoriteStatus(userId: string, itemId: string, isFavorite: boolean): Promise<UserItemDataDto>;
|
||||
updateItemImageIndex(itemId: string, imageType: ImageType, imageIndex: number, newIndex: number): Promise<any>;
|
||||
updateItem(item: BaseItemDto): Promise<void>;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Box from '@mui/material/Box';
|
||||
import { type Theme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
@@ -8,9 +7,9 @@ import React, { FC, StrictMode, useCallback, useEffect, useState } from 'react';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
|
||||
import AppBody from 'components/AppBody';
|
||||
import OffsetAppBar from 'components/OffsetAppBar';
|
||||
import AppToolbar from 'components/toolbar/AppToolbar';
|
||||
import ServerButton from 'components/toolbar/ServerButton';
|
||||
import ElevationScroll from 'components/ElevationScroll';
|
||||
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import ThemeCss from 'components/ThemeCss';
|
||||
@@ -50,39 +49,44 @@ export const Component: FC = () => {
|
||||
|
||||
return (
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={dateFnsLocale}>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: '100%'
|
||||
}}
|
||||
>
|
||||
<StrictMode>
|
||||
<ElevationScroll elevate={false}>
|
||||
<AppBar
|
||||
position='fixed'
|
||||
sx={{
|
||||
width: {
|
||||
xs: '100%',
|
||||
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
|
||||
},
|
||||
ml: {
|
||||
xs: 0,
|
||||
md: isDrawerAvailable ? DRAWER_WIDTH : 0
|
||||
}
|
||||
}}
|
||||
<OffsetAppBar
|
||||
dense
|
||||
elevation={4}
|
||||
sx={{
|
||||
width: {
|
||||
xs: '100%',
|
||||
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
|
||||
},
|
||||
ml: {
|
||||
xs: 0,
|
||||
md: isDrawerAvailable ? DRAWER_WIDTH : 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AppToolbar
|
||||
isBackButtonAvailable={appRouter.canGoBack()}
|
||||
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
|
||||
isDrawerOpen={isDrawerOpen}
|
||||
onDrawerButtonClick={onToggleDrawer}
|
||||
buttons={
|
||||
<HelpButton />
|
||||
}
|
||||
>
|
||||
<AppToolbar
|
||||
isBackButtonAvailable={appRouter.canGoBack()}
|
||||
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
|
||||
isDrawerOpen={isDrawerOpen}
|
||||
onDrawerButtonClick={onToggleDrawer}
|
||||
buttons={
|
||||
<HelpButton />
|
||||
}
|
||||
>
|
||||
{isMetadataManager && (
|
||||
<ServerButton />
|
||||
)}
|
||||
{isMetadataManager && (
|
||||
<ServerButton />
|
||||
)}
|
||||
|
||||
<AppTabs isDrawerOpen={isDrawerOpen} />
|
||||
</AppToolbar>
|
||||
</AppBar>
|
||||
</ElevationScroll>
|
||||
<AppTabs isDrawerOpen={isDrawerOpen} />
|
||||
</AppToolbar>
|
||||
</OffsetAppBar>
|
||||
|
||||
{
|
||||
isDrawerAvailable && (
|
||||
@@ -98,6 +102,7 @@ export const Component: FC = () => {
|
||||
<Box
|
||||
component='main'
|
||||
sx={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
flexGrow: 1
|
||||
}}
|
||||
|
||||
@@ -19,20 +19,8 @@ $drawer-width: 240px;
|
||||
position: unset !important;
|
||||
}
|
||||
|
||||
// Fix the padding of dashboard pages
|
||||
.content-primary {
|
||||
padding-top: 3.25rem;
|
||||
}
|
||||
// Tabbed pages
|
||||
.withTabs .content-primary {
|
||||
padding-top: 6.5rem;
|
||||
|
||||
@media all and (min-width: $mui-bp-lg) {
|
||||
padding-top: 3.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix the padding of the metadata editor
|
||||
.metadataEditorPage {
|
||||
padding-top: 3.25rem !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import ListItem from '@mui/material/ListItem';
|
||||
import List from '@mui/material/List';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import DrawerHeaderLink from 'apps/experimental/components/drawers/DrawerHeaderLink';
|
||||
import DrawerHeaderLink from 'apps/modern/components/drawers/DrawerHeaderLink';
|
||||
import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';
|
||||
|
||||
import ServerDrawerSection from './sections/ServerDrawerSection';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Extension from '@mui/icons-material/Extension';
|
||||
import Folder from '@mui/icons-material/Folder';
|
||||
import Icon from '@mui/material/Icon';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
@@ -50,8 +50,7 @@ const PluginDrawerSection = () => {
|
||||
to={`/${Dashboard.getPluginUrl(pageInfo.Name)}`}
|
||||
>
|
||||
<ListItemIcon>
|
||||
{/* TODO: Support different icons? */}
|
||||
<Folder />
|
||||
<Icon>{pageInfo.MenuIcon ?? 'folder'}</Icon>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={pageInfo.DisplayName} />
|
||||
</ListItemLink>
|
||||
|
||||
@@ -76,7 +76,8 @@ const ServerInfoWidget = ({
|
||||
sx={{
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
disabled={isScanning}
|
||||
loading={isScanning}
|
||||
loadingPosition='start'
|
||||
>
|
||||
{globalize.translate('ButtonScanAllLibraries')}
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ActivityLogApiGetLogEntriesRequest } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { ActivityLogApiGetLogEntriesRequest } from '@jellyfin/sdk/lib/generated-client/api/activity-log-api';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import type { Api } from '@jellyfin/sdk';
|
||||
import { getActivityLogApi } from '@jellyfin/sdk/lib/utils/api/activity-log-api';
|
||||
@@ -27,6 +27,7 @@ export const useLogEntries = (
|
||||
queryFn: ({ signal }) =>
|
||||
fetchLogEntries(api!, requestParams, { signal }),
|
||||
enabled: !!api,
|
||||
refetchOnMount: false
|
||||
refetchOnMount: false,
|
||||
staleTime: 0 // ensure we load the latest log entries
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DevicesApiGetDevicesRequest } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { DevicesApiGetDevicesRequest } from '@jellyfin/sdk/lib/generated-client/api/devices-api';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import type { Api } from '@jellyfin/sdk';
|
||||
import { getDevicesApi } from '@jellyfin/sdk/lib/utils/api/devices-api';
|
||||
@@ -28,6 +28,7 @@ export const useDevices = (
|
||||
queryKey: [QUERY_KEY, requestParams],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchDevices(api!, requestParams, { signal }),
|
||||
enabled: !!api
|
||||
enabled: !!api,
|
||||
staleTime: 0 // ensure we load the latest device data
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ApiKeyApiRevokeKeyRequest } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { ApiKeyApiRevokeKeyRequest } from '@jellyfin/sdk/lib/generated-client/api/api-key-api';
|
||||
import { getApiKeyApi } from '@jellyfin/sdk/lib/utils/api/api-key-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
|
||||
import type { VirtualFolderInfo } from '@jellyfin/sdk/lib/generated-client/models/virtual-folder-info';
|
||||
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
import { QUERY_KEY } from './useVirtualFolders';
|
||||
|
||||
/** Invalidates queries related to libraries/virtual folders. */
|
||||
export const invalidateVirtualFolders = (user?: UserDto, virtualFolder?: VirtualFolderInfo) => {
|
||||
// Admin API - Virtual Folders
|
||||
void queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
|
||||
if (user) {
|
||||
// User API - Views
|
||||
void queryClient.invalidateQueries({ queryKey: ['User', user.Id, 'Views'] });
|
||||
|
||||
if (virtualFolder?.ItemId) {
|
||||
// Items API - Library children
|
||||
void queryClient.invalidateQueries({ queryKey: ['User', user.Id, 'Items', virtualFolder.ItemId] });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -3,19 +3,18 @@ import { LibraryStructureApiRemoveVirtualFolderRequest } from '@jellyfin/sdk/lib
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
import { invalidateVirtualFolders } from './invalidateVirtualFolders';
|
||||
|
||||
export const useRemoveVirtualFolder = () => {
|
||||
const { api } = useApi();
|
||||
const { api, user } = useApi();
|
||||
return useMutation({
|
||||
mutationFn: (params: LibraryStructureApiRemoveVirtualFolderRequest) => (
|
||||
getLibraryStructureApi(api!)
|
||||
.removeVirtualFolder(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ 'VirtualFolders' ]
|
||||
});
|
||||
invalidateVirtualFolders(user);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,19 +3,18 @@ import { LibraryStructureApiRenameVirtualFolderRequest } from '@jellyfin/sdk/lib
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
import { invalidateVirtualFolders } from './invalidateVirtualFolders';
|
||||
|
||||
export const useRenameVirtualFolder = () => {
|
||||
const { api } = useApi();
|
||||
const { api, user } = useApi();
|
||||
return useMutation({
|
||||
mutationFn: (params: LibraryStructureApiRenameVirtualFolderRequest) => (
|
||||
getLibraryStructureApi(api!)
|
||||
.renameVirtualFolder(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ 'VirtualFolders' ]
|
||||
});
|
||||
invalidateVirtualFolders(user);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useApi } from 'hooks/useApi';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import { getLibraryStructureApi } from '@jellyfin/sdk/lib/utils/api/library-structure-api';
|
||||
|
||||
export const QUERY_KEY = 'VirtualFolders';
|
||||
|
||||
const fetchVirtualFolders = async (api: Api, options?: AxiosRequestConfig) => {
|
||||
const response = await getLibraryStructureApi(api).getVirtualFolders(options);
|
||||
|
||||
@@ -14,7 +16,7 @@ export const useVirtualFolders = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [ 'VirtualFolders' ],
|
||||
queryKey: [ QUERY_KEY ],
|
||||
queryFn: ({ signal }) => fetchVirtualFolders(api!, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import globalize from 'lib/globalize';
|
||||
import Icon from '@mui/material/Icon';
|
||||
import { getLibraryIcon } from 'utils/image';
|
||||
import MediaLibraryEditor from 'components/mediaLibraryEditor/mediaLibraryEditor';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
@@ -27,13 +26,14 @@ import { getImageApi } from '@jellyfin/sdk/lib/utils/api/image-api';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||
import dom from 'utils/dom';
|
||||
import { invalidateVirtualFolders } from '../api/invalidateVirtualFolders';
|
||||
|
||||
type LibraryCardProps = {
|
||||
virtualFolder: VirtualFolderInfo;
|
||||
};
|
||||
|
||||
const LibraryCard = ({ virtualFolder }: LibraryCardProps) => {
|
||||
const { api } = useApi();
|
||||
const { api, user } = useApi();
|
||||
const actionRef = useRef<HTMLButtonElement | null>(null);
|
||||
const [ anchorEl, setAnchorEl ] = useState<HTMLElement | null>(null);
|
||||
const [ isMenuOpen, setIsMenuOpen ] = useState(false);
|
||||
@@ -44,9 +44,10 @@ const LibraryCard = ({ virtualFolder }: LibraryCardProps) => {
|
||||
|
||||
const imageUrl = useMemo(() => {
|
||||
if (virtualFolder.PrimaryImageItemId && virtualFolder.ItemId && api) {
|
||||
const dpr = window?.devicePixelRatio || 1;
|
||||
return getImageApi(api)
|
||||
.getItemImageUrlById(virtualFolder.ItemId, ImageType.Primary, {
|
||||
maxWidth: Math.round(dom.getScreenWidth() * 0.40)
|
||||
maxWidth: Math.round(dom.getScreenWidth() * dpr * 0.40)
|
||||
});
|
||||
}
|
||||
}, [ api, virtualFolder ]);
|
||||
@@ -105,17 +106,14 @@ const LibraryCard = ({ virtualFolder }: LibraryCardProps) => {
|
||||
setIsMenuOpen(false);
|
||||
|
||||
const mediaLibraryEditor = new MediaLibraryEditor({
|
||||
library: virtualFolder
|
||||
library: virtualFolder,
|
||||
refresh: true
|
||||
}) as Promise<boolean>;
|
||||
|
||||
void mediaLibraryEditor.then((hasChanges: boolean) => {
|
||||
if (hasChanges) {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ['VirtualFolders']
|
||||
});
|
||||
}
|
||||
if (hasChanges) invalidateVirtualFolders(user, virtualFolder);
|
||||
});
|
||||
}, [ virtualFolder ]);
|
||||
}, [ user, virtualFolder ]);
|
||||
|
||||
const showImageEditor = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
@@ -125,13 +123,11 @@ const LibraryCard = ({ virtualFolder }: LibraryCardProps) => {
|
||||
itemId: virtualFolder.ItemId,
|
||||
serverId: ServerConnections.currentApiClient()?.serverId()
|
||||
}).then(() => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ['VirtualFolders']
|
||||
});
|
||||
invalidateVirtualFolders(user);
|
||||
}).catch(() => {
|
||||
/* pop up closed */
|
||||
});
|
||||
}, [ virtualFolder ]);
|
||||
}, [ user, virtualFolder ]);
|
||||
|
||||
const showDeleteLibraryDialog = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
|
||||
@@ -27,6 +27,7 @@ export const useServerLog = (name: string, refetchInterval: false | number) => {
|
||||
queryKey: ['ServerLog', name],
|
||||
queryFn: ({ signal }) => fetchServerLog(api!, name, { signal }),
|
||||
enabled: !!api,
|
||||
refetchInterval
|
||||
refetchInterval,
|
||||
staleTime: 0 // ensure we load the latest log data
|
||||
});
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ export const useServerLogs = () => {
|
||||
return useQuery({
|
||||
queryKey: [ 'ServerLogs' ],
|
||||
queryFn: ({ signal }) => fetchServerLogs(api!, { signal }),
|
||||
enabled: !!api
|
||||
enabled: !!api,
|
||||
staleTime: 0 // ensure we load the latest log entries
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import type { SessionInfoDto } from '@jellyfin/sdk/lib/generated-client/models/session-info-dto';
|
||||
import { SessionMessageType } from '@jellyfin/sdk/lib/generated-client/models/session-message-type';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { ApiClient } from 'jellyfin-apiclient';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import serverNotifications from 'scripts/serverNotifications';
|
||||
import Events, { Event } from 'utils/events';
|
||||
import { QUERY_KEY, useSessions } from '../api/useSessions';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import filterSessions from '../utils/filterSessions';
|
||||
import { OutboundWebSocketMessageType } from '@jellyfin/sdk/lib/websocket';
|
||||
|
||||
const QUERY_PARAMS = {
|
||||
activeWithinSeconds: 960
|
||||
};
|
||||
|
||||
const FALLBACK_POLL_INTERVAL_MS = 2000;
|
||||
|
||||
const useLiveSessions = () => {
|
||||
const { __legacyApiClient__ } = useApi();
|
||||
const { api } = useApi();
|
||||
|
||||
const sessionsQuery = useSessions(QUERY_PARAMS);
|
||||
|
||||
@@ -41,27 +36,11 @@ const useLiveSessions = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onSessionsUpdate = (evt: Event, apiClient: ApiClient, info: SessionInfoDto[]) => {
|
||||
queryClient.setQueryData([ QUERY_KEY, QUERY_PARAMS ], updateSessions(info));
|
||||
};
|
||||
|
||||
const fallbackInterval = setInterval(() => {
|
||||
if (!__legacyApiClient__?.isMessageChannelOpen()) {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QUERY_KEY ]
|
||||
});
|
||||
}
|
||||
}, FALLBACK_POLL_INTERVAL_MS);
|
||||
|
||||
__legacyApiClient__?.sendMessage(SessionMessageType.SessionsStart, '0,1500');
|
||||
Events.on(serverNotifications, SessionMessageType.Sessions, onSessionsUpdate);
|
||||
|
||||
return () => {
|
||||
clearInterval(fallbackInterval);
|
||||
__legacyApiClient__?.sendMessage(SessionMessageType.SessionsStop, null);
|
||||
Events.off(serverNotifications, SessionMessageType.Sessions, onSessionsUpdate);
|
||||
};
|
||||
}, []);
|
||||
// Return function for unsubscribing
|
||||
return api?.subscribe([OutboundWebSocketMessageType.Sessions], ({ Data }) => {
|
||||
queryClient.setQueryData([ QUERY_KEY, QUERY_PARAMS ], updateSessions(Data ?? []));
|
||||
});
|
||||
}, [api, updateSessions]);
|
||||
|
||||
return sessionsQuery;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,10 @@ const getNowPlayingImageUrl = (item: BaseItemDto) => {
|
||||
if (!item.ServerId) return null;
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
if (!apiClient) {
|
||||
console.error('[getNowPlayingImageUrl] No ApiClient instance available for serverId', item.ServerId);
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Screen width is multiplied by 0.2, as the there is currently no way to get the width of
|
||||
elements that aren't created yet. */
|
||||
|
||||
@@ -35,25 +35,28 @@ const getNowPlayingName = (session: SessionInfo): NowPlayingInfo => {
|
||||
bottomText = nowPlayingItem.ProductionYear.toString();
|
||||
}
|
||||
|
||||
if (nowPlayingItem.ImageTags?.Logo) {
|
||||
imgUrl = ServerConnections.getApiClient(session.ServerId!).getScaledImageUrl(nowPlayingItem.Id!, {
|
||||
tag: nowPlayingItem.ImageTags.Logo,
|
||||
maxHeight: 24,
|
||||
maxWidth: 130,
|
||||
type: 'Logo'
|
||||
});
|
||||
} else if (nowPlayingItem.ParentLogoImageTag) {
|
||||
imgUrl = ServerConnections.getApiClient(session.ServerId!).getScaledImageUrl(nowPlayingItem.ParentLogoItemId!, {
|
||||
tag: nowPlayingItem.ParentLogoImageTag,
|
||||
maxHeight: 24,
|
||||
maxWidth: 130,
|
||||
type: 'Logo'
|
||||
});
|
||||
const apiClient = ServerConnections.getApiClient(session.ServerId);
|
||||
if (apiClient) {
|
||||
if (nowPlayingItem.ImageTags?.Logo) {
|
||||
imgUrl = apiClient.getScaledImageUrl(nowPlayingItem.Id!, {
|
||||
tag: nowPlayingItem.ImageTags.Logo,
|
||||
maxHeight: 24,
|
||||
maxWidth: 130,
|
||||
type: 'Logo'
|
||||
});
|
||||
} else if (nowPlayingItem.ParentLogoImageTag) {
|
||||
imgUrl = apiClient.getScaledImageUrl(nowPlayingItem.ParentLogoItemId!, {
|
||||
tag: nowPlayingItem.ParentLogoImageTag,
|
||||
maxHeight: 24,
|
||||
maxWidth: 130,
|
||||
type: 'Logo'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
topText: topText,
|
||||
bottomText: bottomText,
|
||||
topText,
|
||||
bottomText,
|
||||
image: imgUrl
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { FolderStorageDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import Box from '@mui/material/Box';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
@@ -79,15 +80,29 @@ const StorageListItem: FC<StorageListItemProps> = ({
|
||||
color={statusColor}
|
||||
value={usedPercentage}
|
||||
/>
|
||||
<Typography
|
||||
variant='body2'
|
||||
color='textSecondary'
|
||||
<Box
|
||||
sx={{
|
||||
textAlign: 'end'
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap'
|
||||
}}
|
||||
>
|
||||
{`${readableUsedSpace} / ${readableTotalSpace}`}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant='body2'
|
||||
color='textSecondary'
|
||||
>
|
||||
{globalize.translate('OnFilesystem', folder?.DeviceId || '?')}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant='body2'
|
||||
color='textSecondary'
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
textAlign: 'end'
|
||||
}}
|
||||
>
|
||||
{`${readableUsedSpace} / ${readableTotalSpace}`}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
slots={{
|
||||
|
||||
@@ -43,7 +43,7 @@ const Task: FunctionComponent<TaskProps> = ({ task }: TaskProps) => {
|
||||
<ListItemLink to={`/dashboard/tasks/${task.Id}`}>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: 'primary.main' }}>
|
||||
<AccessTimeIcon sx={{ color: '#fff' }} />
|
||||
<AccessTimeIcon sx={{ color: '#fff', fontSize: 24 }} />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
|
||||
@@ -3,41 +3,17 @@ import { useApi } from 'hooks/useApi';
|
||||
import { QUERY_KEY, useTasks } from '../api/useTasks';
|
||||
import type { ScheduledTasksApiGetTasksRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-tasks-api';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import { ApiClient } from 'jellyfin-apiclient';
|
||||
import type { TaskInfo } from '@jellyfin/sdk/lib/generated-client/models/task-info';
|
||||
import Events, { Event } from 'utils/events';
|
||||
import serverNotifications from 'scripts/serverNotifications';
|
||||
import { SessionMessageType } from '@jellyfin/sdk/lib/generated-client/models/session-message-type';
|
||||
|
||||
const FALLBACK_POLL_INTERVAL_MS = 10000;
|
||||
import { OutboundWebSocketMessageType } from '@jellyfin/sdk/lib/websocket';
|
||||
|
||||
const useLiveTasks = (params: ScheduledTasksApiGetTasksRequest) => {
|
||||
const { __legacyApiClient__ } = useApi();
|
||||
const { api } = useApi();
|
||||
const tasksQuery = useTasks(params);
|
||||
|
||||
// TODO: Replace usage of the legacy apiclient when websocket support is added to the TS SDK.
|
||||
useEffect(() => {
|
||||
const onScheduledTasksUpdate = (_e: Event, _apiClient: ApiClient, info: TaskInfo[]) => {
|
||||
queryClient.setQueryData([ QUERY_KEY ], info);
|
||||
};
|
||||
|
||||
const fallbackInterval = setInterval(() => {
|
||||
if (!__legacyApiClient__?.isMessageChannelOpen()) {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QUERY_KEY ]
|
||||
});
|
||||
}
|
||||
}, FALLBACK_POLL_INTERVAL_MS);
|
||||
|
||||
__legacyApiClient__?.sendMessage(SessionMessageType.ScheduledTasksInfoStart, '1000,1000');
|
||||
Events.on(serverNotifications, SessionMessageType.ScheduledTasksInfo, onScheduledTasksUpdate);
|
||||
|
||||
return () => {
|
||||
clearInterval(fallbackInterval);
|
||||
__legacyApiClient__?.sendMessage(SessionMessageType.ScheduledTasksInfoStop, null);
|
||||
Events.off(serverNotifications, SessionMessageType.ScheduledTasksInfo, onScheduledTasksUpdate);
|
||||
};
|
||||
}, [ __legacyApiClient__ ]);
|
||||
return api?.subscribe([OutboundWebSocketMessageType.ScheduledTasksInfo], ({ Data }) => {
|
||||
queryClient.setQueryData([ QUERY_KEY ], Data ?? []);
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
return tasksQuery;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { getSessionApi } from '@jellyfin/sdk/lib/utils/api/session-api';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { getAuthenticationApi } from 'utils/sdk/authentication-api';
|
||||
|
||||
const fetchAuthProviders = async (api: Api, options?: AxiosRequestConfig) => {
|
||||
const response = await getSessionApi(api).getAuthProviders(options);
|
||||
const response = await getAuthenticationApi(api).getAuthProviders(options);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { getSessionApi } from '@jellyfin/sdk/lib/utils/api/session-api';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { getAuthenticationApi } from 'utils/sdk/authentication-api';
|
||||
|
||||
const fetchPasswordResetProviders = async (api: Api, options?: AxiosRequestConfig) => {
|
||||
const response = await getSessionApi(api).getPasswordResetProviders(options);
|
||||
const response = await getAuthenticationApi(api).getPasswordResetProviders(options);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import { QUERY_KEY } from './useUser';
|
||||
import { QUERY_KEY } from 'hooks/api/useUser';
|
||||
|
||||
export const useUpdateUser = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import { QUERY_KEY } from './useUser';
|
||||
import { QUERY_KEY } from 'hooks/api/useUser';
|
||||
|
||||
export const useUpdateUserPolicy = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { UserApiGetUserByIdRequest } from '@jellyfin/sdk/lib/generated-client/api/user-api';
|
||||
import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
|
||||
export const QUERY_KEY = 'User';
|
||||
|
||||
const fetchUser = async (api: Api, params: UserApiGetUserByIdRequest, options?: AxiosRequestConfig) => {
|
||||
const response = await getUserApi(api).getUserById(params, options);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const useUser = (params?: UserApiGetUserByIdRequest) => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [ QUERY_KEY, params?.userId ],
|
||||
queryFn: ({ signal }) => fetchUser(api!, params!, { signal }),
|
||||
enabled: !!api && !!params
|
||||
});
|
||||
};
|
||||
@@ -304,8 +304,14 @@ const Access = ({ userId }: AccessProps) => {
|
||||
key={Item.Id}
|
||||
className='chkDevice'
|
||||
itemId={Item.Id}
|
||||
itemName={Item.CustomName || Item.Name}
|
||||
itemAppName={Item.AppName}
|
||||
itemName={
|
||||
[
|
||||
Item.CustomName || Item.Name,
|
||||
Item.AppName
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' - ')
|
||||
}
|
||||
itemCheckedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import parseISO from 'date-fns/parseISO';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
|
||||
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
||||
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import ToggleButton from '@mui/material/ToggleButton';
|
||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||
import { type MRT_ColumnDef, type MRT_Theme, useMaterialReactTable } from 'material-react-table';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { type MRT_ColumnDef, type MRT_Theme, type MRT_ColumnFiltersState, type MRT_SortingState, useMaterialReactTable } from 'material-react-table';
|
||||
import DateTimeCell from 'apps/dashboard/components/table/DateTimeCell';
|
||||
import TablePage, { DEFAULT_TABLE_OPTIONS } from 'apps/dashboard/components/table/TablePage';
|
||||
import { useLogEntries } from 'apps/dashboard/features/activity/api/useLogEntries';
|
||||
@@ -18,10 +16,12 @@ import UserAvatarButton from 'apps/dashboard/components/UserAvatarButton';
|
||||
import type { ActivityLogEntryCell } from 'apps/dashboard/features/activity/types/ActivityLogEntryCell';
|
||||
import { type UsersRecords, useUsersDetails } from 'hooks/useUsers';
|
||||
import globalize from 'lib/globalize';
|
||||
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
|
||||
import { ActivityLogSortBy } from '@jellyfin/sdk/lib/generated-client/models/activity-log-sort-by';
|
||||
import { toBoolean } from 'utils/string';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 25;
|
||||
const VIEW_PARAM = 'useractivity';
|
||||
|
||||
const enum ActivityView {
|
||||
All = 'All',
|
||||
@@ -29,6 +29,8 @@ const enum ActivityView {
|
||||
System = 'System'
|
||||
}
|
||||
|
||||
const VIEW_PARAM = 'useractivity';
|
||||
|
||||
const getActivityView = (param: string | null) => {
|
||||
if (param === null) return ActivityView.All;
|
||||
if (toBoolean(param)) return ActivityView.User;
|
||||
@@ -43,10 +45,12 @@ const getUserCell = (users: UsersRecords) => function UserCell({ row }: Activity
|
||||
|
||||
export const Component = () => {
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
|
||||
const [ columnFilters, setColumnFilters ] = useState<MRT_ColumnFiltersState>([]);
|
||||
const [ activityView, setActivityView ] = useState(
|
||||
getActivityView(searchParams.get(VIEW_PARAM)));
|
||||
|
||||
const [ sorting, setSorting ] = useState<MRT_SortingState>([{ id: 'Date', desc: true }]);
|
||||
|
||||
const [ pagination, setPagination ] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: DEFAULT_PAGE_SIZE
|
||||
@@ -63,11 +67,49 @@ export const Component = () => {
|
||||
|
||||
const UserCell = getUserCell(users);
|
||||
|
||||
const activityParams = useMemo(() => ({
|
||||
startIndex: pagination.pageIndex * pagination.pageSize,
|
||||
limit: pagination.pageSize,
|
||||
hasUserId: activityView !== ActivityView.All ? activityView === ActivityView.User : undefined
|
||||
}), [activityView, pagination.pageIndex, pagination.pageSize]);
|
||||
const activityParams = useMemo(() => {
|
||||
const getFilter = (id: string) => columnFilters.find(f => f.id === id)?.value;
|
||||
const sortFields: ActivityLogSortBy[] = [];
|
||||
const sortOrders: SortOrder[] = [];
|
||||
|
||||
const mapSortField = (id: string): ActivityLogSortBy => {
|
||||
switch (id) {
|
||||
case 'Date': return ActivityLogSortBy.DateCreated;
|
||||
case 'Severity': return ActivityLogSortBy.LogSeverity;
|
||||
case 'Name': return ActivityLogSortBy.Name;
|
||||
case 'Type': return ActivityLogSortBy.Type;
|
||||
case 'Overview': return ActivityLogSortBy.ShortOverview;
|
||||
case 'User': return ActivityLogSortBy.Username;
|
||||
default: return ActivityLogSortBy.DateCreated;
|
||||
}
|
||||
};
|
||||
|
||||
if (sorting.length === 0) {
|
||||
sortFields.push(ActivityLogSortBy.DateCreated);
|
||||
sortOrders.push(SortOrder.Descending);
|
||||
} else {
|
||||
sorting.forEach(sort => {
|
||||
sortFields.push(mapSortField(sort.id));
|
||||
sortOrders.push(sort.desc ? SortOrder.Descending : SortOrder.Ascending);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
startIndex: pagination.pageIndex * pagination.pageSize,
|
||||
limit: pagination.pageSize,
|
||||
hasUserId: activityView !== ActivityView.All ? activityView === ActivityView.User : undefined,
|
||||
|
||||
name: getFilter('Name') as string || undefined,
|
||||
type: getFilter('Type') as string || undefined,
|
||||
shortOverview: getFilter('Overview') as string || undefined,
|
||||
username: getFilter('User') as string || undefined,
|
||||
severity: getFilter('Severity') as LogLevel || undefined,
|
||||
minDate: (getFilter('Date') as string[] | undefined)?.[0] ?? undefined,
|
||||
maxDate: (getFilter('Date') as string[] | undefined)?.[1] ?? undefined,
|
||||
sortBy: sortFields,
|
||||
sortOrder: sortOrders
|
||||
};
|
||||
}, [ pagination, columnFilters, sorting, activityView ]);
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -88,13 +130,13 @@ export const Component = () => {
|
||||
id: 'User',
|
||||
accessorFn: row => row.UserId && users[row.UserId]?.Name,
|
||||
header: globalize.translate('LabelUser'),
|
||||
size: 75,
|
||||
size: 100,
|
||||
Cell: UserCell,
|
||||
enableResizing: false,
|
||||
muiTableBodyCellProps: {
|
||||
align: 'center'
|
||||
},
|
||||
filterVariant: 'multi-select',
|
||||
filterVariant: 'select',
|
||||
filterSelectOptions: userNames
|
||||
}], [ activityView, userNames, users, UserCell ]);
|
||||
|
||||
@@ -105,7 +147,9 @@ export const Component = () => {
|
||||
header: globalize.translate('LabelTime'),
|
||||
size: 160,
|
||||
Cell: DateTimeCell,
|
||||
filterVariant: 'datetime-range'
|
||||
filterVariant: 'datetime-range',
|
||||
grow: true,
|
||||
maxSize: 320
|
||||
},
|
||||
{
|
||||
accessorKey: 'Severity',
|
||||
@@ -116,26 +160,34 @@ export const Component = () => {
|
||||
muiTableBodyCellProps: {
|
||||
align: 'center'
|
||||
},
|
||||
filterVariant: 'multi-select',
|
||||
filterSelectOptions: Object.values(LogLevel).map(level => globalize.translate(`LogLevel.${level}`))
|
||||
filterVariant: 'select',
|
||||
filterSelectOptions: Object.values(LogLevel).map(level => ({
|
||||
text: globalize.translate(`LogLevel.${level}`),
|
||||
value: level
|
||||
}))
|
||||
},
|
||||
...userColumn,
|
||||
{
|
||||
accessorKey: 'Name',
|
||||
header: globalize.translate('LabelName'),
|
||||
size: 270
|
||||
size: 270,
|
||||
grow: true
|
||||
},
|
||||
{
|
||||
id: 'Overview',
|
||||
accessorFn: row => row.ShortOverview || row.Overview,
|
||||
header: globalize.translate('LabelOverview'),
|
||||
size: 170,
|
||||
Cell: OverviewCell
|
||||
Cell: OverviewCell,
|
||||
grow: true,
|
||||
maxSize: 220
|
||||
},
|
||||
{
|
||||
accessorKey: 'Type',
|
||||
header: globalize.translate('LabelType'),
|
||||
size: 150
|
||||
size: 150,
|
||||
grow: true,
|
||||
maxSize: 220
|
||||
},
|
||||
{
|
||||
id: 'Actions',
|
||||
@@ -183,13 +235,23 @@ export const Component = () => {
|
||||
|
||||
// State
|
||||
initialState: {
|
||||
density: 'compact'
|
||||
density: 'compact',
|
||||
showColumnFilters: false
|
||||
},
|
||||
state: {
|
||||
isLoading,
|
||||
pagination
|
||||
columnFilters,
|
||||
pagination,
|
||||
sorting
|
||||
},
|
||||
|
||||
manualFiltering: true,
|
||||
manualSorting: true,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onSortingChange: setSorting,
|
||||
enableMultiSort: true,
|
||||
enableGlobalFilter: false,
|
||||
|
||||
// Server pagination
|
||||
manualPagination: true,
|
||||
onPaginationChange: setPagination,
|
||||
|
||||
@@ -22,6 +22,9 @@ import { useRestoreBackup } from 'apps/dashboard/features/backups/api/useRestore
|
||||
import RestoreProgressDialog from 'apps/dashboard/features/backups/components/RestoreProgressDialog';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
|
||||
import useLiveTasks from 'apps/dashboard/features/tasks/hooks/useLiveTasks';
|
||||
import { TaskState } from '@jellyfin/sdk/lib/generated-client/models/task-state';
|
||||
import ConfirmDialog from 'components/ConfirmDialog';
|
||||
|
||||
export const Component = () => {
|
||||
const { api } = useApi();
|
||||
@@ -33,9 +36,16 @@ export const Component = () => {
|
||||
const [ isErrorOccurred, setIsErrorOccurred ] = useState(false);
|
||||
const [ isRestoreDialogOpen, setIsRestoreDialogOpen ] = useState(false);
|
||||
const [ backupToRestore, setBackupToRestore ] = useState<BackupManifestDto | null>(null);
|
||||
const [ isConfirmBackupOpen, setIsConfirmBackupOpen ] = useState(false);
|
||||
const [ pendingBackupOptions, setPendingBackupOptions ] = useState<BackupOptionsDto | null>(null);
|
||||
const createBackup = useCreateBackup();
|
||||
const restoreBackup = useRestoreBackup();
|
||||
|
||||
const {
|
||||
data: tasks,
|
||||
isPending: isTasksPending
|
||||
} = useLiveTasks({ isHidden: false });
|
||||
|
||||
const onCreateClick = useCallback(() => {
|
||||
setIsCreateFormOpen(true);
|
||||
}, []);
|
||||
@@ -56,7 +66,11 @@ export const Component = () => {
|
||||
setIsRestoreSuccess(false);
|
||||
}, []);
|
||||
|
||||
const onBackupCreate = useCallback((backupOptions: BackupOptionsDto) => {
|
||||
const onCancelBackupCreateDialog = useCallback(() => {
|
||||
setIsConfirmBackupOpen(false);
|
||||
}, []);
|
||||
|
||||
const startBackupProcess = useCallback((backupOptions: BackupOptionsDto) => {
|
||||
setBackupInProgress(true);
|
||||
setIsCreateFormOpen(false);
|
||||
createBackup.mutate(backupOptions, {
|
||||
@@ -69,6 +83,24 @@ export const Component = () => {
|
||||
});
|
||||
}, [ createBackup ]);
|
||||
|
||||
const startPendingBackup = useCallback(() => {
|
||||
setIsConfirmBackupOpen(false);
|
||||
if (pendingBackupOptions) {
|
||||
startBackupProcess(pendingBackupOptions);
|
||||
}
|
||||
}, [ startBackupProcess, pendingBackupOptions ]);
|
||||
|
||||
const onBackupCreate = useCallback((backupOptions: BackupOptionsDto) => {
|
||||
const isTasksRunning = tasks?.some(task => task?.State !== TaskState.Idle);
|
||||
if (isTasksRunning) {
|
||||
setPendingBackupOptions(backupOptions);
|
||||
setIsCreateFormOpen(false);
|
||||
setIsConfirmBackupOpen(true);
|
||||
} else {
|
||||
startBackupProcess(backupOptions);
|
||||
}
|
||||
}, [ startBackupProcess, tasks ]);
|
||||
|
||||
const promptRestore = useCallback((backup: BackupManifestDto) => {
|
||||
setIsRestoreDialogOpen(true);
|
||||
setBackupToRestore(backup);
|
||||
@@ -110,7 +142,7 @@ export const Component = () => {
|
||||
}
|
||||
}, [api, restoreInProgress]);
|
||||
|
||||
if (isPending) {
|
||||
if (isPending || isTasksPending) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
@@ -122,6 +154,20 @@ export const Component = () => {
|
||||
>
|
||||
<BackupProgressDialog open={backupInProgress} />
|
||||
<RestoreProgressDialog open={restoreInProgress} />
|
||||
<ConfirmDialog
|
||||
open={isConfirmBackupOpen}
|
||||
title={globalize.translate('HeaderBackupWarning')}
|
||||
text={
|
||||
globalize.translate('LabelBackupWarning')
|
||||
+ '\n\n'
|
||||
+ tasks?.filter(task => task.State !== TaskState.Idle)
|
||||
.map(task => '- ' + task.Name).join('\n')
|
||||
}
|
||||
confirmButtonText={globalize.translate('Create')}
|
||||
confirmButtonColor='warning'
|
||||
onConfirm={startPendingBackup}
|
||||
onCancel={onCancelBackupCreateDialog}
|
||||
/>
|
||||
<CreateBackupForm
|
||||
open={isCreateFormOpen}
|
||||
onClose={onCreateFormClose}
|
||||
|
||||
@@ -33,7 +33,7 @@ const BrandingOption = {
|
||||
};
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const api = ServerConnections.getCurrentApi();
|
||||
const api = ServerConnections.getApi();
|
||||
if (!api) throw new Error('No Api instance available');
|
||||
|
||||
const formData = await request.formData();
|
||||
@@ -60,9 +60,12 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const loader = () => {
|
||||
export const loader = async () => {
|
||||
const api = ServerConnections.getApi();
|
||||
if (!api) return {};
|
||||
|
||||
return queryClient.ensureQueryData(
|
||||
getBrandingOptionsQuery(ServerConnections.getCurrentApi()));
|
||||
getBrandingOptionsQuery(api));
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ import type { MetadataConfiguration } from '@jellyfin/sdk/lib/generated-client/m
|
||||
const CONFIG_KEY = 'metadata';
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const api = ServerConnections.getCurrentApi();
|
||||
const api = ServerConnections.getApi();
|
||||
if (!api) throw new Error('No Api instance available');
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
@@ -14,12 +14,14 @@ import LibraryCard from 'apps/dashboard/features/libraries/components/LibraryCar
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import MediaLibraryCreator from 'components/mediaLibraryCreator/mediaLibraryCreator';
|
||||
import getCollectionTypeOptions from 'apps/dashboard/features/libraries/utils/collectionTypeOptions';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import Add from '@mui/icons-material/Add';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { invalidateVirtualFolders } from 'apps/dashboard/features/libraries/api/invalidateVirtualFolders';
|
||||
|
||||
export const Component = () => {
|
||||
const { user } = useApi();
|
||||
const { data: virtualFolders, isPending: isVirtualFoldersPending, isError: isVirtualFoldersError } = useVirtualFolders();
|
||||
const startTask = useStartTask();
|
||||
const { data: tasks, isPending: isLiveTasksPending } = useLiveTasks({ isHidden: false });
|
||||
@@ -35,13 +37,9 @@ export const Component = () => {
|
||||
}) as Promise<boolean>;
|
||||
|
||||
void mediaLibraryCreator.then((hasChanges: boolean) => {
|
||||
if (hasChanges) {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ['VirtualFolders']
|
||||
});
|
||||
}
|
||||
if (hasChanges) invalidateVirtualFolders(user);
|
||||
});
|
||||
}, []);
|
||||
}, [ user ]);
|
||||
|
||||
const onScanLibraries = useCallback(() => {
|
||||
if (librariesTask?.Id) {
|
||||
|
||||
@@ -21,7 +21,7 @@ import { ActionData } from 'types/actionData';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const api = ServerConnections.getCurrentApi();
|
||||
const api = ServerConnections.getApi();
|
||||
if (!api) throw new Error('No Api instance available');
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
@@ -26,7 +26,7 @@ import type { XbmcMetadataOptions } from '@jellyfin/sdk/lib/generated-client/mod
|
||||
const CONFIG_KEY = 'xbmcmetadata';
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const api = ServerConnections.getCurrentApi();
|
||||
const api = ServerConnections.getApi();
|
||||
if (!api) throw new Error('No Api instance available');
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
@@ -28,7 +28,7 @@ import { ActionData } from 'types/actionData';
|
||||
const CONFIG_KEY = 'livetv';
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const api = ServerConnections.getCurrentApi();
|
||||
const api = ServerConnections.getApi();
|
||||
if (!api) throw new Error('No Api instance available');
|
||||
|
||||
const data = await request.json() as LiveTvOptions;
|
||||
|
||||
@@ -20,7 +20,7 @@ import { ActionData } from 'types/actionData';
|
||||
import LogItemList from 'apps/dashboard/features/logs/components/LogItemList';
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const api = ServerConnections.getCurrentApi();
|
||||
const api = ServerConnections.getApi();
|
||||
if (!api) throw new Error('No Api instance available');
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
@@ -31,7 +31,7 @@ import Switch from '@mui/material/Switch';
|
||||
const CONFIG_KEY = 'network';
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const api = ServerConnections.getCurrentApi();
|
||||
const api = ServerConnections.getApi();
|
||||
if (!api) throw new Error('No Api instance available');
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
@@ -16,7 +16,7 @@ import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-a
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const api = ServerConnections.getCurrentApi();
|
||||
const api = ServerConnections.getApi();
|
||||
if (!api) throw new Error('No Api instance available');
|
||||
|
||||
const { data: config } = await getConfigurationApi(api).getConfiguration();
|
||||
|
||||
@@ -16,7 +16,7 @@ import { ActionData } from 'types/actionData';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const api = ServerConnections.getCurrentApi();
|
||||
const api = ServerConnections.getApi();
|
||||
if (!api) throw new Error('No Api instance available');
|
||||
|
||||
const { data: config } = await getConfigurationApi(api).getConfiguration();
|
||||
|
||||
@@ -33,7 +33,7 @@ import SimpleAlert from 'components/SimpleAlert';
|
||||
const CONFIG_KEY = 'encoding';
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const api = ServerConnections.getCurrentApi();
|
||||
const api = ServerConnections.getApi();
|
||||
if (!api) throw new Error('No Api instance available');
|
||||
|
||||
const data = await request.json() as EncodingOptions;
|
||||
|
||||
@@ -24,7 +24,7 @@ import { ActionData } from 'types/actionData';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const api = ServerConnections.getCurrentApi();
|
||||
const api = ServerConnections.getApi();
|
||||
if (!api) throw new Error('No Api instance available');
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
@@ -8,7 +8,8 @@ import Typography from '@mui/material/Typography';
|
||||
import { useLocalizationOptions } from 'apps/dashboard/features/settings/api/useLocalizationOptions';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import Page from 'components/Page';
|
||||
import { QUERY_KEY, useConfiguration } from 'hooks/useConfiguration';
|
||||
import { QUERY_KEY as CONFIGURATION_QUERY_KEY, useConfiguration } from 'hooks/useConfiguration';
|
||||
import { QUERY_KEY as SYSTEM_INFO_QUERY_KEY } from 'hooks/useSystemInfo';
|
||||
import globalize from 'lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
@@ -26,7 +27,7 @@ import { queryClient } from 'utils/query/queryClient';
|
||||
import { ActionData } from 'types/actionData';
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const api = ServerConnections.getCurrentApi();
|
||||
const api = ServerConnections.getApi();
|
||||
if (!api) throw new Error('No Api instance available');
|
||||
|
||||
const { data: config } = await getConfigurationApi(api).getConfiguration();
|
||||
@@ -44,7 +45,10 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
.updateConfiguration({ serverConfiguration: config });
|
||||
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QUERY_KEY ]
|
||||
queryKey: [ CONFIGURATION_QUERY_KEY ]
|
||||
});
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ SYSTEM_INFO_QUERY_KEY ]
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useUser } from 'apps/dashboard/features/users/api/useUser';
|
||||
import { useUser } from 'hooks/api/useUser';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import Page from 'components/Page';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
@@ -19,7 +19,7 @@ import Alert from '@mui/material/Alert';
|
||||
export const Component = () => {
|
||||
const navigate = useNavigate();
|
||||
const { userId, tab } = useParams();
|
||||
const { data: user, isPending, isError } = useUser(userId ? { userId: userId } : undefined);
|
||||
const { data: user, isPending, isError } = useUser({ userId });
|
||||
|
||||
const handleTabChange = useCallback((event: React.SyntheticEvent, newValue: UserTab) => {
|
||||
navigate(`/dashboard/users/${userId}/${newValue}`);
|
||||
|
||||
@@ -28,7 +28,7 @@ const UserProfiles = () => {
|
||||
const [ isSettingsSavedToastOpen, setIsSettingsSavedToastOpen ] = useState(false);
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
const navigate = useNavigate();
|
||||
const { data: users, isPending } = useUsers();
|
||||
const { data: users, isPending } = useUsers(undefined, { forceRefresh: true });
|
||||
const deleteUser = useDeleteUser();
|
||||
|
||||
const handleToastClose = useCallback(() => {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText/ListItemText';
|
||||
import Menu, { type MenuProps } from '@mui/material/Menu/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem/MenuItem';
|
||||
import React, { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import LibraryIcon from 'apps/experimental/components/LibraryIcon';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
|
||||
interface UserViewsMenuProps extends MenuProps {
|
||||
userViews: BaseItemDto[]
|
||||
selectedId?: string
|
||||
includeGlobalViews?: boolean
|
||||
onMenuClose: () => void
|
||||
}
|
||||
|
||||
const UserViewsMenu: FC<UserViewsMenuProps> = ({
|
||||
userViews,
|
||||
selectedId,
|
||||
onMenuClose,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<Menu
|
||||
{...props}
|
||||
keepMounted
|
||||
onClose={onMenuClose}
|
||||
>
|
||||
{userViews.map(view => (
|
||||
<MenuItem
|
||||
key={view.Id}
|
||||
component={Link}
|
||||
to={appRouter.getRouteUrl(view, { context: view.CollectionType }).substring(1)}
|
||||
onClick={onMenuClose}
|
||||
selected={view.Id === selectedId}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<LibraryIcon item={view} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{view.Name}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserViewsMenu;
|
||||
@@ -1,420 +0,0 @@
|
||||
import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by';
|
||||
import Box from '@mui/material/Box';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import type { Theme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import classNames from 'classnames';
|
||||
import React, { type FC, useCallback } from 'react';
|
||||
|
||||
import { CardShape } from 'components/cardbuilder/utils/shape';
|
||||
import { ItemAction } from 'constants/itemAction';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||
import { useGetItemsViewByType } from 'hooks/useFetchItems';
|
||||
import { getDefaultLibraryViewSettings, getSettingsKey } from 'utils/items';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import ItemsContainer from 'elements/emby-itemscontainer/ItemsContainer';
|
||||
import NoItemsMessage from 'components/common/NoItemsMessage';
|
||||
import Lists from 'components/listview/List/Lists';
|
||||
import Cards from 'components/cardbuilder/Card/Cards';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
import { type LibraryViewSettings, type ParentId, ViewMode } from 'types/library';
|
||||
import type { CardOptions } from 'types/cardOptions';
|
||||
import type { ListOptions } from 'types/listOptions';
|
||||
import { useItem } from 'hooks/useItem';
|
||||
|
||||
import AlphabetPicker from './AlphabetPicker';
|
||||
import FilterButton from './filter/FilterButton';
|
||||
import NewCollectionButton from './NewCollectionButton';
|
||||
import NewPlaylistButton from './NewPlaylistButton';
|
||||
import Pagination from './Pagination';
|
||||
import PlayAllButton from './PlayAllButton';
|
||||
import QueueButton from './QueueButton';
|
||||
import ShuffleButton from './ShuffleButton';
|
||||
import SortButton from './SortButton';
|
||||
import LibraryViewMenu from './LibraryViewMenu';
|
||||
import ViewSettingsButton from './ViewSettingsButton';
|
||||
|
||||
interface ItemsViewProps {
|
||||
viewType: LibraryTab;
|
||||
parentId: ParentId;
|
||||
itemType: BaseItemKind[];
|
||||
collectionType?: CollectionType;
|
||||
isPaginationEnabled?: boolean;
|
||||
isBtnPlayAllEnabled?: boolean;
|
||||
isBtnQueueEnabled?: boolean;
|
||||
isBtnShuffleEnabled?: boolean;
|
||||
isBtnSortEnabled?: boolean;
|
||||
isBtnFilterEnabled?: boolean;
|
||||
isBtnNewCollectionEnabled?: boolean;
|
||||
isBtnNewPlaylistEnabled?: boolean;
|
||||
isBtnGridListEnabled?: boolean;
|
||||
isAlphabetPickerEnabled?: boolean;
|
||||
noItemsMessage: string;
|
||||
}
|
||||
|
||||
const ItemsView: FC<ItemsViewProps> = ({
|
||||
viewType,
|
||||
parentId,
|
||||
collectionType,
|
||||
isPaginationEnabled = true,
|
||||
isBtnPlayAllEnabled = false,
|
||||
isBtnQueueEnabled = false,
|
||||
isBtnShuffleEnabled = false,
|
||||
isBtnSortEnabled = true,
|
||||
isBtnFilterEnabled = true,
|
||||
isBtnNewCollectionEnabled = false,
|
||||
isBtnNewPlaylistEnabled = false,
|
||||
isBtnGridListEnabled = true,
|
||||
isAlphabetPickerEnabled = true,
|
||||
itemType,
|
||||
noItemsMessage
|
||||
}) => {
|
||||
const [libraryViewSettings, setLibraryViewSettings] =
|
||||
useLocalStorage<LibraryViewSettings>(
|
||||
getSettingsKey(viewType, parentId),
|
||||
getDefaultLibraryViewSettings(viewType)
|
||||
);
|
||||
const isSmallScreen = useMediaQuery((t: Theme) => t.breakpoints.up('sm'));
|
||||
|
||||
const { __legacyApiClient__ } = useApi();
|
||||
const {
|
||||
isPending,
|
||||
data: itemsResult,
|
||||
isPlaceholderData,
|
||||
refetch
|
||||
} = useGetItemsViewByType(
|
||||
viewType,
|
||||
parentId,
|
||||
itemType,
|
||||
libraryViewSettings
|
||||
);
|
||||
const { data: item } = useItem(parentId || undefined);
|
||||
|
||||
const getListOptions = useCallback(() => {
|
||||
const listOptions: ListOptions = {
|
||||
items: itemsResult?.Items ?? [],
|
||||
context: collectionType
|
||||
};
|
||||
|
||||
if (viewType === LibraryTab.Songs) {
|
||||
listOptions.showParentTitle = true;
|
||||
listOptions.action = ItemAction.PlayAllFromHere;
|
||||
listOptions.smallIcon = true;
|
||||
listOptions.showArtist = true;
|
||||
listOptions.addToListButton = true;
|
||||
} else if (viewType === LibraryTab.Albums) {
|
||||
listOptions.sortBy = libraryViewSettings.SortBy;
|
||||
listOptions.addToListButton = true;
|
||||
} else if (viewType === LibraryTab.Episodes) {
|
||||
listOptions.showParentTitle = true;
|
||||
}
|
||||
|
||||
return listOptions;
|
||||
}, [itemsResult?.Items, collectionType, viewType, libraryViewSettings.SortBy]);
|
||||
|
||||
const getCardOptions = useCallback(() => {
|
||||
let shape;
|
||||
let preferThumb;
|
||||
let preferDisc;
|
||||
let preferLogo;
|
||||
|
||||
if (libraryViewSettings.ImageType === ImageType.Banner) {
|
||||
shape = CardShape.Banner;
|
||||
} else if (libraryViewSettings.ImageType === ImageType.Disc) {
|
||||
shape = CardShape.Square;
|
||||
preferDisc = true;
|
||||
} else if (libraryViewSettings.ImageType === ImageType.Logo) {
|
||||
shape = CardShape.Backdrop;
|
||||
preferLogo = true;
|
||||
} else if (libraryViewSettings.ImageType === ImageType.Thumb) {
|
||||
shape = CardShape.Backdrop;
|
||||
preferThumb = true;
|
||||
} else {
|
||||
shape = CardShape.Auto;
|
||||
}
|
||||
|
||||
const cardOptions: CardOptions = {
|
||||
shape: shape,
|
||||
showTitle: libraryViewSettings.ShowTitle,
|
||||
showYear: libraryViewSettings.ShowYear,
|
||||
cardLayout: libraryViewSettings.CardLayout,
|
||||
centerText: true,
|
||||
context: collectionType,
|
||||
coverImage: true,
|
||||
preferThumb: preferThumb,
|
||||
preferDisc: preferDisc,
|
||||
preferLogo: preferLogo,
|
||||
overlayText: !libraryViewSettings.ShowTitle,
|
||||
imageType: libraryViewSettings.ImageType,
|
||||
queryKey: ['ItemsViewByType'],
|
||||
serverId: __legacyApiClient__?.serverId()
|
||||
};
|
||||
|
||||
if (
|
||||
viewType === LibraryTab.Songs
|
||||
|| viewType === LibraryTab.Albums
|
||||
|| viewType === LibraryTab.Episodes
|
||||
) {
|
||||
cardOptions.showParentTitle = libraryViewSettings.ShowTitle;
|
||||
cardOptions.overlayPlayButton = true;
|
||||
} else if (viewType === LibraryTab.Artists) {
|
||||
cardOptions.lines = 1;
|
||||
cardOptions.showYear = false;
|
||||
cardOptions.overlayPlayButton = true;
|
||||
} else if (viewType === LibraryTab.Channels) {
|
||||
cardOptions.shape = CardShape.Square;
|
||||
cardOptions.showDetailsMenu = true;
|
||||
cardOptions.showCurrentProgram = true;
|
||||
cardOptions.showCurrentProgramTime = true;
|
||||
} else if (viewType === LibraryTab.SeriesTimers) {
|
||||
cardOptions.shape = CardShape.Backdrop;
|
||||
cardOptions.showSeriesTimerTime = true;
|
||||
cardOptions.showSeriesTimerChannel = true;
|
||||
cardOptions.overlayMoreButton = true;
|
||||
cardOptions.lines = 3;
|
||||
} else if (viewType === LibraryTab.Movies) {
|
||||
cardOptions.overlayPlayButton = true;
|
||||
} else if (viewType === LibraryTab.Series || viewType === LibraryTab.Networks) {
|
||||
cardOptions.overlayMoreButton = true;
|
||||
}
|
||||
|
||||
return cardOptions;
|
||||
}, [
|
||||
libraryViewSettings.ShowTitle,
|
||||
libraryViewSettings.ImageType,
|
||||
libraryViewSettings.ShowYear,
|
||||
libraryViewSettings.CardLayout,
|
||||
collectionType,
|
||||
viewType
|
||||
]);
|
||||
|
||||
const getItems = useCallback(() => {
|
||||
if (!itemsResult?.Items?.length) {
|
||||
return <NoItemsMessage message={noItemsMessage} />;
|
||||
}
|
||||
|
||||
if (libraryViewSettings.ViewMode === ViewMode.ListView) {
|
||||
return (
|
||||
<Lists
|
||||
items={itemsResult?.Items ?? []}
|
||||
listOptions={getListOptions()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Cards
|
||||
items={itemsResult?.Items ?? []}
|
||||
cardOptions={getCardOptions()}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
libraryViewSettings.ViewMode,
|
||||
itemsResult?.Items,
|
||||
getListOptions,
|
||||
getCardOptions,
|
||||
noItemsMessage
|
||||
]);
|
||||
|
||||
const totalRecordCount = itemsResult?.TotalRecordCount ?? 0;
|
||||
const items = itemsResult?.Items ?? [];
|
||||
const hasFilters = Object.values(libraryViewSettings.Filters ?? {}).some(
|
||||
(filter) => !!filter
|
||||
);
|
||||
const hasSortName = libraryViewSettings.SortBy !== ItemSortBy.Random;
|
||||
|
||||
const itemsContainerClass = classNames(
|
||||
'padded-left padded-right padded-right-withalphapicker',
|
||||
libraryViewSettings.ViewMode === ViewMode.ListView ?
|
||||
'vertical-list' :
|
||||
'vertical-wrap'
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className='padded-bottom-page'>
|
||||
<Box
|
||||
className={classNames(
|
||||
'padded-top padded-left padded-right',
|
||||
{ 'padded-right-withalphapicker': isAlphabetPickerEnabled }
|
||||
)}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{ marginRight: 1 }}
|
||||
>
|
||||
<LibraryViewMenu />
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: {
|
||||
xs: 1,
|
||||
sm: 0
|
||||
},
|
||||
marginRight: 1
|
||||
}}
|
||||
>
|
||||
<ButtonGroup
|
||||
color='inherit'
|
||||
variant='text'
|
||||
>
|
||||
{isBtnFilterEnabled && (
|
||||
<FilterButton
|
||||
parentId={parentId}
|
||||
itemType={itemType}
|
||||
viewType={viewType}
|
||||
hasFilters={hasFilters}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBtnSortEnabled && (
|
||||
<SortButton
|
||||
viewType={viewType}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBtnGridListEnabled && (
|
||||
<ViewSettingsButton
|
||||
viewType={viewType}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexGrow: {
|
||||
xs: 1,
|
||||
sm: 0
|
||||
},
|
||||
justifyContent: 'flex-end'
|
||||
}}
|
||||
>
|
||||
{!isPending && (
|
||||
<>
|
||||
<ButtonGroup
|
||||
variant='contained'
|
||||
>
|
||||
{isBtnPlayAllEnabled && (
|
||||
<PlayAllButton
|
||||
item={item}
|
||||
items={items}
|
||||
viewType={viewType}
|
||||
hasFilters={hasFilters}
|
||||
isTextVisible={isSmallScreen}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBtnShuffleEnabled && totalRecordCount > 1 && (
|
||||
<ShuffleButton
|
||||
item={item}
|
||||
items={items}
|
||||
viewType={viewType}
|
||||
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 && <NewCollectionButton isTextVisible={isSmallScreen} />}
|
||||
{isBtnNewPlaylistEnabled && <NewPlaylistButton isTextVisible={isSmallScreen} />}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
flexBasis: {
|
||||
xs: '100%',
|
||||
sm: 'auto'
|
||||
},
|
||||
flexGrow: 1,
|
||||
marginTop: {
|
||||
xs: 0.5,
|
||||
sm: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!isPending && isPaginationEnabled && (
|
||||
<Pagination
|
||||
totalRecordCount={totalRecordCount}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
isPlaceholderData={isPlaceholderData}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{isAlphabetPickerEnabled && hasSortName && (
|
||||
<AlphabetPicker
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isPending ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<ItemsContainer
|
||||
className={itemsContainerClass}
|
||||
parentId={parentId}
|
||||
reloadItems={refetch}
|
||||
queryKey={['ItemsViewByType']}
|
||||
>
|
||||
{getItems()}
|
||||
</ItemsContainer>
|
||||
)}
|
||||
|
||||
{!isPending && isPaginationEnabled && (
|
||||
<Box
|
||||
className={classNames(
|
||||
'padded-left padded-right',
|
||||
{ 'padded-right-withalphapicker': isAlphabetPickerEnabled }
|
||||
)}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end'
|
||||
}}
|
||||
>
|
||||
<Pagination
|
||||
totalRecordCount={totalRecordCount}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
isPlaceholderData={isPlaceholderData}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemsView;
|
||||
@@ -1,127 +0,0 @@
|
||||
import Box from '@mui/material/Box/Box';
|
||||
import React, { type FC } from 'react';
|
||||
import SuggestionsSectionView from './SuggestionsSectionView';
|
||||
import UpcomingView from './UpcomingView';
|
||||
import GenresView from './GenresView';
|
||||
import ItemsView from './ItemsView';
|
||||
import GuideView from './GuideView';
|
||||
import ProgramsSectionView from './ProgramsSectionView';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
import type { ParentId } from 'types/library';
|
||||
import type { LibraryTabContent } from 'types/libraryTabContent';
|
||||
import LibraryViewMenu from './LibraryViewMenu';
|
||||
|
||||
interface PageTabContentProps {
|
||||
parentId: ParentId;
|
||||
currentTab: LibraryTabContent;
|
||||
}
|
||||
|
||||
const PageTabContent: FC<PageTabContentProps> = ({ parentId, currentTab }) => {
|
||||
if (currentTab.viewType === LibraryTab.Suggestions) {
|
||||
return (
|
||||
<>
|
||||
<Box className='padded-top padded-left padded-right padded-bottom'>
|
||||
<LibraryViewMenu />
|
||||
</Box>
|
||||
|
||||
<SuggestionsSectionView
|
||||
parentId={parentId}
|
||||
sectionType={
|
||||
currentTab.sectionsView?.suggestionSections ?? []
|
||||
}
|
||||
isMovieRecommendationEnabled={
|
||||
currentTab.sectionsView?.isMovieRecommendations
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentTab.viewType === LibraryTab.Programs || currentTab.viewType === LibraryTab.Recordings || currentTab.viewType === LibraryTab.Schedule) {
|
||||
return (
|
||||
<>
|
||||
<Box className='padded-top padded-left padded-right padded-bottom'>
|
||||
<LibraryViewMenu />
|
||||
</Box>
|
||||
|
||||
<ProgramsSectionView
|
||||
parentId={parentId}
|
||||
sectionType={
|
||||
currentTab.sectionsView?.programSections ?? []
|
||||
}
|
||||
isUpcomingRecordingsEnabled={currentTab.sectionsView?.isLiveTvUpcomingRecordings}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentTab.viewType === LibraryTab.Upcoming) {
|
||||
return (
|
||||
<>
|
||||
<Box className='padded-top padded-left padded-right padded-bottom'>
|
||||
<LibraryViewMenu />
|
||||
</Box>
|
||||
|
||||
<UpcomingView parentId={parentId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentTab.viewType === LibraryTab.Genres) {
|
||||
return (
|
||||
<>
|
||||
<Box className='padded-top padded-left padded-right padded-bottom'>
|
||||
<LibraryViewMenu />
|
||||
</Box>
|
||||
|
||||
<GenresView
|
||||
parentId={parentId}
|
||||
collectionType={currentTab.collectionType}
|
||||
itemType={currentTab.itemType || []}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentTab.viewType === LibraryTab.Guide) {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
className='padded-top padded-left padded-right padded-bottom'
|
||||
sx={{
|
||||
position: 'relative',
|
||||
zIndex: 2
|
||||
}}
|
||||
>
|
||||
<LibraryViewMenu />
|
||||
</Box>
|
||||
|
||||
<GuideView />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemsView
|
||||
viewType={currentTab.viewType}
|
||||
parentId={parentId}
|
||||
collectionType={currentTab.collectionType}
|
||||
isPaginationEnabled={currentTab.isPaginationEnabled}
|
||||
isBtnPlayAllEnabled={currentTab.isBtnPlayAllEnabled}
|
||||
isBtnQueueEnabled={currentTab.isBtnQueueEnabled}
|
||||
isBtnShuffleEnabled={currentTab.isBtnShuffleEnabled}
|
||||
isBtnNewCollectionEnabled={currentTab.isBtnNewCollectionEnabled}
|
||||
isBtnNewPlaylistEnabled={currentTab.isBtnNewPlaylistEnabled}
|
||||
isBtnFilterEnabled={currentTab.isBtnFilterEnabled}
|
||||
isBtnGridListEnabled={currentTab.isBtnGridListEnabled}
|
||||
isBtnSortEnabled={currentTab.isBtnSortEnabled}
|
||||
isAlphabetPickerEnabled={currentTab.isAlphabetPickerEnabled}
|
||||
itemType={currentTab.itemType || []}
|
||||
noItemsMessage={
|
||||
currentTab.noItemsMessage || 'MessageNoItemsAvailable'
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageTabContent;
|
||||
@@ -1,141 +0,0 @@
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import type { Theme } from '@mui/material/styles';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import * as userSettings from 'scripts/settings/userSettings';
|
||||
import { LibraryViewSettings } from 'types/library';
|
||||
|
||||
interface PaginationProps {
|
||||
libraryViewSettings: LibraryViewSettings;
|
||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||
totalRecordCount: number;
|
||||
isPlaceholderData: boolean
|
||||
}
|
||||
|
||||
const Pagination: FC<PaginationProps> = ({
|
||||
libraryViewSettings,
|
||||
setLibraryViewSettings,
|
||||
totalRecordCount,
|
||||
isPlaceholderData
|
||||
}) => {
|
||||
const isSmallScreen = useMediaQuery((t: Theme) => t.breakpoints.up('sm'));
|
||||
|
||||
const limit = userSettings.libraryPageSize(undefined);
|
||||
const startIndex = libraryViewSettings.StartIndex ?? 0;
|
||||
const recordsStart = totalRecordCount ? startIndex + 1 : 0;
|
||||
const recordsEnd = limit ?
|
||||
Math.min(startIndex + limit, totalRecordCount) :
|
||||
totalRecordCount;
|
||||
const showControls = limit > 0 && limit < totalRecordCount;
|
||||
|
||||
const onNextPageClick = useCallback(() => {
|
||||
const newIndex = startIndex + limit;
|
||||
setLibraryViewSettings((prevState) => ({
|
||||
...prevState,
|
||||
StartIndex: newIndex
|
||||
}));
|
||||
}, [limit, setLibraryViewSettings, startIndex]);
|
||||
|
||||
const onPreviousPageClick = useCallback(() => {
|
||||
const newIndex = Math.max(0, startIndex - limit);
|
||||
setLibraryViewSettings((prevState) => ({
|
||||
...prevState,
|
||||
StartIndex: newIndex
|
||||
}));
|
||||
}, [limit, setLibraryViewSettings, startIndex]);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction='row'
|
||||
spacing={0.5}
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
flexGrow: {
|
||||
xs: 1,
|
||||
sm: 0
|
||||
},
|
||||
marginLeft: {
|
||||
xs: 0,
|
||||
sm: 0.5
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!isSmallScreen && (
|
||||
<Button
|
||||
color='inherit'
|
||||
variant='text'
|
||||
title={globalize.translate('Previous')}
|
||||
disabled={!showControls || startIndex == 0 || isPlaceholderData}
|
||||
onClick={onPreviousPageClick}
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 1,
|
||||
marginRight: 1
|
||||
}}
|
||||
>
|
||||
<Typography variant='body2'>
|
||||
{globalize.translate(
|
||||
'ListPaging',
|
||||
recordsStart,
|
||||
recordsEnd,
|
||||
totalRecordCount
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{isSmallScreen && (
|
||||
<ButtonGroup
|
||||
color='inherit'
|
||||
variant='text'
|
||||
>
|
||||
<Button
|
||||
title={globalize.translate('Previous')}
|
||||
disabled={!showControls || startIndex == 0 || isPlaceholderData}
|
||||
onClick={onPreviousPageClick}
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
title={globalize.translate('Next')}
|
||||
disabled={!showControls || startIndex + limit >= totalRecordCount || isPlaceholderData }
|
||||
onClick={onNextPageClick}
|
||||
>
|
||||
<ArrowForwardIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
|
||||
{!isSmallScreen && (
|
||||
<Button
|
||||
color='inherit'
|
||||
variant='text'
|
||||
title={globalize.translate('Next')}
|
||||
disabled={!showControls || startIndex + limit >= totalRecordCount || isPlaceholderData }
|
||||
onClick={onNextPageClick}
|
||||
>
|
||||
<ArrowForwardIcon />
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
||||
@@ -1,228 +0,0 @@
|
||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import Check from '@mui/icons-material/Check';
|
||||
import MoreVert from '@mui/icons-material/MoreVert';
|
||||
import Button from '@mui/material/Button';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import { LibraryViewSettings, ViewMode } from 'types/library';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
|
||||
const IMAGE_TYPE_EXCLUDED_VIEWS = [
|
||||
LibraryTab.Episodes,
|
||||
LibraryTab.Artists,
|
||||
LibraryTab.AlbumArtists,
|
||||
LibraryTab.Albums
|
||||
];
|
||||
|
||||
const imageTypesOptions = [
|
||||
{ label: 'Primary', value: ImageType.Primary },
|
||||
{ label: 'Banner', value: ImageType.Banner },
|
||||
{ label: 'Disc', value: ImageType.Disc },
|
||||
{ label: 'Logo', value: ImageType.Logo },
|
||||
{ label: 'Thumb', value: ImageType.Thumb }
|
||||
];
|
||||
|
||||
interface ViewSettingsButtonProps {
|
||||
viewType: LibraryTab;
|
||||
libraryViewSettings: LibraryViewSettings;
|
||||
setLibraryViewSettings: React.Dispatch<
|
||||
React.SetStateAction<LibraryViewSettings>
|
||||
>;
|
||||
}
|
||||
|
||||
const ViewSettingsButton: FC<ViewSettingsButtonProps> = ({
|
||||
viewType,
|
||||
libraryViewSettings,
|
||||
setLibraryViewSettings
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const id = open ? 'selectview-popover' : undefined;
|
||||
|
||||
const handleClick = useCallback((event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
}, []);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const name = event.target.name;
|
||||
|
||||
setLibraryViewSettings((prevState) => ({
|
||||
...prevState,
|
||||
[name]: event.target.checked
|
||||
}));
|
||||
},
|
||||
[setLibraryViewSettings]
|
||||
);
|
||||
|
||||
const onGridViewClick = useCallback(() => {
|
||||
setLibraryViewSettings(prevState => ({
|
||||
...prevState,
|
||||
ViewMode: ViewMode.GridView
|
||||
}));
|
||||
}, [ setLibraryViewSettings ]);
|
||||
|
||||
const onListViewClick = useCallback(() => {
|
||||
setLibraryViewSettings(prevState => ({
|
||||
...prevState,
|
||||
ViewMode: ViewMode.ListView
|
||||
}));
|
||||
}, [ setLibraryViewSettings ]);
|
||||
|
||||
const onSelectChange = useCallback(
|
||||
(event: SelectChangeEvent) => {
|
||||
setLibraryViewSettings((prevState) => ({
|
||||
...prevState,
|
||||
ImageType: event.target.value as ImageType
|
||||
}));
|
||||
},
|
||||
[setLibraryViewSettings]
|
||||
);
|
||||
|
||||
const isGridView = libraryViewSettings.ViewMode === ViewMode.GridView;
|
||||
const isImageTypeVisible = !IMAGE_TYPE_EXCLUDED_VIEWS.includes(viewType);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
title={globalize.translate('ViewSettings')}
|
||||
aria-describedby={id}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<MoreVert />
|
||||
</Button>
|
||||
<Popover
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center'
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiFormControl-root': { m: 1, width: 220 }
|
||||
}}
|
||||
>
|
||||
<MenuList>
|
||||
|
||||
<MenuItem
|
||||
onClick={onGridViewClick}
|
||||
>
|
||||
{isGridView && (
|
||||
<ListItemIcon><Check /></ListItemIcon>
|
||||
)}
|
||||
<ListItemText inset={!isGridView}>
|
||||
{globalize.translate('GridView')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={onListViewClick}
|
||||
>
|
||||
{!isGridView && (
|
||||
<ListItemIcon><Check /></ListItemIcon>
|
||||
)}
|
||||
<ListItemText inset={isGridView}>
|
||||
{globalize.translate('ListView')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
{isGridView && (
|
||||
<>
|
||||
<Divider />
|
||||
{isImageTypeVisible && (
|
||||
<>
|
||||
<FormControl>
|
||||
<InputLabel>
|
||||
<Typography component='span'>
|
||||
{globalize.translate('LabelImageType')}
|
||||
</Typography>
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={libraryViewSettings.ImageType}
|
||||
label={globalize.translate('LabelImageType')}
|
||||
onChange={onSelectChange}
|
||||
>
|
||||
{imageTypesOptions.map((imageType) => (
|
||||
<MenuItem
|
||||
key={imageType.value}
|
||||
value={imageType.value}
|
||||
>
|
||||
<Typography component='span'>
|
||||
{globalize.translate(imageType.label)}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
<FormControl>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={libraryViewSettings.ShowTitle}
|
||||
onChange={handleChange}
|
||||
name='ShowTitle'
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('ShowTitle')}
|
||||
/>
|
||||
{isImageTypeVisible && (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={libraryViewSettings.ShowYear}
|
||||
onChange={handleChange}
|
||||
name='ShowYear'
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('ShowYear')}
|
||||
/>
|
||||
)}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={libraryViewSettings.CardLayout}
|
||||
onChange={handleChange}
|
||||
name='CardLayout'
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('EnableCardLayout')}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
</MenuList>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewSettingsButton;
|
||||
@@ -1,18 +0,0 @@
|
||||
import { AsyncRoute } from 'components/router/AsyncRoute';
|
||||
import { AppType } from 'constants/appType';
|
||||
|
||||
export const ASYNC_USER_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'home', type: AppType.Experimental },
|
||||
{ path: 'homevideos', type: AppType.Experimental },
|
||||
{ path: 'livetv', type: AppType.Experimental },
|
||||
{ path: 'movies', type: AppType.Experimental },
|
||||
{ path: 'music', type: AppType.Experimental },
|
||||
{ path: 'books', type: AppType.Experimental },
|
||||
{ path: 'musicvideos', type: AppType.Experimental },
|
||||
{ path: 'mypreferencesdisplay', page: 'user/display', type: AppType.Experimental },
|
||||
{ path: 'mypreferencesmenu', page: 'user/settings' },
|
||||
{ path: 'quickconnect', page: 'quickConnect' },
|
||||
{ path: 'search' },
|
||||
{ path: 'tv', page: 'shows', type: AppType.Experimental },
|
||||
{ path: 'userprofile', page: 'user/userprofile' }
|
||||
];
|
||||
@@ -1,60 +0,0 @@
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React, { FC } from 'react';
|
||||
import useCurrentTab from 'hooks/useCurrentTab';
|
||||
import Page from 'components/Page';
|
||||
import PageTabContent from '../../components/library/PageTabContent';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
|
||||
import { BookSuggestionsSectionsView } from 'types/sections';
|
||||
|
||||
const booksTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Books,
|
||||
collectionType: CollectionType.Books,
|
||||
itemType: [BaseItemKind.Book]
|
||||
};
|
||||
|
||||
const suggestionsTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Suggestions,
|
||||
collectionType: CollectionType.Books,
|
||||
sectionsView: BookSuggestionsSectionsView
|
||||
};
|
||||
|
||||
const genresTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Genres,
|
||||
collectionType: CollectionType.Books,
|
||||
itemType: [BaseItemKind.Book]
|
||||
};
|
||||
|
||||
const favoritesTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Favorites,
|
||||
collectionType: CollectionType.Books,
|
||||
itemType: [BaseItemKind.Book]
|
||||
};
|
||||
|
||||
const booksTabMapping: LibraryTabMapping = {
|
||||
0: booksTabContent,
|
||||
1: suggestionsTabContent,
|
||||
2: genresTabContent,
|
||||
3: favoritesTabContent
|
||||
};
|
||||
|
||||
const Books: FC = () => {
|
||||
const { libraryId, activeTab } = useCurrentTab();
|
||||
const currentTab = booksTabMapping[activeTab];
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='booksPage'
|
||||
className='mainAnimatedPage libraryPage pageWithAbsoluteTabs withTabs'
|
||||
>
|
||||
<PageTabContent
|
||||
key={`${currentTab.viewType} - ${libraryId}`}
|
||||
currentTab={currentTab}
|
||||
parentId={libraryId}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Books;
|
||||
@@ -1,71 +0,0 @@
|
||||
import React, { FC } from 'react';
|
||||
import useCurrentTab from 'hooks/useCurrentTab';
|
||||
import Page from 'components/Page';
|
||||
import PageTabContent from '../../components/library/PageTabContent';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
|
||||
import { ProgramSectionsView, RecordingsSectionsView, ScheduleSectionsView } from 'types/sections';
|
||||
|
||||
const seriestimersTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.SeriesTimers,
|
||||
isPaginationEnabled: false,
|
||||
isBtnFilterEnabled: false,
|
||||
isBtnGridListEnabled: false,
|
||||
isBtnSortEnabled: false,
|
||||
isAlphabetPickerEnabled: false
|
||||
};
|
||||
|
||||
const scheduleTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Schedule,
|
||||
sectionsView: ScheduleSectionsView
|
||||
};
|
||||
|
||||
const recordingsTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Recordings,
|
||||
sectionsView: RecordingsSectionsView
|
||||
};
|
||||
|
||||
const channelsTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Channels,
|
||||
isBtnGridListEnabled: false,
|
||||
isBtnSortEnabled: false,
|
||||
isAlphabetPickerEnabled: false
|
||||
};
|
||||
|
||||
const programsTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Programs,
|
||||
sectionsView: ProgramSectionsView
|
||||
};
|
||||
|
||||
const guideTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Guide
|
||||
};
|
||||
|
||||
const liveTvTabMapping: LibraryTabMapping = {
|
||||
0: programsTabContent,
|
||||
1: guideTabContent,
|
||||
2: channelsTabContent,
|
||||
3: recordingsTabContent,
|
||||
4: scheduleTabContent,
|
||||
5: seriestimersTabContent
|
||||
};
|
||||
|
||||
const LiveTv: FC = () => {
|
||||
const { libraryId, activeTab } = useCurrentTab();
|
||||
const currentTab = liveTvTabMapping[activeTab];
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='liveTvPage'
|
||||
className='mainAnimatedPage libraryPage collectionEditorPage pageWithAbsoluteTabs withTabs'
|
||||
>
|
||||
<PageTabContent
|
||||
key={`${currentTab.viewType} - ${libraryId}`}
|
||||
currentTab={currentTab}
|
||||
parentId={libraryId}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default LiveTv;
|
||||
@@ -1,58 +0,0 @@
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React, { type FC } from 'react';
|
||||
import useCurrentTab from 'hooks/useCurrentTab';
|
||||
import Page from 'components/Page';
|
||||
import PageTabContent from '../../components/library/PageTabContent';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
|
||||
import { MusicVideoSuggestionsSectionsView } from 'types/sections';
|
||||
|
||||
const musicVideosTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.MusicVideos,
|
||||
collectionType: CollectionType.Musicvideos,
|
||||
isBtnPlayAllEnabled: true,
|
||||
isBtnShuffleEnabled: true,
|
||||
itemType: [BaseItemKind.MusicVideo]
|
||||
};
|
||||
|
||||
const suggestionsTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Suggestions,
|
||||
collectionType: CollectionType.Musicvideos,
|
||||
sectionsView: MusicVideoSuggestionsSectionsView
|
||||
};
|
||||
|
||||
const foldersTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Folders,
|
||||
collectionType: CollectionType.Musicvideos,
|
||||
isBtnPlayAllEnabled: false,
|
||||
isBtnShuffleEnabled: false,
|
||||
itemType: [BaseItemKind.Folder, BaseItemKind.MusicVideo]
|
||||
};
|
||||
|
||||
const musicVideosTabMapping: LibraryTabMapping = {
|
||||
0: musicVideosTabContent,
|
||||
1: suggestionsTabContent,
|
||||
2: foldersTabContent
|
||||
};
|
||||
|
||||
const MusicVideos: FC = () => {
|
||||
const { libraryId, activeTab } = useCurrentTab();
|
||||
const currentTab = musicVideosTabMapping[activeTab];
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='musicvideos'
|
||||
className='mainAnimatedPage libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs'
|
||||
backDropType='video, photo'
|
||||
>
|
||||
<PageTabContent
|
||||
key={`${currentTab.viewType} - ${libraryId}`}
|
||||
currentTab={currentTab}
|
||||
parentId={libraryId}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default MusicVideos;
|
||||
+1
-1
@@ -5,7 +5,7 @@ function reload(context, itemId) {
|
||||
loading.show();
|
||||
|
||||
if (itemId) {
|
||||
import('../components/metadataEditor/metadataEditor').then(({ default: metadataEditor }) => {
|
||||
import('components/metadataEditor/metadataEditor').then(({ default: metadataEditor }) => {
|
||||
metadataEditor.embed(context.querySelector('.editPageInnerContent'), itemId, ApiClient.serverInfo().Id);
|
||||
});
|
||||
} else {
|
||||
@@ -1,5 +1,8 @@
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
|
||||
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by';
|
||||
import { getStudiosApi } from '@jellyfin/sdk/lib/utils/api/studios-api';
|
||||
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import { getBackdropShape, getPortraitShape, getSquareShape } from 'components/cardbuilder/utils/shape';
|
||||
@@ -92,6 +95,15 @@ function getSections() {
|
||||
centerText: true,
|
||||
overlayPlayButton: true,
|
||||
coverImage: true
|
||||
}, {
|
||||
name: 'Studios',
|
||||
types: BaseItemKind.Studio,
|
||||
shape: getBackdropShape(enableScrollX()),
|
||||
preferThumb: true,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
centerText: true,
|
||||
overlayPlayButton: true
|
||||
}, {
|
||||
name: 'People',
|
||||
types: 'Person',
|
||||
@@ -189,6 +201,25 @@ function getFetchDataFn(section) {
|
||||
options.Limit = 20;
|
||||
const userId = apiClient.getCurrentUserId();
|
||||
|
||||
if (section.types === BaseItemKind.Studio) {
|
||||
const api = ServerConnections.getApi(apiClient.serverId());
|
||||
if (!api) {
|
||||
console.error('[Favorites] no Api instance available for server', apiClient.serverId());
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return getStudiosApi(api)
|
||||
.getStudios({
|
||||
userId,
|
||||
isFavorite: true,
|
||||
fields: [ItemFields.PrimaryImageAspectRatio],
|
||||
enableImageTypes: [ImageType.Thumb],
|
||||
enableTotalRecordCount: false,
|
||||
limit: options.Limit
|
||||
})
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
||||
if (section.types === 'MusicArtist') {
|
||||
return apiClient.getArtists(userId, options);
|
||||
}
|
||||
@@ -350,4 +381,3 @@ class FavoritesTab {
|
||||
}
|
||||
|
||||
export default FavoritesTab;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import TabbedView from '../components/tabbedview/tabbedview';
|
||||
import globalize from '../lib/globalize';
|
||||
import '../elements/emby-tabs/emby-tabs';
|
||||
import '../elements/emby-button/emby-button';
|
||||
import '../elements/emby-scroller/emby-scroller';
|
||||
import LibraryMenu from '../scripts/libraryMenu';
|
||||
import TabbedView from 'components/tabbedview/tabbedview';
|
||||
import globalize from 'lib/globalize';
|
||||
import 'elements/emby-tabs/emby-tabs';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-scroller/emby-scroller';
|
||||
import LibraryMenu from 'scripts/libraryMenu';
|
||||
|
||||
class HomeView extends TabbedView {
|
||||
setTitle() {
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as userSettings from '../scripts/settings/userSettings';
|
||||
import focusManager from '../components/focusManager';
|
||||
import homeSections from '../components/homesections/homesections';
|
||||
import * as userSettings from 'scripts/settings/userSettings';
|
||||
import focusManager from 'components/focusManager';
|
||||
import homeSections from 'components/homesections/homesections';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
|
||||
import '../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
||||
class HomeTab {
|
||||
constructor(view, params) {
|
||||
+7
@@ -232,6 +232,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="collectionsCollapsible" class="verticalSection detailVerticalSection verticalSection-extrabottompadding hide">
|
||||
<h2 class="sectionTitle sectionTitle-cards padded-right">${Collections}</h2>
|
||||
<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale no-padding" data-centerfocus="true">
|
||||
<div id="collectionsContent" is="emby-itemscontainer" class="scrollSlider focuscontainer-x itemsContainer collectionsContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="similarCollapsible" class="verticalSection detailVerticalSection verticalSection-extrabottompadding hide">
|
||||
<h2 class="sectionTitle sectionTitle-cards padded-right">${HeaderMoreLikeThis}</h2>
|
||||
<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale no-padding" data-centerfocus="true">
|
||||
Regular → Executable
+151
-39
@@ -1,6 +1,8 @@
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
|
||||
import { PersonKind } from '@jellyfin/sdk/lib/generated-client/models/person-kind';
|
||||
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 DOMPurify from 'dompurify';
|
||||
import escapeHtml from 'escape-html';
|
||||
@@ -25,6 +27,7 @@ import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import itemShortcuts from 'components/shortcuts';
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
import { EventType } from 'constants/eventType';
|
||||
import { ItemAction } from 'constants/itemAction';
|
||||
import globalize from 'lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
@@ -38,7 +41,7 @@ import * as userSettings from 'scripts/settings/userSettings';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import Events from 'utils/events';
|
||||
import { getItemBackdropImageUrl } from 'utils/jellyfin-apiclient/backdropImage';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
import { OutboundWebSocketMessageType } from '@jellyfin/sdk/lib/websocket';
|
||||
|
||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
@@ -59,7 +62,7 @@ const LIST_VIEW_TYPES = [
|
||||
];
|
||||
|
||||
function autoFocus(container) {
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(container);
|
||||
});
|
||||
}
|
||||
@@ -176,7 +179,7 @@ function renderSeriesTimerEditor(page, item, apiClient, user) {
|
||||
}
|
||||
|
||||
if (user.Policy.EnableLiveTvManagement) {
|
||||
import('../../components/recordingcreator/seriesrecordingeditor').then(({ default: seriesRecordingEditor }) => {
|
||||
import('components/recordingcreator/seriesrecordingeditor').then(({ default: seriesRecordingEditor }) => {
|
||||
seriesRecordingEditor.embed(item, apiClient.serverId(), {
|
||||
context: page.querySelector('.seriesRecordingEditor')
|
||||
});
|
||||
@@ -706,7 +709,7 @@ function showRecordingFields(instance, page, item, user) {
|
||||
const recordingFieldsElement = page.querySelector('.recordingFields');
|
||||
|
||||
if (item.Type == 'Program' && user.Policy.EnableLiveTvManagement) {
|
||||
import('../../components/recordingcreator/recordingfields').then(({ default: RecordingFields }) => {
|
||||
import('components/recordingcreator/recordingfields').then(({ default: RecordingFields }) => {
|
||||
instance.currentRecordingFields = new RecordingFields({
|
||||
parent: recordingFieldsElement,
|
||||
programId: item.Id,
|
||||
@@ -825,7 +828,7 @@ function setInitialCollapsibleState(page, item, apiClient, context, user) {
|
||||
} else if (item.Type == 'Studio' || item.Type == 'Person' || item.Type == 'Genre' || item.Type == 'MusicGenre' || item.Type == 'MusicArtist') {
|
||||
page.querySelector('#listChildrenCollapsible').classList.remove('hide');
|
||||
page.querySelector('#childrenCollapsible').classList.add('hide');
|
||||
renderItemsByName(page, item);
|
||||
renderItemsByName(page, item, user);
|
||||
} else if (item.IsFolder) {
|
||||
if (item.Type == 'BoxSet') {
|
||||
page.querySelector('#listChildrenCollapsible').classList.add('hide');
|
||||
@@ -859,6 +862,9 @@ function setInitialCollapsibleState(page, item, apiClient, context, user) {
|
||||
(item.People || []).forEach(p => {
|
||||
if (p.Type === PersonKind.GuestStar) {
|
||||
guestCast.push(p);
|
||||
} else if (p.Type === PersonKind.Artist || p.Type === PersonKind.AlbumArtist) {
|
||||
// TODO remove this exclusion when artists are migrated to the persons endpoint
|
||||
return;
|
||||
} else {
|
||||
cast.push(p);
|
||||
}
|
||||
@@ -980,10 +986,11 @@ function renderDetails(page, instance, item, apiClient, context) {
|
||||
const itemDetailsGroup = page.querySelector('.itemDetailsGroup');
|
||||
|
||||
if (itemDetailsGroup) {
|
||||
itemDetailsGroup.replaceChildren();
|
||||
itemDetailsGroup.innerHTML = '';
|
||||
|
||||
const metadataTypes = [
|
||||
PersonKind.Author,
|
||||
PersonKind.Creator,
|
||||
PersonKind.Director,
|
||||
PersonKind.Writer,
|
||||
BaseItemKind.Studio,
|
||||
@@ -999,6 +1006,7 @@ function renderDetails(page, instance, item, apiClient, context) {
|
||||
}
|
||||
}
|
||||
|
||||
renderItemCollections(page, item, apiClient, context);
|
||||
renderSimilarItems(page, item, context);
|
||||
renderMoreFromSeason(page, item, apiClient);
|
||||
renderMoreFromArtist(page, item, apiClient);
|
||||
@@ -1155,6 +1163,57 @@ function renderMoreFromArtist(view, item, apiClient) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderItemCollections(page, item, apiClient, context) {
|
||||
const section = page.querySelector('#collectionsCollapsible');
|
||||
|
||||
if (!section) {
|
||||
return;
|
||||
}
|
||||
|
||||
const api = ServerConnections.getApi(item.ServerId);
|
||||
const userId = apiClient.getCurrentUserId();
|
||||
|
||||
if (!api) {
|
||||
console.error('[renderItemCollections] No Api instance available for server', item.ServerId);
|
||||
section.classList.add('hide');
|
||||
return;
|
||||
}
|
||||
|
||||
getLibraryApi(api)
|
||||
.getItemCollections({
|
||||
itemId: item.Id,
|
||||
userId,
|
||||
fields: [ItemFields.PrimaryImageAspectRatio]
|
||||
})
|
||||
.then(response => response.data?.Items || [])
|
||||
.then(collectionItems => {
|
||||
if (!collectionItems.length) {
|
||||
section.classList.add('hide');
|
||||
return;
|
||||
}
|
||||
|
||||
section.classList.remove('hide');
|
||||
cardBuilder.buildCards(collectionItems, {
|
||||
parentContainer: section,
|
||||
itemsContainer: section.querySelector('.collectionsContent'),
|
||||
shape: 'overflowPortrait',
|
||||
sectionTitleTagName: 'h2',
|
||||
scalable: true,
|
||||
centerText: true,
|
||||
showTitle: true,
|
||||
showParentTitle: false,
|
||||
overlayText: false,
|
||||
overlayPlayButton: false,
|
||||
coverImage: true,
|
||||
// Similar to "More Like This"
|
||||
showYear: item.Type === 'Movie' || item.Type === 'Trailer' || item.Type === 'Series',
|
||||
context
|
||||
});
|
||||
}).catch(() => {
|
||||
section.classList.add('hide');
|
||||
});
|
||||
}
|
||||
|
||||
function renderSimilarItems(page, item, context) {
|
||||
const similarCollapsible = page.querySelector('#similarCollapsible');
|
||||
|
||||
@@ -1450,14 +1509,14 @@ function renderChildren(page, item) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderItemsByName(page, item) {
|
||||
import('../../scripts/itemsByName').then(({ default: ItemsByName }) => {
|
||||
ItemsByName.renderItems(page, item);
|
||||
function renderItemsByName(page, item, user) {
|
||||
import('scripts/itemsByName').then(({ default: ItemsByName }) => {
|
||||
ItemsByName.renderItems(page, item, user);
|
||||
});
|
||||
}
|
||||
|
||||
function renderPlaylistItems(page, item) {
|
||||
import('../../scripts/playlistViewer').then(({ default: PlaylistViewer }) => {
|
||||
import('scripts/playlistViewer').then(({ default: PlaylistViewer }) => {
|
||||
PlaylistViewer.render(page, item);
|
||||
});
|
||||
}
|
||||
@@ -1734,7 +1793,7 @@ function renderScenes(page, item) {
|
||||
page.querySelector('#scenesCollapsible').classList.remove('hide');
|
||||
const scenesContent = page.querySelector('#scenesContent');
|
||||
|
||||
import('../../components/cardbuilder/chaptercardbuilder').then(({ default: chaptercardbuilder }) => {
|
||||
import('components/cardbuilder/chaptercardbuilder').then(({ default: chaptercardbuilder }) => {
|
||||
chaptercardbuilder.buildChapterCards(item, chapters, {
|
||||
itemsContainer: scenesContent,
|
||||
backdropShape: 'overflowBackdrop',
|
||||
@@ -1776,7 +1835,7 @@ function renderCast(page, item, people) {
|
||||
page.querySelector('#castCollapsible').classList.remove('hide');
|
||||
const castContent = page.querySelector('#castContent');
|
||||
|
||||
import('../../components/cardbuilder/peoplecardbuilder').then(({ default: peoplecardbuilder }) => {
|
||||
import('components/cardbuilder/peoplecardbuilder').then(({ default: peoplecardbuilder }) => {
|
||||
peoplecardbuilder.buildPeopleCards(people, {
|
||||
itemsContainer: castContent,
|
||||
coverImage: true,
|
||||
@@ -1796,7 +1855,7 @@ function renderGuestCast(page, item, people) {
|
||||
page.querySelector('#guestCastCollapsible').classList.remove('hide');
|
||||
const guestCastContent = page.querySelector('#guestCastContent');
|
||||
|
||||
import('../../components/cardbuilder/peoplecardbuilder').then(({ default: peoplecardbuilder }) => {
|
||||
import('components/cardbuilder/peoplecardbuilder').then(({ default: peoplecardbuilder }) => {
|
||||
peoplecardbuilder.buildPeopleCards(people, {
|
||||
itemsContainer: guestCastContent,
|
||||
coverImage: true,
|
||||
@@ -1840,7 +1899,7 @@ window.ItemDetailPage = new ItemDetailPage();
|
||||
|
||||
export default function (view, params) {
|
||||
function getApiClient() {
|
||||
return params.serverId ? ServerConnections.getApiClient(params.serverId) : ApiClient;
|
||||
return params.serverId ? ServerConnections.getApiClient(params.serverId) : ServerConnections.currentApiClient();
|
||||
}
|
||||
|
||||
function reload(instance, page, pageParams) {
|
||||
@@ -1857,16 +1916,21 @@ export default function (view, params) {
|
||||
}
|
||||
|
||||
function splitVersions(instance, page, apiClient, pageParams) {
|
||||
confirm('Are you sure you wish to split the media sources into separate items?', 'Split Media Apart').then(function () {
|
||||
loading.show();
|
||||
apiClient.ajax({
|
||||
type: 'DELETE',
|
||||
url: apiClient.getUrl('Videos/' + pageParams.id + '/AlternateSources')
|
||||
}).then(function () {
|
||||
loading.hide();
|
||||
confirm(globalize.translate('ConfirmSplitMedia'), globalize.translate('HeaderSplitMedia'))
|
||||
.then(() => {
|
||||
loading.show();
|
||||
return apiClient.ajax({
|
||||
type: 'DELETE',
|
||||
url: apiClient.getUrl('Videos/' + pageParams.id + '/AlternateSources')
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
reload(instance, page, pageParams);
|
||||
Events.trigger(document, EventType.REFRESH_NEEDED);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getPlayOptions(startPosition) {
|
||||
@@ -1926,7 +1990,7 @@ export default function (view, params) {
|
||||
}
|
||||
|
||||
function onCancelSeriesTimerClick() {
|
||||
import('../../components/recordingcreator/recordinghelper').then(({ default: recordingHelper }) => {
|
||||
import('components/recordingcreator/recordinghelper').then(({ default: recordingHelper }) => {
|
||||
recordingHelper.cancelSeriesTimerWithConfirmation(currentItem.Id, currentItem.ServerId).then(function () {
|
||||
Dashboard.navigate('livetv');
|
||||
});
|
||||
@@ -1934,7 +1998,7 @@ export default function (view, params) {
|
||||
}
|
||||
|
||||
function onCancelTimerClick() {
|
||||
import('../../components/recordingcreator/recordinghelper').then(({ default: recordingHelper }) => {
|
||||
import('components/recordingcreator/recordinghelper').then(({ default: recordingHelper }) => {
|
||||
recordingHelper.cancelTimer(ServerConnections.getApiClient(currentItem.ServerId), currentItem.TimerId).then(function () {
|
||||
reload(self, view, params);
|
||||
});
|
||||
@@ -1946,7 +2010,12 @@ export default function (view, params) {
|
||||
}
|
||||
|
||||
function onDownloadClick() {
|
||||
const api = toApi(getApiClient());
|
||||
const api = ServerConnections.getApi(currentItem.ServerId);
|
||||
if (!api) {
|
||||
console.error('[ItemDetails] no Api instance available for server', currentItem.ServerId);
|
||||
return;
|
||||
}
|
||||
|
||||
const url = getLibraryApi(api).getDownloadUrl({ itemId: currentItem.Id });
|
||||
download([{
|
||||
url,
|
||||
@@ -1992,21 +2061,18 @@ export default function (view, params) {
|
||||
setTrailerButtonVisibility(view, currentItem);
|
||||
}
|
||||
|
||||
function onWebSocketMessage(e, data) {
|
||||
const msg = data;
|
||||
function onUserDataChanged({ Data }) {
|
||||
const apiClient = getApiClient();
|
||||
|
||||
if (msg.MessageType === 'UserDataChanged' && currentItem && msg.Data.UserId == apiClient.getCurrentUserId()) {
|
||||
const key = currentItem.UserData.Key;
|
||||
const userData = msg.Data.UserDataList.filter(function (u) {
|
||||
return u.Key == key;
|
||||
})[0];
|
||||
if (!currentItem || Data?.UserId != apiClient.getCurrentUserId()) return;
|
||||
|
||||
if (userData) {
|
||||
currentItem.UserData = userData;
|
||||
reloadPlayButtons(view, currentItem);
|
||||
autoFocus(view);
|
||||
}
|
||||
const key = currentItem.UserData.Key;
|
||||
const userData = (Data?.UserDataList ?? []).find(u => u.Key == key);
|
||||
|
||||
if (userData) {
|
||||
currentItem.UserData = userData;
|
||||
reloadPlayButtons(view, currentItem);
|
||||
autoFocus(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2036,6 +2102,7 @@ export default function (view, params) {
|
||||
renderAudioSelections(view, self._currentPlaybackMediaSources);
|
||||
renderSubtitleSelections(view, self._currentPlaybackMediaSources);
|
||||
updateMiscInfo();
|
||||
refreshSelectedVersion();
|
||||
});
|
||||
view.addEventListener('viewshow', function (e) {
|
||||
const page = this;
|
||||
@@ -2052,14 +2119,18 @@ export default function (view, params) {
|
||||
reload(self, page, params);
|
||||
}
|
||||
|
||||
Events.on(apiClient, 'message', onWebSocketMessage);
|
||||
self._unsubscribeUserData = apiClient.subscribe(
|
||||
[OutboundWebSocketMessageType.UserDataChanged],
|
||||
onUserDataChanged
|
||||
);
|
||||
Events.on(playbackManager, 'playerchange', onPlayerChange);
|
||||
|
||||
itemShortcuts.on(view.querySelector('.nameContainer'));
|
||||
});
|
||||
view.addEventListener('viewbeforehide', function () {
|
||||
itemShortcuts.off(view.querySelector('.nameContainer'));
|
||||
Events.off(apiClient, 'message', onWebSocketMessage);
|
||||
self._unsubscribeUserData?.();
|
||||
self._unsubscribeUserData = null;
|
||||
Events.off(playbackManager, 'playerchange', onPlayerChange);
|
||||
libraryMenu.setTransparentMenu(false);
|
||||
});
|
||||
@@ -2081,5 +2152,46 @@ export default function (view, params) {
|
||||
});
|
||||
}
|
||||
|
||||
// When the user picks an alternate version, fetch that Video item's own DTO
|
||||
// and refresh the play/user-data buttons so resume position, watched state, and
|
||||
// stack-part count reflect the selected version rather than the primary's.
|
||||
function refreshSelectedVersion() {
|
||||
const selectedId = view.querySelector('.selectSource').value;
|
||||
if (!selectedId || !currentItem || selectedId === currentItem.Id) {
|
||||
reloadPlayButtons(view, currentItem);
|
||||
reloadUserDataButtons(view, currentItem);
|
||||
return;
|
||||
}
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
|
||||
const api = ServerConnections.getApi(currentItem.ServerId);
|
||||
if (!api) {
|
||||
console.error('[ItemDetails] no Api instance available for server', currentItem.ServerId);
|
||||
return;
|
||||
}
|
||||
|
||||
getUserLibraryApi(api).getItem({
|
||||
userId: apiClient?.getCurrentUserId(),
|
||||
itemId: selectedId
|
||||
}).then(function ({ data: altItem }) {
|
||||
if (view.querySelector('.selectSource').value !== selectedId) {
|
||||
return;
|
||||
}
|
||||
// Keep primary's shared metadata (overview, cast, etc.) and override the
|
||||
// fields that are intrinsic to the playback target (UserData, runtime, parts).
|
||||
const merged = {
|
||||
...currentItem,
|
||||
UserData: altItem.UserData,
|
||||
RunTimeTicks: altItem.RunTimeTicks,
|
||||
PartCount: altItem.PartCount,
|
||||
MediaStreams: altItem.MediaStreams
|
||||
};
|
||||
reloadPlayButtons(view, merged);
|
||||
reloadUserDataButtons(view, merged);
|
||||
}).catch(function (err) {
|
||||
console.error('failed to load alternate version item', err);
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import globalize from '../lib/globalize';
|
||||
import listView from '../components/listview/listview';
|
||||
import * as userSettings from '../scripts/settings/userSettings';
|
||||
import focusManager from '../components/focusManager';
|
||||
import cardBuilder from '../components/cardbuilder/cardBuilder';
|
||||
import loading from '../components/loading/loading';
|
||||
import AlphaNumericShortcuts from '../scripts/alphanumericshortcuts';
|
||||
import libraryBrowser from '../scripts/libraryBrowser';
|
||||
import { playbackManager } from '../components/playback/playbackmanager';
|
||||
import AlphaPicker from '../components/alphaPicker/alphaPicker';
|
||||
import globalize from 'lib/globalize';
|
||||
import listView from 'components/listview/listview';
|
||||
import * as userSettings from 'scripts/settings/userSettings';
|
||||
import focusManager from 'components/focusManager';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import loading from 'components/loading/loading';
|
||||
import AlphaNumericShortcuts from 'scripts/alphanumericshortcuts';
|
||||
import libraryBrowser from 'scripts/libraryBrowser';
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import AlphaPicker from 'components/alphaPicker/alphaPicker';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import '../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import '../elements/emby-scroller/emby-scroller';
|
||||
import LibraryMenu from '../scripts/libraryMenu';
|
||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import 'elements/emby-scroller/emby-scroller';
|
||||
import LibraryMenu from 'scripts/libraryMenu';
|
||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by';
|
||||
import { stopMultiSelect } from 'components/multiSelect/multiSelect';
|
||||
@@ -315,7 +315,7 @@ function getItems(instance, params, item, sortBy, startIndex, limit) {
|
||||
} else if (item.CollectionType === CollectionType.Tvshows) {
|
||||
query.IncludeItemTypes = 'Series';
|
||||
} else if (item.Type === 'Genre') {
|
||||
query.IncludeItemTypes = 'Movie,Series,Video';
|
||||
query.IncludeItemTypes = 'Audiobook,Book,BoxSet,Movie,Series,Video';
|
||||
} else if (item.Type === 'Person') {
|
||||
query.IncludeItemTypes = params.type;
|
||||
}
|
||||
@@ -332,7 +332,11 @@ function getItems(instance, params, item, sortBy, startIndex, limit) {
|
||||
SortBy: sortBy
|
||||
};
|
||||
|
||||
if (sortBy === 'Random') {
|
||||
if (params.type) {
|
||||
instance.queryRecursive = true;
|
||||
query.Recursive = true;
|
||||
query.IncludeItemTypes = params.type;
|
||||
} else if (sortBy === 'Random') {
|
||||
instance.queryRecursive = true;
|
||||
query.IncludeItemTypes = 'Video,Movie,Series,Music,MusicVideo';
|
||||
query.Recursive = true;
|
||||
@@ -359,7 +363,7 @@ function getItem(params) {
|
||||
function showViewSettingsMenu() {
|
||||
const instance = this;
|
||||
|
||||
import('../components/viewSettings/viewSettings').then(({ default: ViewSettings }) => {
|
||||
import('components/viewSettings/viewSettings').then(({ default: ViewSettings }) => {
|
||||
new ViewSettings().show({
|
||||
settingsKey: instance.getSettingsKey(),
|
||||
settings: instance.getViewSettings(),
|
||||
@@ -374,7 +378,7 @@ function showViewSettingsMenu() {
|
||||
function showFilterMenu() {
|
||||
const instance = this;
|
||||
|
||||
import('../components/filtermenu/filtermenu').then(({ default: FilterMenu }) => {
|
||||
import('components/filtermenu/filtermenu').then(({ default: FilterMenu }) => {
|
||||
new FilterMenu().show({
|
||||
settingsKey: instance.getSettingsKey(),
|
||||
settings: instance.getFilters(),
|
||||
@@ -393,7 +397,7 @@ function showFilterMenu() {
|
||||
function showSortMenu() {
|
||||
const instance = this;
|
||||
|
||||
import('../components/sortmenu/sortmenu').then(({ default: SortMenu }) => {
|
||||
import('components/sortmenu/sortmenu').then(({ default: SortMenu }) => {
|
||||
new SortMenu().show({
|
||||
settingsKey: instance.getSettingsKey(),
|
||||
settings: instance.getSortValues(),
|
||||
@@ -411,7 +415,7 @@ function showSortMenu() {
|
||||
function onNewItemClick() {
|
||||
const instance = this;
|
||||
|
||||
import('../components/playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
||||
import('components/playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
||||
const playlistEditor = new PlaylistEditor();
|
||||
playlistEditor.show({
|
||||
items: [],
|
||||
@@ -796,7 +800,7 @@ class ItemsView {
|
||||
}
|
||||
|
||||
function autoFocus() {
|
||||
import('../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(view);
|
||||
});
|
||||
}
|
||||
+9
-9
@@ -1,12 +1,12 @@
|
||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
||||
import imageLoader from '../../components/images/imageLoader';
|
||||
import libraryBrowser from '../../scripts/libraryBrowser';
|
||||
import loading from '../../components/loading/loading';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import Events from '../../utils/events.ts';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import imageLoader from 'components/images/imageLoader';
|
||||
import libraryBrowser from 'scripts/libraryBrowser';
|
||||
import loading from 'components/loading/loading';
|
||||
import * as userSettings from 'scripts/settings/userSettings';
|
||||
import Events from 'utils/events';
|
||||
import { setFilterStatus } from 'components/filterdialog/filterIndicator';
|
||||
|
||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
||||
export default function (view, params, tabContent) {
|
||||
function getPageData() {
|
||||
@@ -101,7 +101,7 @@ export default function (view, params, tabContent) {
|
||||
}
|
||||
|
||||
function showFilterMenu(context) {
|
||||
import('../../components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
|
||||
import('components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
|
||||
const filterDialog = new FilterDialog({
|
||||
query: getQuery(),
|
||||
mode: 'livetvchannels',
|
||||
@@ -127,7 +127,7 @@ export default function (view, params, tabContent) {
|
||||
loading.hide();
|
||||
isLoading = false;
|
||||
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(context);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import Guide from '../../components/guide/guide';
|
||||
import Guide from 'components/guide/guide';
|
||||
|
||||
export default function (view, params, tabContent) {
|
||||
let guideInstance;
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
||||
import imageLoader from '../../components/images/imageLoader';
|
||||
import loading from '../../components/loading/loading';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import imageLoader from 'components/images/imageLoader';
|
||||
import loading from 'components/loading/loading';
|
||||
import 'elements/emby-button/paper-icon-button-light';
|
||||
import 'elements/emby-button/emby-button';
|
||||
|
||||
function renderTimers(context, timers) {
|
||||
const html = cardBuilder.getCardsHtml({
|
||||
+1
-1
@@ -51,7 +51,7 @@ function loadRecommendedPrograms(page) {
|
||||
});
|
||||
loading.hide();
|
||||
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getLyricsApi } from '@jellyfin/sdk/lib/utils/api/lyrics-api';
|
||||
import escapeHtml from 'escape-html';
|
||||
|
||||
import { AutoScroll } from 'apps/stable/features/lyrics/constants/autoScroll';
|
||||
import { AutoScroll } from 'apps/legacy/features/lyrics/constants/autoScroll';
|
||||
import autoFocuser from 'components/autoFocuser';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
@@ -13,9 +13,8 @@ import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import keyboardNavigation from 'scripts/keyboardNavigation';
|
||||
import LibraryMenu from 'scripts/libraryMenu';
|
||||
import Events from 'utils/events';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
|
||||
import '../styles/lyrics.scss';
|
||||
import 'styles/lyrics.scss';
|
||||
|
||||
let currentPlayer;
|
||||
let currentItem;
|
||||
@@ -132,10 +131,13 @@ export default function (view) {
|
||||
}
|
||||
|
||||
function getLyrics(serverId, itemId) {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
const lyricsApi = getLyricsApi(toApi(apiClient));
|
||||
const api = ServerConnections.getApi(serverId);
|
||||
if (!api) {
|
||||
console.error('[Lyrics] no Api instance available for server', serverId);
|
||||
return;
|
||||
}
|
||||
|
||||
return lyricsApi.getLyrics({ itemId })
|
||||
return getLyricsApi(api).getLyrics({ itemId })
|
||||
.then(({ data }) => {
|
||||
if (!data.Lyrics?.length) {
|
||||
throw new Error('No lyrics returned');
|
||||
+11
-11
@@ -1,12 +1,12 @@
|
||||
import loading from '../../components/loading/loading';
|
||||
import libraryBrowser from '../../scripts/libraryBrowser';
|
||||
import { AlphaPicker } from '../../components/alphaPicker/alphaPicker';
|
||||
import imageLoader from '../../components/images/imageLoader';
|
||||
import listView from '../../components/listview/listview';
|
||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import globalize from '../../lib/globalize';
|
||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import loading from 'components/loading/loading';
|
||||
import libraryBrowser from 'scripts/libraryBrowser';
|
||||
import { AlphaPicker } from 'components/alphaPicker/alphaPicker';
|
||||
import imageLoader from 'components/images/imageLoader';
|
||||
import listView from 'components/listview/listview';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import * as userSettings from 'scripts/settings/userSettings';
|
||||
import globalize from 'lib/globalize';
|
||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
||||
export default function (view, params, tabContent) {
|
||||
function getPageData() {
|
||||
@@ -189,7 +189,7 @@ export default function (view, params, tabContent) {
|
||||
loading.hide();
|
||||
isLoading = false;
|
||||
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
});
|
||||
});
|
||||
@@ -242,7 +242,7 @@ export default function (view, params, tabContent) {
|
||||
reloadItems(tabElement);
|
||||
});
|
||||
tabElement.querySelector('.btnNewCollection').addEventListener('click', () => {
|
||||
import('../../components/collectionEditor/collectionEditor').then(({ default: CollectionEditor }) => {
|
||||
import('components/collectionEditor/collectionEditor').then(({ default: CollectionEditor }) => {
|
||||
const serverId = ApiClient.serverInfo().Id;
|
||||
const collectionEditor = new CollectionEditor();
|
||||
collectionEditor.show({
|
||||
@@ -1,15 +1,15 @@
|
||||
import loading from '../../components/loading/loading';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import libraryBrowser from '../../scripts/libraryBrowser';
|
||||
import { AlphaPicker } from '../../components/alphaPicker/alphaPicker';
|
||||
import listView from '../../components/listview/listview';
|
||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
||||
import globalize from '../../lib/globalize';
|
||||
import Events from '../../utils/events.ts';
|
||||
import { playbackManager } from '../../components/playback/playbackmanager';
|
||||
import loading from 'components/loading/loading';
|
||||
import * as userSettings from 'scripts/settings/userSettings';
|
||||
import libraryBrowser from 'scripts/libraryBrowser';
|
||||
import { AlphaPicker } from 'components/alphaPicker/alphaPicker';
|
||||
import listView from 'components/listview/listview';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import globalize from 'lib/globalize';
|
||||
import Events from 'utils/events';
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import { getFilterStatus, setFilterStatus } from 'components/filterdialog/filterIndicator';
|
||||
|
||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
||||
export default function (view, params, tabContent, options) {
|
||||
const onViewStyleChange = () => {
|
||||
@@ -108,7 +108,7 @@ export default function (view, params, tabContent, options) {
|
||||
isLoading = false;
|
||||
loading.hide();
|
||||
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(tabContent);
|
||||
});
|
||||
};
|
||||
@@ -307,7 +307,7 @@ export default function (view, params, tabContent, options) {
|
||||
query = userSettings.loadQuerySettings(savedQueryKey, query);
|
||||
|
||||
this.showFilterMenu = function () {
|
||||
import('../../components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
|
||||
import('components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
|
||||
const filterDialog = new FilterDialog({
|
||||
query: query,
|
||||
mode: 'movies',
|
||||
+4
-4
@@ -149,7 +149,7 @@ function getRecommendationHtml(recommendation) {
|
||||
return html;
|
||||
}
|
||||
|
||||
function loadSuggestions(page, userId) {
|
||||
function loadSuggestions(page, userId, parentId) {
|
||||
const screenWidth = dom.getWindowSize().innerWidth;
|
||||
let itemLimit = 5;
|
||||
if (screenWidth >= 1600) {
|
||||
@@ -160,6 +160,7 @@ function loadSuggestions(page, userId) {
|
||||
|
||||
const url = ApiClient.getUrl('Movies/Recommendations', {
|
||||
userId: userId,
|
||||
ParentId: parentId,
|
||||
categoryLimit: 6,
|
||||
ItemLimit: itemLimit,
|
||||
Fields: 'PrimaryImageAspectRatio,MediaSourceCount',
|
||||
@@ -185,7 +186,7 @@ function loadSuggestions(page, userId) {
|
||||
}
|
||||
|
||||
function autoFocus(page) {
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
});
|
||||
}
|
||||
@@ -223,7 +224,7 @@ function loadSuggestionsTab(view, params, tabContent) {
|
||||
const userId = ApiClient.getCurrentUserId();
|
||||
loadResume(tabContent, userId, parentId);
|
||||
loadLatest(tabContent, userId, parentId);
|
||||
loadSuggestions(tabContent, userId);
|
||||
loadSuggestions(tabContent, userId, parentId);
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
@@ -403,4 +404,3 @@ export default function (view, params) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-14
@@ -1,16 +1,16 @@
|
||||
import { playbackManager } from '../../components/playback/playbackmanager';
|
||||
import loading from '../../components/loading/loading';
|
||||
import libraryBrowser from '../../scripts/libraryBrowser';
|
||||
import imageLoader from '../../components/images/imageLoader';
|
||||
import AlphaPicker from '../../components/alphaPicker/alphaPicker';
|
||||
import listView from '../../components/listview/listview';
|
||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import globalize from '../../lib/globalize';
|
||||
import Events from '../../utils/events.ts';
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import loading from 'components/loading/loading';
|
||||
import libraryBrowser from 'scripts/libraryBrowser';
|
||||
import imageLoader from 'components/images/imageLoader';
|
||||
import AlphaPicker from 'components/alphaPicker/alphaPicker';
|
||||
import listView from 'components/listview/listview';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import * as userSettings from 'scripts/settings/userSettings';
|
||||
import globalize from 'lib/globalize';
|
||||
import Events from 'utils/events';
|
||||
import { setFilterStatus } from 'components/filterdialog/filterIndicator';
|
||||
|
||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
||||
export default function (view, params, tabContent) {
|
||||
function playAll() {
|
||||
@@ -177,7 +177,7 @@ export default function (view, params, tabContent) {
|
||||
loading.hide();
|
||||
isLoading = false;
|
||||
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(tabContent);
|
||||
});
|
||||
});
|
||||
@@ -187,7 +187,7 @@ export default function (view, params, tabContent) {
|
||||
let isLoading = false;
|
||||
|
||||
this.showFilterMenu = function () {
|
||||
import('../../components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
|
||||
import('components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
|
||||
const filterDialog = new FilterDialog({
|
||||
query: getQuery(),
|
||||
mode: 'albums',
|
||||
@@ -296,4 +296,3 @@ export default function (view, params, tabContent) {
|
||||
this.alphaPicker?.updateControls(getQuery());
|
||||
};
|
||||
}
|
||||
|
||||
+11
-12
@@ -1,14 +1,14 @@
|
||||
import loading from '../../components/loading/loading';
|
||||
import libraryBrowser from '../../scripts/libraryBrowser';
|
||||
import imageLoader from '../../components/images/imageLoader';
|
||||
import { AlphaPicker } from '../../components/alphaPicker/alphaPicker';
|
||||
import listView from '../../components/listview/listview';
|
||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import Events from '../../utils/events.ts';
|
||||
import loading from 'components/loading/loading';
|
||||
import libraryBrowser from 'scripts/libraryBrowser';
|
||||
import imageLoader from 'components/images/imageLoader';
|
||||
import { AlphaPicker } from 'components/alphaPicker/alphaPicker';
|
||||
import listView from 'components/listview/listview';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import * as userSettings from 'scripts/settings/userSettings';
|
||||
import Events from 'utils/events';
|
||||
import { setFilterStatus } from 'components/filterdialog/filterIndicator';
|
||||
|
||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
||||
export default function (view, params, tabContent, options) {
|
||||
function getPageData() {
|
||||
@@ -159,7 +159,7 @@ export default function (view, params, tabContent, options) {
|
||||
loading.hide();
|
||||
isLoading = false;
|
||||
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(tabContent);
|
||||
});
|
||||
});
|
||||
@@ -169,7 +169,7 @@ export default function (view, params, tabContent, options) {
|
||||
let isLoading = false;
|
||||
|
||||
this.showFilterMenu = function () {
|
||||
import('../../components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
|
||||
import('components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
|
||||
const filterDialog = new FilterDialog({
|
||||
query: getQuery(),
|
||||
mode: options.mode,
|
||||
@@ -238,4 +238,3 @@ export default function (view, params, tabContent, options) {
|
||||
this.alphaPicker?.updateControls(getQuery());
|
||||
};
|
||||
}
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
||||
import imageLoader from '../../components/images/imageLoader';
|
||||
import loading from '../../components/loading/loading';
|
||||
import * as userSettings from 'scripts/settings/userSettings';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import imageLoader from 'components/images/imageLoader';
|
||||
import loading from 'components/loading/loading';
|
||||
|
||||
export default function (view, params, tabContent) {
|
||||
function getPageData() {
|
||||
@@ -90,7 +90,7 @@ export default function (view, params, tabContent) {
|
||||
userSettings.saveQuerySettings(getSavedQueryKey(), query);
|
||||
loading.hide();
|
||||
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(context);
|
||||
});
|
||||
});
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
||||
import imageLoader from '../../components/images/imageLoader';
|
||||
import loading from '../../components/loading/loading';
|
||||
import * as userSettings from 'scripts/settings/userSettings';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import imageLoader from 'components/images/imageLoader';
|
||||
import loading from 'components/loading/loading';
|
||||
|
||||
export default function (view, params, tabContent) {
|
||||
function getPageData() {
|
||||
@@ -60,7 +60,7 @@ export default function (view, params, tabContent) {
|
||||
userSettings.saveQuerySettings(getSavedQueryKey(), query);
|
||||
loading.hide();
|
||||
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(context);
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -73,7 +73,7 @@ function loadLatest(page, parentId) {
|
||||
imageLoader.lazyChildren(elem);
|
||||
loading.hide();
|
||||
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
});
|
||||
});
|
||||
@@ -169,7 +169,7 @@ function loadSuggestionsTab(page, tabContent, parentId) {
|
||||
loadRecentlyPlayed(tabContent, parentId);
|
||||
loadFrequentlyPlayed(tabContent, parentId);
|
||||
|
||||
import('../../components/favoriteitems').then(({ default: favoriteItems }) => {
|
||||
import('components/favoriteitems').then(({ default: favoriteItems }) => {
|
||||
favoriteItems.render(tabContent, ApiClient.getCurrentUserId(), parentId, ['favoriteArtists', 'favoriteAlbums', 'favoriteSongs']);
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user