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 🏷️
|
name: Merge conflict labeling 🏷️
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
|
- uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0
|
||||||
with:
|
with:
|
||||||
dirtyLabel: 'merge conflict'
|
dirtyLabel: 'merge conflict'
|
||||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
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:
|
steps:
|
||||||
- name: Checkout repository ⬇️
|
- name: Checkout repository ⬇️
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.commit }}
|
ref: ${{ inputs.commit }}
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
|
||||||
- name: Initialize CodeQL 🛠️
|
- name: Initialize CodeQL 🛠️
|
||||||
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||||
with:
|
with:
|
||||||
queries: security-and-quality
|
queries: security-and-quality
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
- name: Autobuild 📦
|
- name: Autobuild 📦
|
||||||
uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis 🧪
|
- name: Perform CodeQL Analysis 🧪
|
||||||
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
name: CloudFlare Pages 📃
|
name: CloudFlare Pages 📃
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment:
|
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 }}
|
url: ${{ steps.cf.outputs.deployment-url }}
|
||||||
outputs:
|
outputs:
|
||||||
url: ${{ steps.cf.outputs.deployment-url }}
|
url: ${{ steps.cf.outputs.deployment-url }}
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
|
|
||||||
- name: Publish to Cloudflare Pages 📃
|
- name: Publish to Cloudflare Pages 📃
|
||||||
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
|
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
|
||||||
id: cf
|
id: cf
|
||||||
with:
|
with:
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.commit || github.sha }}
|
ref: ${{ inputs.commit || github.sha }}
|
||||||
|
|
||||||
- name: Setup node environment
|
- name: Setup node environment
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: .nvmrc
|
node-version-file: .nvmrc
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
mv dist/config.tmp.json dist/config.json
|
mv dist/config.tmp.json dist/config.json
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: frontend
|
name: frontend
|
||||||
path: dist
|
path: dist
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.commit }}
|
ref: ${{ inputs.commit }}
|
||||||
show-progress: false
|
show-progress: false
|
||||||
@@ -42,13 +42,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout ⬇️
|
- name: Checkout ⬇️
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.commit }}
|
ref: ${{ inputs.commit }}
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
|
||||||
- name: Setup node environment ⚙️
|
- name: Setup node environment ⚙️
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: .nvmrc
|
node-version-file: .nvmrc
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|||||||
@@ -46,12 +46,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Setup node environment
|
- name: Setup node environment
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: .nvmrc
|
node-version-file: .nvmrc
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -61,6 +61,6 @@ jobs:
|
|||||||
run: npm ci --no-audit
|
run: npm ci --no-audit
|
||||||
|
|
||||||
- name: Run eslint
|
- name: Run eslint
|
||||||
uses: CatChen/eslint-suggestion-action@efbe3942e76df120000621539d93ce7e0b54f7a7 # v4.1.27
|
uses: CatChen/eslint-suggestion-action@2b07b898c0dd5a016586d22b54282199b79cd358 # v4.1.33
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
with:
|
with:
|
||||||
branch: ${{ format('{0}/{1}', github.repository, github.ref_name) }}
|
branch: ${{ github.ref_name }}
|
||||||
commit: ${{ github.sha }}
|
commit: ${{ github.sha }}
|
||||||
workflow_run_id: ${{ github.run_id }}
|
workflow_run_id: ${{ github.run_id }}
|
||||||
comment: false
|
comment: false
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
operations-per-run: 75
|
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.
|
* New files **MUST** be written in TypeScript.
|
||||||
* API interactions **MUST** be made using the Jellyfin TypeScript SDK.
|
* 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** 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** 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.
|
* **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.
|
* **SHOULD NOT** rebase once reviews are in progress.
|
||||||
* **MUST** follow the [LLM development policy](https://jellyfin.org/docs/general/contributing/llm-policies).
|
* **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** 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** 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).
|
* **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](https://react.dev/reference/react) — User Interface library
|
||||||
* [React Router](https://reactrouter.com/) — Routing 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
|
* [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
|
* [Webpack](https://webpack.js.org/concepts/) — Bundler / build tooling
|
||||||
* [Vitest](https://vitest.dev/api/) — Test library
|
* [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 |
|
| 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 |
|
| [App Router](./src/components/router/appRouter.js) | React Router |
|
||||||
| [View Manager](./src/components/viewManager) | React Router |
|
| [View Manager](./src/components/viewManager) | React Router |
|
||||||
| [Jellyfin ApiClient](https://github.com/jellyfin-archive/jellyfin-apiclient-javascript) | Jellyfin TypeScript SDK |
|
| [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
|
## 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
|
* Main user application
|
||||||
* Supports TV layout
|
* 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`
|
* Dashboard App `src/apps/dashboard`
|
||||||
* Admin dashboard and metadata editor pages
|
* Admin dashboard and metadata editor pages
|
||||||
* Almost completely rewritten using MUI components
|
* Almost completely rewritten using MUI components
|
||||||
@@ -103,13 +105,12 @@ The official list of supported browser versions can be found in the `browserlist
|
|||||||
└── src
|
└── src
|
||||||
├── apps
|
├── apps
|
||||||
│ ├── dashboard # Admin dashboard app
|
│ ├── dashboard # Admin dashboard app
|
||||||
│ ├── experimental # New experimental app
|
│ ├── legacy # Legacy app
|
||||||
│ ├── stable # Classic (stable) app
|
│ ├── modern # New modern (React based) app
|
||||||
│ └── wizard # Startup wizard app
|
│ └── wizard # Startup wizard app
|
||||||
├── assets # Static assets
|
├── assets # Static assets
|
||||||
├── components # Higher order visual components and React components
|
├── components # Higher order visual components and React components
|
||||||
├── constants # Common constant values
|
├── constants # Common constant values
|
||||||
├── controllers # Legacy page views and controllers 🧹 ❌
|
|
||||||
├── elements # Basic webcomponents and React equivalents 🧹
|
├── elements # Basic webcomponents and React equivalents 🧹
|
||||||
├── hooks # Custom React hooks
|
├── hooks # Custom React hooks
|
||||||
├── lib # Reusable libraries
|
├── lib # Reusable libraries
|
||||||
|
|||||||
@@ -98,13 +98,17 @@
|
|||||||
- [Jxiced](https://github.com/Jxiced)
|
- [Jxiced](https://github.com/Jxiced)
|
||||||
- [Derek Huber](https://github.com/Derek4aty1)
|
- [Derek Huber](https://github.com/Derek4aty1)
|
||||||
- [StableCrimson](https://github.com/StableCrimson)
|
- [StableCrimson](https://github.com/StableCrimson)
|
||||||
|
- [hazemKrimi](https://github.com/hazemKrimi)
|
||||||
- [diegoeche](https://github.com/diegoeche)
|
- [diegoeche](https://github.com/diegoeche)
|
||||||
- [Free O'Toole](https://github.com/freeotoole)
|
- [Free O'Toole](https://github.com/freeotoole)
|
||||||
- [TheBosZ](https://github.com/thebosz)
|
- [TheBosZ](https://github.com/thebosz)
|
||||||
- [qm3jp](https://github.com/qm3jp)
|
- [qm3jp](https://github.com/qm3jp)
|
||||||
- [johnnyg](https://github.com/johnnyg)
|
- [johnnyg](https://github.com/johnnyg)
|
||||||
|
- [Lampan-git](https://github.com/Lampan-git)
|
||||||
- [lmaotrigine](https://github.com/lmaotrigine)
|
- [lmaotrigine](https://github.com/lmaotrigine)
|
||||||
- [bjorntp](https://github.com/bjorntp)
|
- [bjorntp](https://github.com/bjorntp)
|
||||||
|
- [austinhardaway](https://github.com/austinhardaway)
|
||||||
|
- [shindouj](https://github.com/shindouj)
|
||||||
|
|
||||||
## Emby Contributors
|
## Emby Contributors
|
||||||
|
|
||||||
|
|||||||
+9
-2
@@ -59,8 +59,7 @@ export default tseslint.config(
|
|||||||
'new-cap': [
|
'new-cap': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
'capIsNewExceptions': ['jQuery.Deferred'],
|
'newIsCapExceptionPattern': String.raw`\.default$`
|
||||||
'newIsCapExceptionPattern': '\\.default$'
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
'no-duplicate-imports': 'error',
|
'no-duplicate-imports': 'error',
|
||||||
@@ -71,6 +70,13 @@ export default tseslint.config(
|
|||||||
'no-redeclare': 'off',
|
'no-redeclare': 'off',
|
||||||
'@typescript-eslint/no-redeclare': ['error', { builtinGlobals: false }],
|
'@typescript-eslint/no-redeclare': ['error', { builtinGlobals: false }],
|
||||||
'no-restricted-globals': ['error'].concat(restrictedGlobals),
|
'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-assign': 'error',
|
||||||
'no-return-await': 'error',
|
'no-return-await': 'error',
|
||||||
'no-sequences': ['error', { 'allowInParentheses': false }],
|
'no-sequences': ['error', { 'allowInParentheses': false }],
|
||||||
@@ -103,6 +109,7 @@ export default tseslint.config(
|
|||||||
'sonarjs/no-inverted-boolean-check': 'error',
|
'sonarjs/no-inverted-boolean-check': 'error',
|
||||||
'sonarjs/no-selector-parameter': 'off',
|
'sonarjs/no-selector-parameter': 'off',
|
||||||
'sonarjs/pseudo-random': 'warn',
|
'sonarjs/pseudo-random': 'warn',
|
||||||
|
'sonarjs/aws-restricted-ip-admin-access': 'off',
|
||||||
// TODO: Enable the following sonarjs rules and fix issues
|
// TODO: Enable the following sonarjs rules and fix issues
|
||||||
'sonarjs/no-duplicate-string': 'off',
|
'sonarjs/no-duplicate-string': 'off',
|
||||||
'sonarjs/no-nested-functions': 'warn',
|
'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",
|
"name": "jellyfin-web",
|
||||||
"version": "10.12.0",
|
"version": "12.0.0",
|
||||||
"description": "Web interface for Jellyfin",
|
"description": "Web interface for Jellyfin",
|
||||||
"repository": "https://github.com/jellyfin/jellyfin-web",
|
"repository": "https://github.com/jellyfin/jellyfin-web",
|
||||||
"license": "GPL-2.0-or-later",
|
"license": "GPL-2.0-or-later",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.29.0",
|
"@babel/core": "7.29.7",
|
||||||
"@babel/plugin-transform-modules-umd": "7.27.1",
|
"@babel/plugin-transform-modules-umd": "7.29.7",
|
||||||
"@babel/preset-env": "7.29.2",
|
"@babel/preset-env": "7.29.7",
|
||||||
"@babel/preset-react": "7.28.5",
|
"@babel/preset-react": "7.29.7",
|
||||||
"@eslint-community/eslint-plugin-eslint-comments": "4.7.1",
|
"@eslint-community/eslint-plugin-eslint-comments": "4.7.2",
|
||||||
"@eslint/js": "9.39.4",
|
"@eslint/js": "9.39.4",
|
||||||
"@stylistic/eslint-plugin": "4.4.1",
|
"@stylistic/eslint-plugin": "4.4.1",
|
||||||
"@stylistic/stylelint-plugin": "3.1.3",
|
"@stylistic/stylelint-plugin": "3.1.3",
|
||||||
@@ -18,23 +18,23 @@
|
|||||||
"@types/loadable__component": "5.13.10",
|
"@types/loadable__component": "5.13.10",
|
||||||
"@types/lodash-es": "4.17.12",
|
"@types/lodash-es": "4.17.12",
|
||||||
"@types/markdown-it": "14.1.2",
|
"@types/markdown-it": "14.1.2",
|
||||||
"@types/react": "18.3.28",
|
"@types/react": "18.3.31",
|
||||||
"@types/react-dom": "18.3.7",
|
"@types/react-dom": "18.3.7",
|
||||||
"@types/react-lazy-load-image-component": "1.6.5",
|
"@types/react-lazy-load-image-component": "1.6.5",
|
||||||
"@types/sortablejs": "1.15.9",
|
"@types/sortablejs": "1.15.9",
|
||||||
"@typescript-eslint/parser": "8.57.0",
|
"@typescript-eslint/parser": "8.62.0",
|
||||||
"@uupaa/dynamic-import-polyfill": "1.0.2",
|
"@uupaa/dynamic-import-polyfill": "1.0.2",
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@vitest/coverage-v8": "3.2.6",
|
||||||
"autoprefixer": "10.4.27",
|
"autoprefixer": "10.5.2",
|
||||||
"babel-loader": "10.1.1",
|
"babel-loader": "10.1.1",
|
||||||
"caniuse-lite": "1.0.30001781",
|
"caniuse-lite": "1.0.30001799",
|
||||||
"clean-webpack-plugin": "4.0.0",
|
"clean-webpack-plugin": "4.0.0",
|
||||||
"confusing-browser-globals": "1.0.11",
|
"confusing-browser-globals": "1.0.11",
|
||||||
"copy-webpack-plugin": "14.0.0",
|
"copy-webpack-plugin": "14.0.0",
|
||||||
"cross-env": "10.1.0",
|
"cross-env": "10.1.0",
|
||||||
"css-loader": "7.1.4",
|
"css-loader": "7.1.4",
|
||||||
"cssnano": "7.1.3",
|
"cssnano": "7.1.9",
|
||||||
"es-check": "9.6.2",
|
"es-check": "9.6.4",
|
||||||
"eslint": "9.39.4",
|
"eslint": "9.39.4",
|
||||||
"eslint-plugin-compat": "6.2.1",
|
"eslint-plugin-compat": "6.2.1",
|
||||||
"eslint-plugin-import": "2.32.0",
|
"eslint-plugin-import": "2.32.0",
|
||||||
@@ -45,34 +45,34 @@
|
|||||||
"expose-loader": "5.0.1",
|
"expose-loader": "5.0.1",
|
||||||
"fast-glob": "3.3.3",
|
"fast-glob": "3.3.3",
|
||||||
"fork-ts-checker-webpack-plugin": "9.1.0",
|
"fork-ts-checker-webpack-plugin": "9.1.0",
|
||||||
"globals": "17.4.0",
|
"globals": "17.7.0",
|
||||||
"html-loader": "5.1.0",
|
"html-loader": "5.1.0",
|
||||||
"html-webpack-plugin": "5.6.6",
|
"html-webpack-plugin": "5.6.7",
|
||||||
"jsdom": "28.1.0",
|
"jsdom": "29.1.1",
|
||||||
"mini-css-extract-plugin": "2.10.1",
|
"mini-css-extract-plugin": "2.10.2",
|
||||||
"postcss": "8.5.8",
|
"postcss": "8.5.15",
|
||||||
"postcss-loader": "8.2.1",
|
"postcss-loader": "8.2.1",
|
||||||
"postcss-preset-env": "10.6.1",
|
"postcss-preset-env": "10.6.1",
|
||||||
"postcss-scss": "4.0.9",
|
"postcss-scss": "4.0.9",
|
||||||
"sass": "1.98.0",
|
"sass": "1.101.0",
|
||||||
"sass-loader": "16.0.7",
|
"sass-loader": "16.0.8",
|
||||||
"source-map-loader": "5.0.0",
|
"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",
|
"style-loader": "4.0.0",
|
||||||
"stylelint": "16.26.1",
|
"stylelint": "16.26.1",
|
||||||
"stylelint-config-rational-order": "0.1.2",
|
"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-order": "7.0.1",
|
||||||
"stylelint-scss": "6.14.0",
|
"stylelint-scss": "6.14.0",
|
||||||
"ts-loader": "9.5.4",
|
"ts-loader": "9.6.2",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.57.0",
|
"typescript-eslint": "8.62.0",
|
||||||
"vite-tsconfig-paths": "6.1.1",
|
"vite-tsconfig-paths": "6.1.1",
|
||||||
"vitest": "3.2.4",
|
"vitest": "3.2.6",
|
||||||
"webpack": "5.105.4",
|
"webpack": "5.107.2",
|
||||||
"webpack-bundle-analyzer": "5.2.0",
|
"webpack-bundle-analyzer": "5.3.0",
|
||||||
"webpack-cli": "7.0.2",
|
"webpack-cli": "7.0.3",
|
||||||
"webpack-dev-server": "5.2.3",
|
"webpack-dev-server": "5.2.5",
|
||||||
"webpack-merge": "6.0.1",
|
"webpack-merge": "6.0.1",
|
||||||
"worker-loader": "3.0.8"
|
"worker-loader": "3.0.8"
|
||||||
},
|
},
|
||||||
@@ -86,16 +86,17 @@
|
|||||||
"@fontsource/noto-sans-sc": "5.2.9",
|
"@fontsource/noto-sans-sc": "5.2.9",
|
||||||
"@fontsource/noto-sans-tc": "5.2.9",
|
"@fontsource/noto-sans-tc": "5.2.9",
|
||||||
"@jellyfin/libass-wasm": "4.2.4",
|
"@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",
|
"@jellyfin/ux-web": "1.0.0",
|
||||||
"@mui/icons-material": "6.5.0",
|
"@mui/icons-material": "6.5.0",
|
||||||
"@mui/material": "6.5.0",
|
"@mui/material": "6.5.0",
|
||||||
"@mui/x-date-pickers": "7.29.4",
|
"@mui/x-date-pickers": "7.29.4",
|
||||||
"@react-hook/resize-observer": "2.0.2",
|
"@react-hook/resize-observer": "2.0.2",
|
||||||
"@tanstack/react-query": "5.91.2",
|
"@tanstack/react-query": "5.101.1",
|
||||||
"@tanstack/react-query-devtools": "5.91.3",
|
"@tanstack/react-query-devtools": "5.101.1",
|
||||||
|
"@tanstack/react-query-persist-client": "5.101.1",
|
||||||
"abortcontroller-polyfill": "1.7.8",
|
"abortcontroller-polyfill": "1.7.8",
|
||||||
"axios": "1.13.6",
|
"axios": "1.18.1",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||||
"classnames": "2.5.1",
|
"classnames": "2.5.1",
|
||||||
@@ -109,7 +110,8 @@
|
|||||||
"flv.js": "1.6.2",
|
"flv.js": "1.6.2",
|
||||||
"headroom.js": "0.12.0",
|
"headroom.js": "0.12.0",
|
||||||
"history": "5.3.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",
|
"intersection-observer": "0.12.2",
|
||||||
"jellyfin-apiclient": "1.11.0",
|
"jellyfin-apiclient": "1.11.0",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
@@ -117,7 +119,7 @@
|
|||||||
"libarchive.js": "2.0.2",
|
"libarchive.js": "2.0.2",
|
||||||
"libbitsub": "1.10.1",
|
"libbitsub": "1.10.1",
|
||||||
"lodash-es": "4.18.1",
|
"lodash-es": "4.18.1",
|
||||||
"markdown-it": "14.1.1",
|
"markdown-it": "14.2.0",
|
||||||
"material-design-icons-iconfont": "6.7.0",
|
"material-design-icons-iconfont": "6.7.0",
|
||||||
"material-react-table": "3.2.1",
|
"material-react-table": "3.2.1",
|
||||||
"native-promise-only": "0.8.1",
|
"native-promise-only": "0.8.1",
|
||||||
@@ -127,17 +129,17 @@
|
|||||||
"react-blurhash": "0.3.0",
|
"react-blurhash": "0.3.0",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-lazy-load-image-component": "1.6.3",
|
"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",
|
"resize-observer-polyfill": "1.5.1",
|
||||||
"screenfull": "6.0.2",
|
"screenfull": "6.0.2",
|
||||||
"sortablejs": "1.15.7",
|
"sortablejs": "1.15.7",
|
||||||
"swiper": "12.1.2",
|
"swiper": "12.2.0",
|
||||||
"usehooks-ts": "3.1.1",
|
"usehooks-ts": "3.1.1",
|
||||||
"webcomponents.js": "0.7.24",
|
"webcomponents.js": "0.7.24",
|
||||||
"whatwg-fetch": "3.6.20"
|
"whatwg-fetch": "3.6.20"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"sass-embedded": "1.98.0"
|
"sass-embedded": "1.100.0"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 2 Firefox versions",
|
"last 2 Firefox versions",
|
||||||
|
|||||||
+12
-4
@@ -1,12 +1,13 @@
|
|||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
|
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import QueryClientEventHandler from 'components/QueryClientEventHandler';
|
||||||
import { ApiProvider } from 'hooks/useApi';
|
import { ApiProvider } from 'hooks/useApi';
|
||||||
import { UserSettingsProvider } from 'hooks/useUserSettings';
|
import { UserSettingsProvider } from 'hooks/useUserSettings';
|
||||||
import { WebConfigProvider } from 'hooks/useWebConfig';
|
import { WebConfigProvider } from 'hooks/useWebConfig';
|
||||||
import browser from 'scripts/browser';
|
import browser from 'scripts/browser';
|
||||||
import { queryClient } from 'utils/query/queryClient';
|
import { persister, queryClient } from 'utils/query/queryClient';
|
||||||
|
|
||||||
import RootAppRouter from 'RootAppRouter';
|
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
|
&& !browser.tv; // Don't use devtools on the TV as the navigation is weird
|
||||||
|
|
||||||
const RootApp = () => (
|
const RootApp = () => (
|
||||||
<QueryClientProvider client={queryClient}>
|
<PersistQueryClientProvider
|
||||||
|
client={queryClient}
|
||||||
|
persistOptions={{
|
||||||
|
buster: __JF_BUILD_VERSION__,
|
||||||
|
persister
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ApiProvider>
|
<ApiProvider>
|
||||||
<UserSettingsProvider>
|
<UserSettingsProvider>
|
||||||
<WebConfigProvider>
|
<WebConfigProvider>
|
||||||
|
<QueryClientEventHandler />
|
||||||
<RootAppRouter />
|
<RootAppRouter />
|
||||||
</WebConfigProvider>
|
</WebConfigProvider>
|
||||||
</UserSettingsProvider>
|
</UserSettingsProvider>
|
||||||
@@ -25,7 +33,7 @@ const RootApp = () => (
|
|||||||
{useReactQueryDevtools && (
|
{useReactQueryDevtools && (
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
)}
|
)}
|
||||||
</QueryClientProvider>
|
</PersistQueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default RootApp;
|
export default RootApp;
|
||||||
|
|||||||
+5
-10
@@ -8,27 +8,22 @@ import {
|
|||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import { DASHBOARD_APP_PATHS, DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
import { DASHBOARD_APP_PATHS, DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
||||||
import { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/routes/routes';
|
import { APP_ROUTES as MODERN_APP_ROUTES } from 'apps/modern/routes/routes';
|
||||||
import { STABLE_APP_ROUTES } from 'apps/stable/routes/routes';
|
import { APP_ROUTES as LEGACY_APP_ROUTES } from 'apps/legacy/routes/routes';
|
||||||
import { WIZARD_APP_ROUTES } from 'apps/wizard/routes/routes';
|
import { WIZARD_APP_ROUTES } from 'apps/wizard/routes/routes';
|
||||||
import AppHeader from 'components/AppHeader';
|
import AppHeader from 'components/AppHeader';
|
||||||
import Backdrop from 'components/Backdrop';
|
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 BangRedirect from 'components/router/BangRedirect';
|
||||||
import { createRouterHistory } from 'components/router/routerHistory';
|
import { createRouterHistory } from 'components/router/routerHistory';
|
||||||
import { LayoutMode } from 'constants/layoutMode';
|
|
||||||
import browser from 'scripts/browser';
|
|
||||||
import appTheme from 'themes';
|
import appTheme from 'themes';
|
||||||
import { ThemeStorageManager } from 'themes/themeStorageManager';
|
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([
|
const router = createHashRouter([
|
||||||
{
|
{
|
||||||
element: <RootAppLayout />,
|
element: <RootAppLayout />,
|
||||||
children: [
|
children: [
|
||||||
...(isExperimentalLayout ? EXPERIMENTAL_APP_ROUTES : STABLE_APP_ROUTES),
|
...(layoutManager.modern ? MODERN_APP_ROUTES : LEGACY_APP_ROUTES),
|
||||||
...DASHBOARD_APP_ROUTES,
|
...DASHBOARD_APP_ROUTES,
|
||||||
...WIZARD_APP_ROUTES,
|
...WIZARD_APP_ROUTES,
|
||||||
{
|
{
|
||||||
@@ -61,7 +56,7 @@ function RootAppLayout() {
|
|||||||
storageManager={ThemeStorageManager}
|
storageManager={ThemeStorageManager}
|
||||||
>
|
>
|
||||||
<Backdrop />
|
<Backdrop />
|
||||||
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
|
<AppHeader isHidden={layoutManager.modern || isNewLayoutPath} />
|
||||||
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
Vendored
-3
@@ -166,7 +166,6 @@ declare module 'jellyfin-apiclient' {
|
|||||||
getMusicGenres(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
getMusicGenres(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||||
getNamedConfiguration(name: string): Promise<any>;
|
getNamedConfiguration(name: string): Promise<any>;
|
||||||
getNetworkDevices(): Promise<any>;
|
getNetworkDevices(): Promise<any>;
|
||||||
getNetworkShares(path: string): Promise<FileSystemEntryInfo[]>;
|
|
||||||
getNewLiveTvTimerDefaults(options?: any): Promise<SeriesTimerInfoDto>;
|
getNewLiveTvTimerDefaults(options?: any): Promise<SeriesTimerInfoDto>;
|
||||||
getNextUpEpisodes(options?: any): Promise<BaseItemDtoQueryResult>;
|
getNextUpEpisodes(options?: any): Promise<BaseItemDtoQueryResult>;
|
||||||
getNotificationSummary(userId: string): Promise<NotificationsSummaryDto>;
|
getNotificationSummary(userId: string): Promise<NotificationsSummaryDto>;
|
||||||
@@ -256,7 +255,6 @@ declare module 'jellyfin-apiclient' {
|
|||||||
requestSyncPlaySetRepeatMode(options?: SetRepeatModeRequestDto): Promise<void>;
|
requestSyncPlaySetRepeatMode(options?: SetRepeatModeRequestDto): Promise<void>;
|
||||||
requestSyncPlaySetShuffleMode(options?: SetShuffleModeRequestDto): Promise<void>;
|
requestSyncPlaySetShuffleMode(options?: SetShuffleModeRequestDto): Promise<void>;
|
||||||
requestSyncPlayUnpause(): Promise<void>;
|
requestSyncPlayUnpause(): Promise<void>;
|
||||||
resetEasyPassword(userId: string): Promise<void>;
|
|
||||||
resetLiveTvTuner(id: string): Promise<void>;
|
resetLiveTvTuner(id: string): Promise<void>;
|
||||||
resetUserPassword(userId: string): Promise<void>;
|
resetUserPassword(userId: string): Promise<void>;
|
||||||
restartServer(): Promise<void>;
|
restartServer(): Promise<void>;
|
||||||
@@ -281,7 +279,6 @@ declare module 'jellyfin-apiclient' {
|
|||||||
uninstallPluginByVersion(id: string, version: string): Promise<void>;
|
uninstallPluginByVersion(id: string, version: string): Promise<void>;
|
||||||
uninstallPlugin(id: string): Promise<void>;
|
uninstallPlugin(id: string): Promise<void>;
|
||||||
updateDisplayPreferences(id: string, obj: DisplayPreferencesDto, userId: string, app: 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>;
|
updateFavoriteStatus(userId: string, itemId: string, isFavorite: boolean): Promise<UserItemDataDto>;
|
||||||
updateItemImageIndex(itemId: string, imageType: ImageType, imageIndex: number, newIndex: number): Promise<any>;
|
updateItemImageIndex(itemId: string, imageType: ImageType, imageIndex: number, newIndex: number): Promise<any>;
|
||||||
updateItem(item: BaseItemDto): Promise<void>;
|
updateItem(item: BaseItemDto): Promise<void>;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import AppBar from '@mui/material/AppBar';
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { type Theme } from '@mui/material/styles';
|
import { type Theme } from '@mui/material/styles';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
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 { Outlet, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import AppBody from 'components/AppBody';
|
import AppBody from 'components/AppBody';
|
||||||
|
import OffsetAppBar from 'components/OffsetAppBar';
|
||||||
import AppToolbar from 'components/toolbar/AppToolbar';
|
import AppToolbar from 'components/toolbar/AppToolbar';
|
||||||
import ServerButton from 'components/toolbar/ServerButton';
|
import ServerButton from 'components/toolbar/ServerButton';
|
||||||
import ElevationScroll from 'components/ElevationScroll';
|
|
||||||
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
||||||
import { appRouter } from 'components/router/appRouter';
|
import { appRouter } from 'components/router/appRouter';
|
||||||
import ThemeCss from 'components/ThemeCss';
|
import ThemeCss from 'components/ThemeCss';
|
||||||
@@ -50,39 +49,44 @@ export const Component: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={dateFnsLocale}>
|
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={dateFnsLocale}>
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
minHeight: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ElevationScroll elevate={false}>
|
<OffsetAppBar
|
||||||
<AppBar
|
dense
|
||||||
position='fixed'
|
elevation={4}
|
||||||
sx={{
|
sx={{
|
||||||
width: {
|
width: {
|
||||||
xs: '100%',
|
xs: '100%',
|
||||||
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
|
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
|
||||||
},
|
},
|
||||||
ml: {
|
ml: {
|
||||||
xs: 0,
|
xs: 0,
|
||||||
md: isDrawerAvailable ? DRAWER_WIDTH : 0
|
md: isDrawerAvailable ? DRAWER_WIDTH : 0
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<AppToolbar
|
||||||
|
isBackButtonAvailable={appRouter.canGoBack()}
|
||||||
|
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
|
||||||
|
isDrawerOpen={isDrawerOpen}
|
||||||
|
onDrawerButtonClick={onToggleDrawer}
|
||||||
|
buttons={
|
||||||
|
<HelpButton />
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<AppToolbar
|
{isMetadataManager && (
|
||||||
isBackButtonAvailable={appRouter.canGoBack()}
|
<ServerButton />
|
||||||
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
|
)}
|
||||||
isDrawerOpen={isDrawerOpen}
|
|
||||||
onDrawerButtonClick={onToggleDrawer}
|
|
||||||
buttons={
|
|
||||||
<HelpButton />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isMetadataManager && (
|
|
||||||
<ServerButton />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<AppTabs isDrawerOpen={isDrawerOpen} />
|
<AppTabs isDrawerOpen={isDrawerOpen} />
|
||||||
</AppToolbar>
|
</AppToolbar>
|
||||||
</AppBar>
|
</OffsetAppBar>
|
||||||
</ElevationScroll>
|
|
||||||
|
|
||||||
{
|
{
|
||||||
isDrawerAvailable && (
|
isDrawerAvailable && (
|
||||||
@@ -98,6 +102,7 @@ export const Component: FC = () => {
|
|||||||
<Box
|
<Box
|
||||||
component='main'
|
component='main'
|
||||||
sx={{
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
flexGrow: 1
|
flexGrow: 1
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -19,20 +19,8 @@ $drawer-width: 240px;
|
|||||||
position: unset !important;
|
position: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix the padding of dashboard pages
|
// Fix the padding of the metadata editor
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadataEditorPage {
|
.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 List from '@mui/material/List';
|
||||||
import React, { FC } from 'react';
|
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 ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';
|
||||||
|
|
||||||
import ServerDrawerSection from './sections/ServerDrawerSection';
|
import ServerDrawerSection from './sections/ServerDrawerSection';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Extension from '@mui/icons-material/Extension';
|
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 List from '@mui/material/List';
|
||||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
@@ -50,8 +50,7 @@ const PluginDrawerSection = () => {
|
|||||||
to={`/${Dashboard.getPluginUrl(pageInfo.Name)}`}
|
to={`/${Dashboard.getPluginUrl(pageInfo.Name)}`}
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
{/* TODO: Support different icons? */}
|
<Icon>{pageInfo.MenuIcon ?? 'folder'}</Icon>
|
||||||
<Folder />
|
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={pageInfo.DisplayName} />
|
<ListItemText primary={pageInfo.DisplayName} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ const ServerInfoWidget = ({
|
|||||||
sx={{
|
sx={{
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}}
|
}}
|
||||||
disabled={isScanning}
|
loading={isScanning}
|
||||||
|
loadingPosition='start'
|
||||||
>
|
>
|
||||||
{globalize.translate('ButtonScanAllLibraries')}
|
{globalize.translate('ButtonScanAllLibraries')}
|
||||||
</Button>
|
</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 { AxiosRequestConfig } from 'axios';
|
||||||
import type { Api } from '@jellyfin/sdk';
|
import type { Api } from '@jellyfin/sdk';
|
||||||
import { getActivityLogApi } from '@jellyfin/sdk/lib/utils/api/activity-log-api';
|
import { getActivityLogApi } from '@jellyfin/sdk/lib/utils/api/activity-log-api';
|
||||||
@@ -27,6 +27,7 @@ export const useLogEntries = (
|
|||||||
queryFn: ({ signal }) =>
|
queryFn: ({ signal }) =>
|
||||||
fetchLogEntries(api!, requestParams, { signal }),
|
fetchLogEntries(api!, requestParams, { signal }),
|
||||||
enabled: !!api,
|
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 { AxiosRequestConfig } from 'axios';
|
||||||
import type { Api } from '@jellyfin/sdk';
|
import type { Api } from '@jellyfin/sdk';
|
||||||
import { getDevicesApi } from '@jellyfin/sdk/lib/utils/api/devices-api';
|
import { getDevicesApi } from '@jellyfin/sdk/lib/utils/api/devices-api';
|
||||||
@@ -28,6 +28,7 @@ export const useDevices = (
|
|||||||
queryKey: [QUERY_KEY, requestParams],
|
queryKey: [QUERY_KEY, requestParams],
|
||||||
queryFn: ({ signal }) =>
|
queryFn: ({ signal }) =>
|
||||||
fetchDevices(api!, requestParams, { 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 { getApiKeyApi } from '@jellyfin/sdk/lib/utils/api/api-key-api';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { useApi } from 'hooks/useApi';
|
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 { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
import { queryClient } from 'utils/query/queryClient';
|
|
||||||
|
import { invalidateVirtualFolders } from './invalidateVirtualFolders';
|
||||||
|
|
||||||
export const useRemoveVirtualFolder = () => {
|
export const useRemoveVirtualFolder = () => {
|
||||||
const { api } = useApi();
|
const { api, user } = useApi();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (params: LibraryStructureApiRemoveVirtualFolderRequest) => (
|
mutationFn: (params: LibraryStructureApiRemoveVirtualFolderRequest) => (
|
||||||
getLibraryStructureApi(api!)
|
getLibraryStructureApi(api!)
|
||||||
.removeVirtualFolder(params)
|
.removeVirtualFolder(params)
|
||||||
),
|
),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void queryClient.invalidateQueries({
|
invalidateVirtualFolders(user);
|
||||||
queryKey: [ 'VirtualFolders' ]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,19 +3,18 @@ import { LibraryStructureApiRenameVirtualFolderRequest } from '@jellyfin/sdk/lib
|
|||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
import { queryClient } from 'utils/query/queryClient';
|
|
||||||
|
import { invalidateVirtualFolders } from './invalidateVirtualFolders';
|
||||||
|
|
||||||
export const useRenameVirtualFolder = () => {
|
export const useRenameVirtualFolder = () => {
|
||||||
const { api } = useApi();
|
const { api, user } = useApi();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (params: LibraryStructureApiRenameVirtualFolderRequest) => (
|
mutationFn: (params: LibraryStructureApiRenameVirtualFolderRequest) => (
|
||||||
getLibraryStructureApi(api!)
|
getLibraryStructureApi(api!)
|
||||||
.renameVirtualFolder(params)
|
.renameVirtualFolder(params)
|
||||||
),
|
),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void queryClient.invalidateQueries({
|
invalidateVirtualFolders(user);
|
||||||
queryKey: [ 'VirtualFolders' ]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { useApi } from 'hooks/useApi';
|
|||||||
import type { AxiosRequestConfig } from 'axios';
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
import { getLibraryStructureApi } from '@jellyfin/sdk/lib/utils/api/library-structure-api';
|
import { getLibraryStructureApi } from '@jellyfin/sdk/lib/utils/api/library-structure-api';
|
||||||
|
|
||||||
|
export const QUERY_KEY = 'VirtualFolders';
|
||||||
|
|
||||||
const fetchVirtualFolders = async (api: Api, options?: AxiosRequestConfig) => {
|
const fetchVirtualFolders = async (api: Api, options?: AxiosRequestConfig) => {
|
||||||
const response = await getLibraryStructureApi(api).getVirtualFolders(options);
|
const response = await getLibraryStructureApi(api).getVirtualFolders(options);
|
||||||
|
|
||||||
@@ -14,7 +16,7 @@ export const useVirtualFolders = () => {
|
|||||||
const { api } = useApi();
|
const { api } = useApi();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [ 'VirtualFolders' ],
|
queryKey: [ QUERY_KEY ],
|
||||||
queryFn: ({ signal }) => fetchVirtualFolders(api!, { signal }),
|
queryFn: ({ signal }) => fetchVirtualFolders(api!, { signal }),
|
||||||
enabled: !!api
|
enabled: !!api
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import globalize from 'lib/globalize';
|
|||||||
import Icon from '@mui/material/Icon';
|
import Icon from '@mui/material/Icon';
|
||||||
import { getLibraryIcon } from 'utils/image';
|
import { getLibraryIcon } from 'utils/image';
|
||||||
import MediaLibraryEditor from 'components/mediaLibraryEditor/mediaLibraryEditor';
|
import MediaLibraryEditor from 'components/mediaLibraryEditor/mediaLibraryEditor';
|
||||||
import { queryClient } from 'utils/query/queryClient';
|
|
||||||
import Menu from '@mui/material/Menu';
|
import Menu from '@mui/material/Menu';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
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 { useApi } from 'hooks/useApi';
|
||||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||||
import dom from 'utils/dom';
|
import dom from 'utils/dom';
|
||||||
|
import { invalidateVirtualFolders } from '../api/invalidateVirtualFolders';
|
||||||
|
|
||||||
type LibraryCardProps = {
|
type LibraryCardProps = {
|
||||||
virtualFolder: VirtualFolderInfo;
|
virtualFolder: VirtualFolderInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LibraryCard = ({ virtualFolder }: LibraryCardProps) => {
|
const LibraryCard = ({ virtualFolder }: LibraryCardProps) => {
|
||||||
const { api } = useApi();
|
const { api, user } = useApi();
|
||||||
const actionRef = useRef<HTMLButtonElement | null>(null);
|
const actionRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const [ anchorEl, setAnchorEl ] = useState<HTMLElement | null>(null);
|
const [ anchorEl, setAnchorEl ] = useState<HTMLElement | null>(null);
|
||||||
const [ isMenuOpen, setIsMenuOpen ] = useState(false);
|
const [ isMenuOpen, setIsMenuOpen ] = useState(false);
|
||||||
@@ -44,9 +44,10 @@ const LibraryCard = ({ virtualFolder }: LibraryCardProps) => {
|
|||||||
|
|
||||||
const imageUrl = useMemo(() => {
|
const imageUrl = useMemo(() => {
|
||||||
if (virtualFolder.PrimaryImageItemId && virtualFolder.ItemId && api) {
|
if (virtualFolder.PrimaryImageItemId && virtualFolder.ItemId && api) {
|
||||||
|
const dpr = window?.devicePixelRatio || 1;
|
||||||
return getImageApi(api)
|
return getImageApi(api)
|
||||||
.getItemImageUrlById(virtualFolder.ItemId, ImageType.Primary, {
|
.getItemImageUrlById(virtualFolder.ItemId, ImageType.Primary, {
|
||||||
maxWidth: Math.round(dom.getScreenWidth() * 0.40)
|
maxWidth: Math.round(dom.getScreenWidth() * dpr * 0.40)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [ api, virtualFolder ]);
|
}, [ api, virtualFolder ]);
|
||||||
@@ -105,17 +106,14 @@ const LibraryCard = ({ virtualFolder }: LibraryCardProps) => {
|
|||||||
setIsMenuOpen(false);
|
setIsMenuOpen(false);
|
||||||
|
|
||||||
const mediaLibraryEditor = new MediaLibraryEditor({
|
const mediaLibraryEditor = new MediaLibraryEditor({
|
||||||
library: virtualFolder
|
library: virtualFolder,
|
||||||
|
refresh: true
|
||||||
}) as Promise<boolean>;
|
}) as Promise<boolean>;
|
||||||
|
|
||||||
void mediaLibraryEditor.then((hasChanges: boolean) => {
|
void mediaLibraryEditor.then((hasChanges: boolean) => {
|
||||||
if (hasChanges) {
|
if (hasChanges) invalidateVirtualFolders(user, virtualFolder);
|
||||||
void queryClient.invalidateQueries({
|
|
||||||
queryKey: ['VirtualFolders']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}, [ virtualFolder ]);
|
}, [ user, virtualFolder ]);
|
||||||
|
|
||||||
const showImageEditor = useCallback(() => {
|
const showImageEditor = useCallback(() => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
@@ -125,13 +123,11 @@ const LibraryCard = ({ virtualFolder }: LibraryCardProps) => {
|
|||||||
itemId: virtualFolder.ItemId,
|
itemId: virtualFolder.ItemId,
|
||||||
serverId: ServerConnections.currentApiClient()?.serverId()
|
serverId: ServerConnections.currentApiClient()?.serverId()
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
void queryClient.invalidateQueries({
|
invalidateVirtualFolders(user);
|
||||||
queryKey: ['VirtualFolders']
|
|
||||||
});
|
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
/* pop up closed */
|
/* pop up closed */
|
||||||
});
|
});
|
||||||
}, [ virtualFolder ]);
|
}, [ user, virtualFolder ]);
|
||||||
|
|
||||||
const showDeleteLibraryDialog = useCallback(() => {
|
const showDeleteLibraryDialog = useCallback(() => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const useServerLog = (name: string, refetchInterval: false | number) => {
|
|||||||
queryKey: ['ServerLog', name],
|
queryKey: ['ServerLog', name],
|
||||||
queryFn: ({ signal }) => fetchServerLog(api!, name, { signal }),
|
queryFn: ({ signal }) => fetchServerLog(api!, name, { signal }),
|
||||||
enabled: !!api,
|
enabled: !!api,
|
||||||
refetchInterval
|
refetchInterval,
|
||||||
|
staleTime: 0 // ensure we load the latest log data
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const useServerLogs = () => {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [ 'ServerLogs' ],
|
queryKey: [ 'ServerLogs' ],
|
||||||
queryFn: ({ signal }) => fetchServerLogs(api!, { signal }),
|
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 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 { useApi } from 'hooks/useApi';
|
||||||
import { ApiClient } from 'jellyfin-apiclient';
|
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
import serverNotifications from 'scripts/serverNotifications';
|
|
||||||
import Events, { Event } from 'utils/events';
|
|
||||||
import { QUERY_KEY, useSessions } from '../api/useSessions';
|
import { QUERY_KEY, useSessions } from '../api/useSessions';
|
||||||
import { queryClient } from 'utils/query/queryClient';
|
import { queryClient } from 'utils/query/queryClient';
|
||||||
import filterSessions from '../utils/filterSessions';
|
import filterSessions from '../utils/filterSessions';
|
||||||
|
import { OutboundWebSocketMessageType } from '@jellyfin/sdk/lib/websocket';
|
||||||
|
|
||||||
const QUERY_PARAMS = {
|
const QUERY_PARAMS = {
|
||||||
activeWithinSeconds: 960
|
activeWithinSeconds: 960
|
||||||
};
|
};
|
||||||
|
|
||||||
const FALLBACK_POLL_INTERVAL_MS = 2000;
|
|
||||||
|
|
||||||
const useLiveSessions = () => {
|
const useLiveSessions = () => {
|
||||||
const { __legacyApiClient__ } = useApi();
|
const { api } = useApi();
|
||||||
|
|
||||||
const sessionsQuery = useSessions(QUERY_PARAMS);
|
const sessionsQuery = useSessions(QUERY_PARAMS);
|
||||||
|
|
||||||
@@ -41,27 +36,11 @@ const useLiveSessions = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onSessionsUpdate = (evt: Event, apiClient: ApiClient, info: SessionInfoDto[]) => {
|
// Return function for unsubscribing
|
||||||
queryClient.setQueryData([ QUERY_KEY, QUERY_PARAMS ], updateSessions(info));
|
return api?.subscribe([OutboundWebSocketMessageType.Sessions], ({ Data }) => {
|
||||||
};
|
queryClient.setQueryData([ QUERY_KEY, QUERY_PARAMS ], updateSessions(Data ?? []));
|
||||||
|
});
|
||||||
const fallbackInterval = setInterval(() => {
|
}, [api, updateSessions]);
|
||||||
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 sessionsQuery;
|
return sessionsQuery;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ const getNowPlayingImageUrl = (item: BaseItemDto) => {
|
|||||||
if (!item.ServerId) return null;
|
if (!item.ServerId) return null;
|
||||||
|
|
||||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
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
|
/* 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. */
|
elements that aren't created yet. */
|
||||||
|
|||||||
@@ -35,25 +35,28 @@ const getNowPlayingName = (session: SessionInfo): NowPlayingInfo => {
|
|||||||
bottomText = nowPlayingItem.ProductionYear.toString();
|
bottomText = nowPlayingItem.ProductionYear.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nowPlayingItem.ImageTags?.Logo) {
|
const apiClient = ServerConnections.getApiClient(session.ServerId);
|
||||||
imgUrl = ServerConnections.getApiClient(session.ServerId!).getScaledImageUrl(nowPlayingItem.Id!, {
|
if (apiClient) {
|
||||||
tag: nowPlayingItem.ImageTags.Logo,
|
if (nowPlayingItem.ImageTags?.Logo) {
|
||||||
maxHeight: 24,
|
imgUrl = apiClient.getScaledImageUrl(nowPlayingItem.Id!, {
|
||||||
maxWidth: 130,
|
tag: nowPlayingItem.ImageTags.Logo,
|
||||||
type: 'Logo'
|
maxHeight: 24,
|
||||||
});
|
maxWidth: 130,
|
||||||
} else if (nowPlayingItem.ParentLogoImageTag) {
|
type: 'Logo'
|
||||||
imgUrl = ServerConnections.getApiClient(session.ServerId!).getScaledImageUrl(nowPlayingItem.ParentLogoItemId!, {
|
});
|
||||||
tag: nowPlayingItem.ParentLogoImageTag,
|
} else if (nowPlayingItem.ParentLogoImageTag) {
|
||||||
maxHeight: 24,
|
imgUrl = apiClient.getScaledImageUrl(nowPlayingItem.ParentLogoItemId!, {
|
||||||
maxWidth: 130,
|
tag: nowPlayingItem.ParentLogoImageTag,
|
||||||
type: 'Logo'
|
maxHeight: 24,
|
||||||
});
|
maxWidth: 130,
|
||||||
|
type: 'Logo'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
topText: topText,
|
topText,
|
||||||
bottomText: bottomText,
|
bottomText,
|
||||||
image: imgUrl
|
image: imgUrl
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { FolderStorageDto } from '@jellyfin/sdk/lib/generated-client';
|
import type { FolderStorageDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
import LinearProgress from '@mui/material/LinearProgress';
|
import LinearProgress from '@mui/material/LinearProgress';
|
||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
@@ -79,15 +80,29 @@ const StorageListItem: FC<StorageListItemProps> = ({
|
|||||||
color={statusColor}
|
color={statusColor}
|
||||||
value={usedPercentage}
|
value={usedPercentage}
|
||||||
/>
|
/>
|
||||||
<Typography
|
<Box
|
||||||
variant='body2'
|
|
||||||
color='textSecondary'
|
|
||||||
sx={{
|
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={{
|
slots={{
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const Task: FunctionComponent<TaskProps> = ({ task }: TaskProps) => {
|
|||||||
<ListItemLink to={`/dashboard/tasks/${task.Id}`}>
|
<ListItemLink to={`/dashboard/tasks/${task.Id}`}>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar sx={{ bgcolor: 'primary.main' }}>
|
<Avatar sx={{ bgcolor: 'primary.main' }}>
|
||||||
<AccessTimeIcon sx={{ color: '#fff' }} />
|
<AccessTimeIcon sx={{ color: '#fff', fontSize: 24 }} />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
|
|||||||
@@ -3,41 +3,17 @@ import { useApi } from 'hooks/useApi';
|
|||||||
import { QUERY_KEY, useTasks } from '../api/useTasks';
|
import { QUERY_KEY, useTasks } from '../api/useTasks';
|
||||||
import type { ScheduledTasksApiGetTasksRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-tasks-api';
|
import type { ScheduledTasksApiGetTasksRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-tasks-api';
|
||||||
import { queryClient } from 'utils/query/queryClient';
|
import { queryClient } from 'utils/query/queryClient';
|
||||||
import { ApiClient } from 'jellyfin-apiclient';
|
import { OutboundWebSocketMessageType } from '@jellyfin/sdk/lib/websocket';
|
||||||
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;
|
|
||||||
|
|
||||||
const useLiveTasks = (params: ScheduledTasksApiGetTasksRequest) => {
|
const useLiveTasks = (params: ScheduledTasksApiGetTasksRequest) => {
|
||||||
const { __legacyApiClient__ } = useApi();
|
const { api } = useApi();
|
||||||
const tasksQuery = useTasks(params);
|
const tasksQuery = useTasks(params);
|
||||||
|
|
||||||
// TODO: Replace usage of the legacy apiclient when websocket support is added to the TS SDK.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onScheduledTasksUpdate = (_e: Event, _apiClient: ApiClient, info: TaskInfo[]) => {
|
return api?.subscribe([OutboundWebSocketMessageType.ScheduledTasksInfo], ({ Data }) => {
|
||||||
queryClient.setQueryData([ QUERY_KEY ], info);
|
queryClient.setQueryData([ QUERY_KEY ], Data ?? []);
|
||||||
};
|
});
|
||||||
|
}, [api]);
|
||||||
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 tasksQuery;
|
return tasksQuery;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Api } from '@jellyfin/sdk';
|
import { Api } from '@jellyfin/sdk';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useApi } from 'hooks/useApi';
|
|
||||||
import { getSessionApi } from '@jellyfin/sdk/lib/utils/api/session-api';
|
|
||||||
import type { AxiosRequestConfig } from 'axios';
|
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 fetchAuthProviders = async (api: Api, options?: AxiosRequestConfig) => {
|
||||||
const response = await getSessionApi(api).getAuthProviders(options);
|
const response = await getAuthenticationApi(api).getAuthProviders(options);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Api } from '@jellyfin/sdk';
|
import { Api } from '@jellyfin/sdk';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useApi } from 'hooks/useApi';
|
|
||||||
import { getSessionApi } from '@jellyfin/sdk/lib/utils/api/session-api';
|
|
||||||
import type { AxiosRequestConfig } from 'axios';
|
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 fetchPasswordResetProviders = async (api: Api, options?: AxiosRequestConfig) => {
|
||||||
const response = await getSessionApi(api).getPasswordResetProviders(options);
|
const response = await getAuthenticationApi(api).getPasswordResetProviders(options);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
|
|||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
import { queryClient } from 'utils/query/queryClient';
|
import { queryClient } from 'utils/query/queryClient';
|
||||||
import { QUERY_KEY } from './useUser';
|
import { QUERY_KEY } from 'hooks/api/useUser';
|
||||||
|
|
||||||
export const useUpdateUser = () => {
|
export const useUpdateUser = () => {
|
||||||
const { api } = useApi();
|
const { api } = useApi();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
|
|||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
import { queryClient } from 'utils/query/queryClient';
|
import { queryClient } from 'utils/query/queryClient';
|
||||||
import { QUERY_KEY } from './useUser';
|
import { QUERY_KEY } from 'hooks/api/useUser';
|
||||||
|
|
||||||
export const useUpdateUserPolicy = () => {
|
export const useUpdateUserPolicy = () => {
|
||||||
const { api } = useApi();
|
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}
|
key={Item.Id}
|
||||||
className='chkDevice'
|
className='chkDevice'
|
||||||
itemId={Item.Id}
|
itemId={Item.Id}
|
||||||
itemName={Item.CustomName || Item.Name}
|
itemName={
|
||||||
itemAppName={Item.AppName}
|
[
|
||||||
|
Item.CustomName || Item.Name,
|
||||||
|
Item.AppName
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' - ')
|
||||||
|
}
|
||||||
itemCheckedAttribute={Item.checkedAttribute}
|
itemCheckedAttribute={Item.checkedAttribute}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import parseISO from 'date-fns/parseISO';
|
import parseISO from 'date-fns/parseISO';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
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 { 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 { useTheme } from '@mui/material/styles';
|
||||||
import ToggleButton from '@mui/material/ToggleButton';
|
import ToggleButton from '@mui/material/ToggleButton';
|
||||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||||
import { type MRT_ColumnDef, type MRT_Theme, useMaterialReactTable } from 'material-react-table';
|
import { type MRT_ColumnDef, type MRT_Theme, type MRT_ColumnFiltersState, type MRT_SortingState, useMaterialReactTable } from 'material-react-table';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import DateTimeCell from 'apps/dashboard/components/table/DateTimeCell';
|
import DateTimeCell from 'apps/dashboard/components/table/DateTimeCell';
|
||||||
import TablePage, { DEFAULT_TABLE_OPTIONS } from 'apps/dashboard/components/table/TablePage';
|
import TablePage, { DEFAULT_TABLE_OPTIONS } from 'apps/dashboard/components/table/TablePage';
|
||||||
import { useLogEntries } from 'apps/dashboard/features/activity/api/useLogEntries';
|
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 { ActivityLogEntryCell } from 'apps/dashboard/features/activity/types/ActivityLogEntryCell';
|
||||||
import { type UsersRecords, useUsersDetails } from 'hooks/useUsers';
|
import { type UsersRecords, useUsersDetails } from 'hooks/useUsers';
|
||||||
import globalize from 'lib/globalize';
|
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 { toBoolean } from 'utils/string';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
const DEFAULT_PAGE_SIZE = 25;
|
const DEFAULT_PAGE_SIZE = 25;
|
||||||
const VIEW_PARAM = 'useractivity';
|
|
||||||
|
|
||||||
const enum ActivityView {
|
const enum ActivityView {
|
||||||
All = 'All',
|
All = 'All',
|
||||||
@@ -29,6 +29,8 @@ const enum ActivityView {
|
|||||||
System = 'System'
|
System = 'System'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VIEW_PARAM = 'useractivity';
|
||||||
|
|
||||||
const getActivityView = (param: string | null) => {
|
const getActivityView = (param: string | null) => {
|
||||||
if (param === null) return ActivityView.All;
|
if (param === null) return ActivityView.All;
|
||||||
if (toBoolean(param)) return ActivityView.User;
|
if (toBoolean(param)) return ActivityView.User;
|
||||||
@@ -43,10 +45,12 @@ const getUserCell = (users: UsersRecords) => function UserCell({ row }: Activity
|
|||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
|
const [ columnFilters, setColumnFilters ] = useState<MRT_ColumnFiltersState>([]);
|
||||||
const [ activityView, setActivityView ] = useState(
|
const [ activityView, setActivityView ] = useState(
|
||||||
getActivityView(searchParams.get(VIEW_PARAM)));
|
getActivityView(searchParams.get(VIEW_PARAM)));
|
||||||
|
|
||||||
|
const [ sorting, setSorting ] = useState<MRT_SortingState>([{ id: 'Date', desc: true }]);
|
||||||
|
|
||||||
const [ pagination, setPagination ] = useState({
|
const [ pagination, setPagination ] = useState({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: DEFAULT_PAGE_SIZE
|
pageSize: DEFAULT_PAGE_SIZE
|
||||||
@@ -63,11 +67,49 @@ export const Component = () => {
|
|||||||
|
|
||||||
const UserCell = getUserCell(users);
|
const UserCell = getUserCell(users);
|
||||||
|
|
||||||
const activityParams = useMemo(() => ({
|
const activityParams = useMemo(() => {
|
||||||
startIndex: pagination.pageIndex * pagination.pageSize,
|
const getFilter = (id: string) => columnFilters.find(f => f.id === id)?.value;
|
||||||
limit: pagination.pageSize,
|
const sortFields: ActivityLogSortBy[] = [];
|
||||||
hasUserId: activityView !== ActivityView.All ? activityView === ActivityView.User : undefined
|
const sortOrders: SortOrder[] = [];
|
||||||
}), [activityView, pagination.pageIndex, pagination.pageSize]);
|
|
||||||
|
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 {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -88,13 +130,13 @@ export const Component = () => {
|
|||||||
id: 'User',
|
id: 'User',
|
||||||
accessorFn: row => row.UserId && users[row.UserId]?.Name,
|
accessorFn: row => row.UserId && users[row.UserId]?.Name,
|
||||||
header: globalize.translate('LabelUser'),
|
header: globalize.translate('LabelUser'),
|
||||||
size: 75,
|
size: 100,
|
||||||
Cell: UserCell,
|
Cell: UserCell,
|
||||||
enableResizing: false,
|
enableResizing: false,
|
||||||
muiTableBodyCellProps: {
|
muiTableBodyCellProps: {
|
||||||
align: 'center'
|
align: 'center'
|
||||||
},
|
},
|
||||||
filterVariant: 'multi-select',
|
filterVariant: 'select',
|
||||||
filterSelectOptions: userNames
|
filterSelectOptions: userNames
|
||||||
}], [ activityView, userNames, users, UserCell ]);
|
}], [ activityView, userNames, users, UserCell ]);
|
||||||
|
|
||||||
@@ -105,7 +147,9 @@ export const Component = () => {
|
|||||||
header: globalize.translate('LabelTime'),
|
header: globalize.translate('LabelTime'),
|
||||||
size: 160,
|
size: 160,
|
||||||
Cell: DateTimeCell,
|
Cell: DateTimeCell,
|
||||||
filterVariant: 'datetime-range'
|
filterVariant: 'datetime-range',
|
||||||
|
grow: true,
|
||||||
|
maxSize: 320
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'Severity',
|
accessorKey: 'Severity',
|
||||||
@@ -116,26 +160,34 @@ export const Component = () => {
|
|||||||
muiTableBodyCellProps: {
|
muiTableBodyCellProps: {
|
||||||
align: 'center'
|
align: 'center'
|
||||||
},
|
},
|
||||||
filterVariant: 'multi-select',
|
filterVariant: 'select',
|
||||||
filterSelectOptions: Object.values(LogLevel).map(level => globalize.translate(`LogLevel.${level}`))
|
filterSelectOptions: Object.values(LogLevel).map(level => ({
|
||||||
|
text: globalize.translate(`LogLevel.${level}`),
|
||||||
|
value: level
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
...userColumn,
|
...userColumn,
|
||||||
{
|
{
|
||||||
accessorKey: 'Name',
|
accessorKey: 'Name',
|
||||||
header: globalize.translate('LabelName'),
|
header: globalize.translate('LabelName'),
|
||||||
size: 270
|
size: 270,
|
||||||
|
grow: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'Overview',
|
id: 'Overview',
|
||||||
accessorFn: row => row.ShortOverview || row.Overview,
|
accessorFn: row => row.ShortOverview || row.Overview,
|
||||||
header: globalize.translate('LabelOverview'),
|
header: globalize.translate('LabelOverview'),
|
||||||
size: 170,
|
size: 170,
|
||||||
Cell: OverviewCell
|
Cell: OverviewCell,
|
||||||
|
grow: true,
|
||||||
|
maxSize: 220
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'Type',
|
accessorKey: 'Type',
|
||||||
header: globalize.translate('LabelType'),
|
header: globalize.translate('LabelType'),
|
||||||
size: 150
|
size: 150,
|
||||||
|
grow: true,
|
||||||
|
maxSize: 220
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'Actions',
|
id: 'Actions',
|
||||||
@@ -183,13 +235,23 @@ export const Component = () => {
|
|||||||
|
|
||||||
// State
|
// State
|
||||||
initialState: {
|
initialState: {
|
||||||
density: 'compact'
|
density: 'compact',
|
||||||
|
showColumnFilters: false
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
isLoading,
|
isLoading,
|
||||||
pagination
|
columnFilters,
|
||||||
|
pagination,
|
||||||
|
sorting
|
||||||
},
|
},
|
||||||
|
|
||||||
|
manualFiltering: true,
|
||||||
|
manualSorting: true,
|
||||||
|
onColumnFiltersChange: setColumnFilters,
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
enableMultiSort: true,
|
||||||
|
enableGlobalFilter: false,
|
||||||
|
|
||||||
// Server pagination
|
// Server pagination
|
||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
onPaginationChange: setPagination,
|
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 RestoreProgressDialog from 'apps/dashboard/features/backups/components/RestoreProgressDialog';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
|
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 = () => {
|
export const Component = () => {
|
||||||
const { api } = useApi();
|
const { api } = useApi();
|
||||||
@@ -33,9 +36,16 @@ export const Component = () => {
|
|||||||
const [ isErrorOccurred, setIsErrorOccurred ] = useState(false);
|
const [ isErrorOccurred, setIsErrorOccurred ] = useState(false);
|
||||||
const [ isRestoreDialogOpen, setIsRestoreDialogOpen ] = useState(false);
|
const [ isRestoreDialogOpen, setIsRestoreDialogOpen ] = useState(false);
|
||||||
const [ backupToRestore, setBackupToRestore ] = useState<BackupManifestDto | null>(null);
|
const [ backupToRestore, setBackupToRestore ] = useState<BackupManifestDto | null>(null);
|
||||||
|
const [ isConfirmBackupOpen, setIsConfirmBackupOpen ] = useState(false);
|
||||||
|
const [ pendingBackupOptions, setPendingBackupOptions ] = useState<BackupOptionsDto | null>(null);
|
||||||
const createBackup = useCreateBackup();
|
const createBackup = useCreateBackup();
|
||||||
const restoreBackup = useRestoreBackup();
|
const restoreBackup = useRestoreBackup();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: tasks,
|
||||||
|
isPending: isTasksPending
|
||||||
|
} = useLiveTasks({ isHidden: false });
|
||||||
|
|
||||||
const onCreateClick = useCallback(() => {
|
const onCreateClick = useCallback(() => {
|
||||||
setIsCreateFormOpen(true);
|
setIsCreateFormOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -56,7 +66,11 @@ export const Component = () => {
|
|||||||
setIsRestoreSuccess(false);
|
setIsRestoreSuccess(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onBackupCreate = useCallback((backupOptions: BackupOptionsDto) => {
|
const onCancelBackupCreateDialog = useCallback(() => {
|
||||||
|
setIsConfirmBackupOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const startBackupProcess = useCallback((backupOptions: BackupOptionsDto) => {
|
||||||
setBackupInProgress(true);
|
setBackupInProgress(true);
|
||||||
setIsCreateFormOpen(false);
|
setIsCreateFormOpen(false);
|
||||||
createBackup.mutate(backupOptions, {
|
createBackup.mutate(backupOptions, {
|
||||||
@@ -69,6 +83,24 @@ export const Component = () => {
|
|||||||
});
|
});
|
||||||
}, [ createBackup ]);
|
}, [ 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) => {
|
const promptRestore = useCallback((backup: BackupManifestDto) => {
|
||||||
setIsRestoreDialogOpen(true);
|
setIsRestoreDialogOpen(true);
|
||||||
setBackupToRestore(backup);
|
setBackupToRestore(backup);
|
||||||
@@ -110,7 +142,7 @@ export const Component = () => {
|
|||||||
}
|
}
|
||||||
}, [api, restoreInProgress]);
|
}, [api, restoreInProgress]);
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending || isTasksPending) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +154,20 @@ export const Component = () => {
|
|||||||
>
|
>
|
||||||
<BackupProgressDialog open={backupInProgress} />
|
<BackupProgressDialog open={backupInProgress} />
|
||||||
<RestoreProgressDialog open={restoreInProgress} />
|
<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
|
<CreateBackupForm
|
||||||
open={isCreateFormOpen}
|
open={isCreateFormOpen}
|
||||||
onClose={onCreateFormClose}
|
onClose={onCreateFormClose}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const BrandingOption = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
const api = ServerConnections.getCurrentApi();
|
const api = ServerConnections.getApi();
|
||||||
if (!api) throw new Error('No Api instance available');
|
if (!api) throw new Error('No Api instance available');
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
@@ -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(
|
return queryClient.ensureQueryData(
|
||||||
getBrandingOptionsQuery(ServerConnections.getCurrentApi()));
|
getBrandingOptionsQuery(api));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import type { MetadataConfiguration } from '@jellyfin/sdk/lib/generated-client/m
|
|||||||
const CONFIG_KEY = 'metadata';
|
const CONFIG_KEY = 'metadata';
|
||||||
|
|
||||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
const api = ServerConnections.getCurrentApi();
|
const api = ServerConnections.getApi();
|
||||||
if (!api) throw new Error('No Api instance available');
|
if (!api) throw new Error('No Api instance available');
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ import LibraryCard from 'apps/dashboard/features/libraries/components/LibraryCar
|
|||||||
import Loading from 'components/loading/LoadingComponent';
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
import MediaLibraryCreator from 'components/mediaLibraryCreator/mediaLibraryCreator';
|
import MediaLibraryCreator from 'components/mediaLibraryCreator/mediaLibraryCreator';
|
||||||
import getCollectionTypeOptions from 'apps/dashboard/features/libraries/utils/collectionTypeOptions';
|
import getCollectionTypeOptions from 'apps/dashboard/features/libraries/utils/collectionTypeOptions';
|
||||||
import { queryClient } from 'utils/query/queryClient';
|
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
import Add from '@mui/icons-material/Add';
|
import Add from '@mui/icons-material/Add';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
|
import { useApi } from 'hooks/useApi';
|
||||||
|
import { invalidateVirtualFolders } from 'apps/dashboard/features/libraries/api/invalidateVirtualFolders';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
|
const { user } = useApi();
|
||||||
const { data: virtualFolders, isPending: isVirtualFoldersPending, isError: isVirtualFoldersError } = useVirtualFolders();
|
const { data: virtualFolders, isPending: isVirtualFoldersPending, isError: isVirtualFoldersError } = useVirtualFolders();
|
||||||
const startTask = useStartTask();
|
const startTask = useStartTask();
|
||||||
const { data: tasks, isPending: isLiveTasksPending } = useLiveTasks({ isHidden: false });
|
const { data: tasks, isPending: isLiveTasksPending } = useLiveTasks({ isHidden: false });
|
||||||
@@ -35,13 +37,9 @@ export const Component = () => {
|
|||||||
}) as Promise<boolean>;
|
}) as Promise<boolean>;
|
||||||
|
|
||||||
void mediaLibraryCreator.then((hasChanges: boolean) => {
|
void mediaLibraryCreator.then((hasChanges: boolean) => {
|
||||||
if (hasChanges) {
|
if (hasChanges) invalidateVirtualFolders(user);
|
||||||
void queryClient.invalidateQueries({
|
|
||||||
queryKey: ['VirtualFolders']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, [ user ]);
|
||||||
|
|
||||||
const onScanLibraries = useCallback(() => {
|
const onScanLibraries = useCallback(() => {
|
||||||
if (librariesTask?.Id) {
|
if (librariesTask?.Id) {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { ActionData } from 'types/actionData';
|
|||||||
import { queryClient } from 'utils/query/queryClient';
|
import { queryClient } from 'utils/query/queryClient';
|
||||||
|
|
||||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
const api = ServerConnections.getCurrentApi();
|
const api = ServerConnections.getApi();
|
||||||
if (!api) throw new Error('No Api instance available');
|
if (!api) throw new Error('No Api instance available');
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import type { XbmcMetadataOptions } from '@jellyfin/sdk/lib/generated-client/mod
|
|||||||
const CONFIG_KEY = 'xbmcmetadata';
|
const CONFIG_KEY = 'xbmcmetadata';
|
||||||
|
|
||||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
const api = ServerConnections.getCurrentApi();
|
const api = ServerConnections.getApi();
|
||||||
if (!api) throw new Error('No Api instance available');
|
if (!api) throw new Error('No Api instance available');
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { ActionData } from 'types/actionData';
|
|||||||
const CONFIG_KEY = 'livetv';
|
const CONFIG_KEY = 'livetv';
|
||||||
|
|
||||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
const api = ServerConnections.getCurrentApi();
|
const api = ServerConnections.getApi();
|
||||||
if (!api) throw new Error('No Api instance available');
|
if (!api) throw new Error('No Api instance available');
|
||||||
|
|
||||||
const data = await request.json() as LiveTvOptions;
|
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';
|
import LogItemList from 'apps/dashboard/features/logs/components/LogItemList';
|
||||||
|
|
||||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
const api = ServerConnections.getCurrentApi();
|
const api = ServerConnections.getApi();
|
||||||
if (!api) throw new Error('No Api instance available');
|
if (!api) throw new Error('No Api instance available');
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import Switch from '@mui/material/Switch';
|
|||||||
const CONFIG_KEY = 'network';
|
const CONFIG_KEY = 'network';
|
||||||
|
|
||||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
const api = ServerConnections.getCurrentApi();
|
const api = ServerConnections.getApi();
|
||||||
if (!api) throw new Error('No Api instance available');
|
if (!api) throw new Error('No Api instance available');
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-a
|
|||||||
import { queryClient } from 'utils/query/queryClient';
|
import { queryClient } from 'utils/query/queryClient';
|
||||||
|
|
||||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
const api = ServerConnections.getCurrentApi();
|
const api = ServerConnections.getApi();
|
||||||
if (!api) throw new Error('No Api instance available');
|
if (!api) throw new Error('No Api instance available');
|
||||||
|
|
||||||
const { data: config } = await getConfigurationApi(api).getConfiguration();
|
const { data: config } = await getConfigurationApi(api).getConfiguration();
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { ActionData } from 'types/actionData';
|
|||||||
import { queryClient } from 'utils/query/queryClient';
|
import { queryClient } from 'utils/query/queryClient';
|
||||||
|
|
||||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
const api = ServerConnections.getCurrentApi();
|
const api = ServerConnections.getApi();
|
||||||
if (!api) throw new Error('No Api instance available');
|
if (!api) throw new Error('No Api instance available');
|
||||||
|
|
||||||
const { data: config } = await getConfigurationApi(api).getConfiguration();
|
const { data: config } = await getConfigurationApi(api).getConfiguration();
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import SimpleAlert from 'components/SimpleAlert';
|
|||||||
const CONFIG_KEY = 'encoding';
|
const CONFIG_KEY = 'encoding';
|
||||||
|
|
||||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
const api = ServerConnections.getCurrentApi();
|
const api = ServerConnections.getApi();
|
||||||
if (!api) throw new Error('No Api instance available');
|
if (!api) throw new Error('No Api instance available');
|
||||||
|
|
||||||
const data = await request.json() as EncodingOptions;
|
const data = await request.json() as EncodingOptions;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { ActionData } from 'types/actionData';
|
|||||||
import { queryClient } from 'utils/query/queryClient';
|
import { queryClient } from 'utils/query/queryClient';
|
||||||
|
|
||||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
const api = ServerConnections.getCurrentApi();
|
const api = ServerConnections.getApi();
|
||||||
if (!api) throw new Error('No Api instance available');
|
if (!api) throw new Error('No Api instance available');
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import Typography from '@mui/material/Typography';
|
|||||||
import { useLocalizationOptions } from 'apps/dashboard/features/settings/api/useLocalizationOptions';
|
import { useLocalizationOptions } from 'apps/dashboard/features/settings/api/useLocalizationOptions';
|
||||||
import Loading from 'components/loading/LoadingComponent';
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
import Page from 'components/Page';
|
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 globalize from 'lib/globalize';
|
||||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
@@ -26,7 +27,7 @@ import { queryClient } from 'utils/query/queryClient';
|
|||||||
import { ActionData } from 'types/actionData';
|
import { ActionData } from 'types/actionData';
|
||||||
|
|
||||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
const api = ServerConnections.getCurrentApi();
|
const api = ServerConnections.getApi();
|
||||||
if (!api) throw new Error('No Api instance available');
|
if (!api) throw new Error('No Api instance available');
|
||||||
|
|
||||||
const { data: config } = await getConfigurationApi(api).getConfiguration();
|
const { data: config } = await getConfigurationApi(api).getConfiguration();
|
||||||
@@ -44,7 +45,10 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||||||
.updateConfiguration({ serverConfiguration: config });
|
.updateConfiguration({ serverConfiguration: config });
|
||||||
|
|
||||||
void queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
queryKey: [ QUERY_KEY ]
|
queryKey: [ CONFIGURATION_QUERY_KEY ]
|
||||||
|
});
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: [ SYSTEM_INFO_QUERY_KEY ]
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useCallback } from 'react';
|
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 Loading from 'components/loading/LoadingComponent';
|
||||||
import Page from 'components/Page';
|
import Page from 'components/Page';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
@@ -19,7 +19,7 @@ import Alert from '@mui/material/Alert';
|
|||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { userId, tab } = useParams();
|
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) => {
|
const handleTabChange = useCallback((event: React.SyntheticEvent, newValue: UserTab) => {
|
||||||
navigate(`/dashboard/users/${userId}/${newValue}`);
|
navigate(`/dashboard/users/${userId}/${newValue}`);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const UserProfiles = () => {
|
|||||||
const [ isSettingsSavedToastOpen, setIsSettingsSavedToastOpen ] = useState(false);
|
const [ isSettingsSavedToastOpen, setIsSettingsSavedToastOpen ] = useState(false);
|
||||||
const element = useRef<HTMLDivElement>(null);
|
const element = useRef<HTMLDivElement>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: users, isPending } = useUsers();
|
const { data: users, isPending } = useUsers(undefined, { forceRefresh: true });
|
||||||
const deleteUser = useDeleteUser();
|
const deleteUser = useDeleteUser();
|
||||||
|
|
||||||
const handleToastClose = useCallback(() => {
|
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();
|
loading.show();
|
||||||
|
|
||||||
if (itemId) {
|
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);
|
metadataEditor.embed(context.querySelector('.editPageInnerContent'), itemId, ApiClient.serverInfo().Id);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import { 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 { 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 cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||||
import { getBackdropShape, getPortraitShape, getSquareShape } from 'components/cardbuilder/utils/shape';
|
import { getBackdropShape, getPortraitShape, getSquareShape } from 'components/cardbuilder/utils/shape';
|
||||||
@@ -92,6 +95,15 @@ function getSections() {
|
|||||||
centerText: true,
|
centerText: true,
|
||||||
overlayPlayButton: true,
|
overlayPlayButton: true,
|
||||||
coverImage: true
|
coverImage: true
|
||||||
|
}, {
|
||||||
|
name: 'Studios',
|
||||||
|
types: BaseItemKind.Studio,
|
||||||
|
shape: getBackdropShape(enableScrollX()),
|
||||||
|
preferThumb: true,
|
||||||
|
showTitle: true,
|
||||||
|
overlayText: false,
|
||||||
|
centerText: true,
|
||||||
|
overlayPlayButton: true
|
||||||
}, {
|
}, {
|
||||||
name: 'People',
|
name: 'People',
|
||||||
types: 'Person',
|
types: 'Person',
|
||||||
@@ -189,6 +201,25 @@ function getFetchDataFn(section) {
|
|||||||
options.Limit = 20;
|
options.Limit = 20;
|
||||||
const userId = apiClient.getCurrentUserId();
|
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') {
|
if (section.types === 'MusicArtist') {
|
||||||
return apiClient.getArtists(userId, options);
|
return apiClient.getArtists(userId, options);
|
||||||
}
|
}
|
||||||
@@ -350,4 +381,3 @@ class FavoritesTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default FavoritesTab;
|
export default FavoritesTab;
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import TabbedView from '../components/tabbedview/tabbedview';
|
import TabbedView from 'components/tabbedview/tabbedview';
|
||||||
import globalize from '../lib/globalize';
|
import globalize from 'lib/globalize';
|
||||||
import '../elements/emby-tabs/emby-tabs';
|
import 'elements/emby-tabs/emby-tabs';
|
||||||
import '../elements/emby-button/emby-button';
|
import 'elements/emby-button/emby-button';
|
||||||
import '../elements/emby-scroller/emby-scroller';
|
import 'elements/emby-scroller/emby-scroller';
|
||||||
import LibraryMenu from '../scripts/libraryMenu';
|
import LibraryMenu from 'scripts/libraryMenu';
|
||||||
|
|
||||||
class HomeView extends TabbedView {
|
class HomeView extends TabbedView {
|
||||||
setTitle() {
|
setTitle() {
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as userSettings from '../scripts/settings/userSettings';
|
import * as userSettings from 'scripts/settings/userSettings';
|
||||||
import focusManager from '../components/focusManager';
|
import focusManager from 'components/focusManager';
|
||||||
import homeSections from '../components/homesections/homesections';
|
import homeSections from 'components/homesections/homesections';
|
||||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||||
|
|
||||||
import '../elements/emby-itemscontainer/emby-itemscontainer';
|
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
|
||||||
class HomeTab {
|
class HomeTab {
|
||||||
constructor(view, params) {
|
constructor(view, params) {
|
||||||
+7
@@ -232,6 +232,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div id="similarCollapsible" class="verticalSection detailVerticalSection verticalSection-extrabottompadding hide">
|
||||||
<h2 class="sectionTitle sectionTitle-cards padded-right">${HeaderMoreLikeThis}</h2>
|
<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">
|
<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 { 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 { PersonKind } from '@jellyfin/sdk/lib/generated-client/models/person-kind';
|
||||||
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
|
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
|
||||||
|
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
|
||||||
import { intervalToDuration } from 'date-fns';
|
import { intervalToDuration } from 'date-fns';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import escapeHtml from 'escape-html';
|
import escapeHtml from 'escape-html';
|
||||||
@@ -25,6 +27,7 @@ import { playbackManager } from 'components/playback/playbackmanager';
|
|||||||
import { appRouter } from 'components/router/appRouter';
|
import { appRouter } from 'components/router/appRouter';
|
||||||
import itemShortcuts from 'components/shortcuts';
|
import itemShortcuts from 'components/shortcuts';
|
||||||
import { AppFeature } from 'constants/appFeature';
|
import { AppFeature } from 'constants/appFeature';
|
||||||
|
import { EventType } from 'constants/eventType';
|
||||||
import { ItemAction } from 'constants/itemAction';
|
import { ItemAction } from 'constants/itemAction';
|
||||||
import globalize from 'lib/globalize';
|
import globalize from 'lib/globalize';
|
||||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||||
@@ -38,7 +41,7 @@ import * as userSettings from 'scripts/settings/userSettings';
|
|||||||
import Dashboard from 'utils/dashboard';
|
import Dashboard from 'utils/dashboard';
|
||||||
import Events from 'utils/events';
|
import Events from 'utils/events';
|
||||||
import { getItemBackdropImageUrl } from 'utils/jellyfin-apiclient/backdropImage';
|
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-itemscontainer/emby-itemscontainer';
|
||||||
import 'elements/emby-checkbox/emby-checkbox';
|
import 'elements/emby-checkbox/emby-checkbox';
|
||||||
@@ -59,7 +62,7 @@ const LIST_VIEW_TYPES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function autoFocus(container) {
|
function autoFocus(container) {
|
||||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||||
autoFocuser.autoFocus(container);
|
autoFocuser.autoFocus(container);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -176,7 +179,7 @@ function renderSeriesTimerEditor(page, item, apiClient, user) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (user.Policy.EnableLiveTvManagement) {
|
if (user.Policy.EnableLiveTvManagement) {
|
||||||
import('../../components/recordingcreator/seriesrecordingeditor').then(({ default: seriesRecordingEditor }) => {
|
import('components/recordingcreator/seriesrecordingeditor').then(({ default: seriesRecordingEditor }) => {
|
||||||
seriesRecordingEditor.embed(item, apiClient.serverId(), {
|
seriesRecordingEditor.embed(item, apiClient.serverId(), {
|
||||||
context: page.querySelector('.seriesRecordingEditor')
|
context: page.querySelector('.seriesRecordingEditor')
|
||||||
});
|
});
|
||||||
@@ -706,7 +709,7 @@ function showRecordingFields(instance, page, item, user) {
|
|||||||
const recordingFieldsElement = page.querySelector('.recordingFields');
|
const recordingFieldsElement = page.querySelector('.recordingFields');
|
||||||
|
|
||||||
if (item.Type == 'Program' && user.Policy.EnableLiveTvManagement) {
|
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({
|
instance.currentRecordingFields = new RecordingFields({
|
||||||
parent: recordingFieldsElement,
|
parent: recordingFieldsElement,
|
||||||
programId: item.Id,
|
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') {
|
} 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('#listChildrenCollapsible').classList.remove('hide');
|
||||||
page.querySelector('#childrenCollapsible').classList.add('hide');
|
page.querySelector('#childrenCollapsible').classList.add('hide');
|
||||||
renderItemsByName(page, item);
|
renderItemsByName(page, item, user);
|
||||||
} else if (item.IsFolder) {
|
} else if (item.IsFolder) {
|
||||||
if (item.Type == 'BoxSet') {
|
if (item.Type == 'BoxSet') {
|
||||||
page.querySelector('#listChildrenCollapsible').classList.add('hide');
|
page.querySelector('#listChildrenCollapsible').classList.add('hide');
|
||||||
@@ -859,6 +862,9 @@ function setInitialCollapsibleState(page, item, apiClient, context, user) {
|
|||||||
(item.People || []).forEach(p => {
|
(item.People || []).forEach(p => {
|
||||||
if (p.Type === PersonKind.GuestStar) {
|
if (p.Type === PersonKind.GuestStar) {
|
||||||
guestCast.push(p);
|
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 {
|
} else {
|
||||||
cast.push(p);
|
cast.push(p);
|
||||||
}
|
}
|
||||||
@@ -980,10 +986,11 @@ function renderDetails(page, instance, item, apiClient, context) {
|
|||||||
const itemDetailsGroup = page.querySelector('.itemDetailsGroup');
|
const itemDetailsGroup = page.querySelector('.itemDetailsGroup');
|
||||||
|
|
||||||
if (itemDetailsGroup) {
|
if (itemDetailsGroup) {
|
||||||
itemDetailsGroup.replaceChildren();
|
itemDetailsGroup.innerHTML = '';
|
||||||
|
|
||||||
const metadataTypes = [
|
const metadataTypes = [
|
||||||
PersonKind.Author,
|
PersonKind.Author,
|
||||||
|
PersonKind.Creator,
|
||||||
PersonKind.Director,
|
PersonKind.Director,
|
||||||
PersonKind.Writer,
|
PersonKind.Writer,
|
||||||
BaseItemKind.Studio,
|
BaseItemKind.Studio,
|
||||||
@@ -999,6 +1006,7 @@ function renderDetails(page, instance, item, apiClient, context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderItemCollections(page, item, apiClient, context);
|
||||||
renderSimilarItems(page, item, context);
|
renderSimilarItems(page, item, context);
|
||||||
renderMoreFromSeason(page, item, apiClient);
|
renderMoreFromSeason(page, item, apiClient);
|
||||||
renderMoreFromArtist(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) {
|
function renderSimilarItems(page, item, context) {
|
||||||
const similarCollapsible = page.querySelector('#similarCollapsible');
|
const similarCollapsible = page.querySelector('#similarCollapsible');
|
||||||
|
|
||||||
@@ -1450,14 +1509,14 @@ function renderChildren(page, item) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItemsByName(page, item) {
|
function renderItemsByName(page, item, user) {
|
||||||
import('../../scripts/itemsByName').then(({ default: ItemsByName }) => {
|
import('scripts/itemsByName').then(({ default: ItemsByName }) => {
|
||||||
ItemsByName.renderItems(page, item);
|
ItemsByName.renderItems(page, item, user);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPlaylistItems(page, item) {
|
function renderPlaylistItems(page, item) {
|
||||||
import('../../scripts/playlistViewer').then(({ default: PlaylistViewer }) => {
|
import('scripts/playlistViewer').then(({ default: PlaylistViewer }) => {
|
||||||
PlaylistViewer.render(page, item);
|
PlaylistViewer.render(page, item);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1734,7 +1793,7 @@ function renderScenes(page, item) {
|
|||||||
page.querySelector('#scenesCollapsible').classList.remove('hide');
|
page.querySelector('#scenesCollapsible').classList.remove('hide');
|
||||||
const scenesContent = page.querySelector('#scenesContent');
|
const scenesContent = page.querySelector('#scenesContent');
|
||||||
|
|
||||||
import('../../components/cardbuilder/chaptercardbuilder').then(({ default: chaptercardbuilder }) => {
|
import('components/cardbuilder/chaptercardbuilder').then(({ default: chaptercardbuilder }) => {
|
||||||
chaptercardbuilder.buildChapterCards(item, chapters, {
|
chaptercardbuilder.buildChapterCards(item, chapters, {
|
||||||
itemsContainer: scenesContent,
|
itemsContainer: scenesContent,
|
||||||
backdropShape: 'overflowBackdrop',
|
backdropShape: 'overflowBackdrop',
|
||||||
@@ -1776,7 +1835,7 @@ function renderCast(page, item, people) {
|
|||||||
page.querySelector('#castCollapsible').classList.remove('hide');
|
page.querySelector('#castCollapsible').classList.remove('hide');
|
||||||
const castContent = page.querySelector('#castContent');
|
const castContent = page.querySelector('#castContent');
|
||||||
|
|
||||||
import('../../components/cardbuilder/peoplecardbuilder').then(({ default: peoplecardbuilder }) => {
|
import('components/cardbuilder/peoplecardbuilder').then(({ default: peoplecardbuilder }) => {
|
||||||
peoplecardbuilder.buildPeopleCards(people, {
|
peoplecardbuilder.buildPeopleCards(people, {
|
||||||
itemsContainer: castContent,
|
itemsContainer: castContent,
|
||||||
coverImage: true,
|
coverImage: true,
|
||||||
@@ -1796,7 +1855,7 @@ function renderGuestCast(page, item, people) {
|
|||||||
page.querySelector('#guestCastCollapsible').classList.remove('hide');
|
page.querySelector('#guestCastCollapsible').classList.remove('hide');
|
||||||
const guestCastContent = page.querySelector('#guestCastContent');
|
const guestCastContent = page.querySelector('#guestCastContent');
|
||||||
|
|
||||||
import('../../components/cardbuilder/peoplecardbuilder').then(({ default: peoplecardbuilder }) => {
|
import('components/cardbuilder/peoplecardbuilder').then(({ default: peoplecardbuilder }) => {
|
||||||
peoplecardbuilder.buildPeopleCards(people, {
|
peoplecardbuilder.buildPeopleCards(people, {
|
||||||
itemsContainer: guestCastContent,
|
itemsContainer: guestCastContent,
|
||||||
coverImage: true,
|
coverImage: true,
|
||||||
@@ -1840,7 +1899,7 @@ window.ItemDetailPage = new ItemDetailPage();
|
|||||||
|
|
||||||
export default function (view, params) {
|
export default function (view, params) {
|
||||||
function getApiClient() {
|
function getApiClient() {
|
||||||
return params.serverId ? ServerConnections.getApiClient(params.serverId) : ApiClient;
|
return params.serverId ? ServerConnections.getApiClient(params.serverId) : ServerConnections.currentApiClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
function reload(instance, page, pageParams) {
|
function reload(instance, page, pageParams) {
|
||||||
@@ -1857,16 +1916,21 @@ export default function (view, params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function splitVersions(instance, page, apiClient, pageParams) {
|
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 () {
|
confirm(globalize.translate('ConfirmSplitMedia'), globalize.translate('HeaderSplitMedia'))
|
||||||
loading.show();
|
.then(() => {
|
||||||
apiClient.ajax({
|
loading.show();
|
||||||
type: 'DELETE',
|
return apiClient.ajax({
|
||||||
url: apiClient.getUrl('Videos/' + pageParams.id + '/AlternateSources')
|
type: 'DELETE',
|
||||||
}).then(function () {
|
url: apiClient.getUrl('Videos/' + pageParams.id + '/AlternateSources')
|
||||||
loading.hide();
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
reload(instance, page, pageParams);
|
reload(instance, page, pageParams);
|
||||||
|
Events.trigger(document, EventType.REFRESH_NEEDED);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.hide();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlayOptions(startPosition) {
|
function getPlayOptions(startPosition) {
|
||||||
@@ -1926,7 +1990,7 @@ export default function (view, params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onCancelSeriesTimerClick() {
|
function onCancelSeriesTimerClick() {
|
||||||
import('../../components/recordingcreator/recordinghelper').then(({ default: recordingHelper }) => {
|
import('components/recordingcreator/recordinghelper').then(({ default: recordingHelper }) => {
|
||||||
recordingHelper.cancelSeriesTimerWithConfirmation(currentItem.Id, currentItem.ServerId).then(function () {
|
recordingHelper.cancelSeriesTimerWithConfirmation(currentItem.Id, currentItem.ServerId).then(function () {
|
||||||
Dashboard.navigate('livetv');
|
Dashboard.navigate('livetv');
|
||||||
});
|
});
|
||||||
@@ -1934,7 +1998,7 @@ export default function (view, params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onCancelTimerClick() {
|
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 () {
|
recordingHelper.cancelTimer(ServerConnections.getApiClient(currentItem.ServerId), currentItem.TimerId).then(function () {
|
||||||
reload(self, view, params);
|
reload(self, view, params);
|
||||||
});
|
});
|
||||||
@@ -1946,7 +2010,12 @@ export default function (view, params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onDownloadClick() {
|
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 });
|
const url = getLibraryApi(api).getDownloadUrl({ itemId: currentItem.Id });
|
||||||
download([{
|
download([{
|
||||||
url,
|
url,
|
||||||
@@ -1992,21 +2061,18 @@ export default function (view, params) {
|
|||||||
setTrailerButtonVisibility(view, currentItem);
|
setTrailerButtonVisibility(view, currentItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWebSocketMessage(e, data) {
|
function onUserDataChanged({ Data }) {
|
||||||
const msg = data;
|
|
||||||
const apiClient = getApiClient();
|
const apiClient = getApiClient();
|
||||||
|
|
||||||
if (msg.MessageType === 'UserDataChanged' && currentItem && msg.Data.UserId == apiClient.getCurrentUserId()) {
|
if (!currentItem || Data?.UserId != apiClient.getCurrentUserId()) return;
|
||||||
const key = currentItem.UserData.Key;
|
|
||||||
const userData = msg.Data.UserDataList.filter(function (u) {
|
|
||||||
return u.Key == key;
|
|
||||||
})[0];
|
|
||||||
|
|
||||||
if (userData) {
|
const key = currentItem.UserData.Key;
|
||||||
currentItem.UserData = userData;
|
const userData = (Data?.UserDataList ?? []).find(u => u.Key == key);
|
||||||
reloadPlayButtons(view, currentItem);
|
|
||||||
autoFocus(view);
|
if (userData) {
|
||||||
}
|
currentItem.UserData = userData;
|
||||||
|
reloadPlayButtons(view, currentItem);
|
||||||
|
autoFocus(view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2036,6 +2102,7 @@ export default function (view, params) {
|
|||||||
renderAudioSelections(view, self._currentPlaybackMediaSources);
|
renderAudioSelections(view, self._currentPlaybackMediaSources);
|
||||||
renderSubtitleSelections(view, self._currentPlaybackMediaSources);
|
renderSubtitleSelections(view, self._currentPlaybackMediaSources);
|
||||||
updateMiscInfo();
|
updateMiscInfo();
|
||||||
|
refreshSelectedVersion();
|
||||||
});
|
});
|
||||||
view.addEventListener('viewshow', function (e) {
|
view.addEventListener('viewshow', function (e) {
|
||||||
const page = this;
|
const page = this;
|
||||||
@@ -2052,14 +2119,18 @@ export default function (view, params) {
|
|||||||
reload(self, page, params);
|
reload(self, page, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
Events.on(apiClient, 'message', onWebSocketMessage);
|
self._unsubscribeUserData = apiClient.subscribe(
|
||||||
|
[OutboundWebSocketMessageType.UserDataChanged],
|
||||||
|
onUserDataChanged
|
||||||
|
);
|
||||||
Events.on(playbackManager, 'playerchange', onPlayerChange);
|
Events.on(playbackManager, 'playerchange', onPlayerChange);
|
||||||
|
|
||||||
itemShortcuts.on(view.querySelector('.nameContainer'));
|
itemShortcuts.on(view.querySelector('.nameContainer'));
|
||||||
});
|
});
|
||||||
view.addEventListener('viewbeforehide', function () {
|
view.addEventListener('viewbeforehide', function () {
|
||||||
itemShortcuts.off(view.querySelector('.nameContainer'));
|
itemShortcuts.off(view.querySelector('.nameContainer'));
|
||||||
Events.off(apiClient, 'message', onWebSocketMessage);
|
self._unsubscribeUserData?.();
|
||||||
|
self._unsubscribeUserData = null;
|
||||||
Events.off(playbackManager, 'playerchange', onPlayerChange);
|
Events.off(playbackManager, 'playerchange', onPlayerChange);
|
||||||
libraryMenu.setTransparentMenu(false);
|
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();
|
init();
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import globalize from '../lib/globalize';
|
import globalize from 'lib/globalize';
|
||||||
import listView from '../components/listview/listview';
|
import listView from 'components/listview/listview';
|
||||||
import * as userSettings from '../scripts/settings/userSettings';
|
import * as userSettings from 'scripts/settings/userSettings';
|
||||||
import focusManager from '../components/focusManager';
|
import focusManager from 'components/focusManager';
|
||||||
import cardBuilder from '../components/cardbuilder/cardBuilder';
|
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||||
import loading from '../components/loading/loading';
|
import loading from 'components/loading/loading';
|
||||||
import AlphaNumericShortcuts from '../scripts/alphanumericshortcuts';
|
import AlphaNumericShortcuts from 'scripts/alphanumericshortcuts';
|
||||||
import libraryBrowser from '../scripts/libraryBrowser';
|
import libraryBrowser from 'scripts/libraryBrowser';
|
||||||
import { playbackManager } from '../components/playback/playbackmanager';
|
import { playbackManager } from 'components/playback/playbackmanager';
|
||||||
import AlphaPicker from '../components/alphaPicker/alphaPicker';
|
import AlphaPicker from 'components/alphaPicker/alphaPicker';
|
||||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||||
import '../elements/emby-itemscontainer/emby-itemscontainer';
|
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
import '../elements/emby-scroller/emby-scroller';
|
import 'elements/emby-scroller/emby-scroller';
|
||||||
import LibraryMenu from '../scripts/libraryMenu';
|
import LibraryMenu from 'scripts/libraryMenu';
|
||||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by';
|
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by';
|
||||||
import { stopMultiSelect } from 'components/multiSelect/multiSelect';
|
import { stopMultiSelect } from 'components/multiSelect/multiSelect';
|
||||||
@@ -315,7 +315,7 @@ function getItems(instance, params, item, sortBy, startIndex, limit) {
|
|||||||
} else if (item.CollectionType === CollectionType.Tvshows) {
|
} else if (item.CollectionType === CollectionType.Tvshows) {
|
||||||
query.IncludeItemTypes = 'Series';
|
query.IncludeItemTypes = 'Series';
|
||||||
} else if (item.Type === 'Genre') {
|
} else if (item.Type === 'Genre') {
|
||||||
query.IncludeItemTypes = 'Movie,Series,Video';
|
query.IncludeItemTypes = 'Audiobook,Book,BoxSet,Movie,Series,Video';
|
||||||
} else if (item.Type === 'Person') {
|
} else if (item.Type === 'Person') {
|
||||||
query.IncludeItemTypes = params.type;
|
query.IncludeItemTypes = params.type;
|
||||||
}
|
}
|
||||||
@@ -332,7 +332,11 @@ function getItems(instance, params, item, sortBy, startIndex, limit) {
|
|||||||
SortBy: sortBy
|
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;
|
instance.queryRecursive = true;
|
||||||
query.IncludeItemTypes = 'Video,Movie,Series,Music,MusicVideo';
|
query.IncludeItemTypes = 'Video,Movie,Series,Music,MusicVideo';
|
||||||
query.Recursive = true;
|
query.Recursive = true;
|
||||||
@@ -359,7 +363,7 @@ function getItem(params) {
|
|||||||
function showViewSettingsMenu() {
|
function showViewSettingsMenu() {
|
||||||
const instance = this;
|
const instance = this;
|
||||||
|
|
||||||
import('../components/viewSettings/viewSettings').then(({ default: ViewSettings }) => {
|
import('components/viewSettings/viewSettings').then(({ default: ViewSettings }) => {
|
||||||
new ViewSettings().show({
|
new ViewSettings().show({
|
||||||
settingsKey: instance.getSettingsKey(),
|
settingsKey: instance.getSettingsKey(),
|
||||||
settings: instance.getViewSettings(),
|
settings: instance.getViewSettings(),
|
||||||
@@ -374,7 +378,7 @@ function showViewSettingsMenu() {
|
|||||||
function showFilterMenu() {
|
function showFilterMenu() {
|
||||||
const instance = this;
|
const instance = this;
|
||||||
|
|
||||||
import('../components/filtermenu/filtermenu').then(({ default: FilterMenu }) => {
|
import('components/filtermenu/filtermenu').then(({ default: FilterMenu }) => {
|
||||||
new FilterMenu().show({
|
new FilterMenu().show({
|
||||||
settingsKey: instance.getSettingsKey(),
|
settingsKey: instance.getSettingsKey(),
|
||||||
settings: instance.getFilters(),
|
settings: instance.getFilters(),
|
||||||
@@ -393,7 +397,7 @@ function showFilterMenu() {
|
|||||||
function showSortMenu() {
|
function showSortMenu() {
|
||||||
const instance = this;
|
const instance = this;
|
||||||
|
|
||||||
import('../components/sortmenu/sortmenu').then(({ default: SortMenu }) => {
|
import('components/sortmenu/sortmenu').then(({ default: SortMenu }) => {
|
||||||
new SortMenu().show({
|
new SortMenu().show({
|
||||||
settingsKey: instance.getSettingsKey(),
|
settingsKey: instance.getSettingsKey(),
|
||||||
settings: instance.getSortValues(),
|
settings: instance.getSortValues(),
|
||||||
@@ -411,7 +415,7 @@ function showSortMenu() {
|
|||||||
function onNewItemClick() {
|
function onNewItemClick() {
|
||||||
const instance = this;
|
const instance = this;
|
||||||
|
|
||||||
import('../components/playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
import('components/playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
||||||
const playlistEditor = new PlaylistEditor();
|
const playlistEditor = new PlaylistEditor();
|
||||||
playlistEditor.show({
|
playlistEditor.show({
|
||||||
items: [],
|
items: [],
|
||||||
@@ -796,7 +800,7 @@ class ItemsView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function autoFocus() {
|
function autoFocus() {
|
||||||
import('../components/autoFocuser').then(({ default: autoFocuser }) => {
|
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||||
autoFocuser.autoFocus(view);
|
autoFocuser.autoFocus(view);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
+9
-9
@@ -1,12 +1,12 @@
|
|||||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||||
import imageLoader from '../../components/images/imageLoader';
|
import imageLoader from 'components/images/imageLoader';
|
||||||
import libraryBrowser from '../../scripts/libraryBrowser';
|
import libraryBrowser from 'scripts/libraryBrowser';
|
||||||
import loading from '../../components/loading/loading';
|
import loading from 'components/loading/loading';
|
||||||
import * as userSettings from '../../scripts/settings/userSettings';
|
import * as userSettings from 'scripts/settings/userSettings';
|
||||||
import Events from '../../utils/events.ts';
|
import Events from 'utils/events';
|
||||||
import { setFilterStatus } from 'components/filterdialog/filterIndicator';
|
import { setFilterStatus } from 'components/filterdialog/filterIndicator';
|
||||||
|
|
||||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
|
||||||
export default function (view, params, tabContent) {
|
export default function (view, params, tabContent) {
|
||||||
function getPageData() {
|
function getPageData() {
|
||||||
@@ -101,7 +101,7 @@ export default function (view, params, tabContent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showFilterMenu(context) {
|
function showFilterMenu(context) {
|
||||||
import('../../components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
|
import('components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
|
||||||
const filterDialog = new FilterDialog({
|
const filterDialog = new FilterDialog({
|
||||||
query: getQuery(),
|
query: getQuery(),
|
||||||
mode: 'livetvchannels',
|
mode: 'livetvchannels',
|
||||||
@@ -127,7 +127,7 @@ export default function (view, params, tabContent) {
|
|||||||
loading.hide();
|
loading.hide();
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
|
||||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||||
autoFocuser.autoFocus(context);
|
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) {
|
export default function (view, params, tabContent) {
|
||||||
let guideInstance;
|
let guideInstance;
|
||||||
+5
-5
@@ -1,8 +1,8 @@
|
|||||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||||
import imageLoader from '../../components/images/imageLoader';
|
import imageLoader from 'components/images/imageLoader';
|
||||||
import loading from '../../components/loading/loading';
|
import loading from 'components/loading/loading';
|
||||||
import '../../elements/emby-button/paper-icon-button-light';
|
import 'elements/emby-button/paper-icon-button-light';
|
||||||
import '../../elements/emby-button/emby-button';
|
import 'elements/emby-button/emby-button';
|
||||||
|
|
||||||
function renderTimers(context, timers) {
|
function renderTimers(context, timers) {
|
||||||
const html = cardBuilder.getCardsHtml({
|
const html = cardBuilder.getCardsHtml({
|
||||||
+1
-1
@@ -51,7 +51,7 @@ function loadRecommendedPrograms(page) {
|
|||||||
});
|
});
|
||||||
loading.hide();
|
loading.hide();
|
||||||
|
|
||||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||||
autoFocuser.autoFocus(page);
|
autoFocuser.autoFocus(page);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { getLyricsApi } from '@jellyfin/sdk/lib/utils/api/lyrics-api';
|
import { getLyricsApi } from '@jellyfin/sdk/lib/utils/api/lyrics-api';
|
||||||
import escapeHtml from 'escape-html';
|
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 autoFocuser from 'components/autoFocuser';
|
||||||
import { appRouter } from 'components/router/appRouter';
|
import { appRouter } from 'components/router/appRouter';
|
||||||
import layoutManager from 'components/layoutManager';
|
import layoutManager from 'components/layoutManager';
|
||||||
@@ -13,9 +13,8 @@ import { ServerConnections } from 'lib/jellyfin-apiclient';
|
|||||||
import keyboardNavigation from 'scripts/keyboardNavigation';
|
import keyboardNavigation from 'scripts/keyboardNavigation';
|
||||||
import LibraryMenu from 'scripts/libraryMenu';
|
import LibraryMenu from 'scripts/libraryMenu';
|
||||||
import Events from 'utils/events';
|
import Events from 'utils/events';
|
||||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
|
||||||
|
|
||||||
import '../styles/lyrics.scss';
|
import 'styles/lyrics.scss';
|
||||||
|
|
||||||
let currentPlayer;
|
let currentPlayer;
|
||||||
let currentItem;
|
let currentItem;
|
||||||
@@ -132,10 +131,13 @@ export default function (view) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getLyrics(serverId, itemId) {
|
function getLyrics(serverId, itemId) {
|
||||||
const apiClient = ServerConnections.getApiClient(serverId);
|
const api = ServerConnections.getApi(serverId);
|
||||||
const lyricsApi = getLyricsApi(toApi(apiClient));
|
if (!api) {
|
||||||
|
console.error('[Lyrics] no Api instance available for server', serverId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return lyricsApi.getLyrics({ itemId })
|
return getLyricsApi(api).getLyrics({ itemId })
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
if (!data.Lyrics?.length) {
|
if (!data.Lyrics?.length) {
|
||||||
throw new Error('No lyrics returned');
|
throw new Error('No lyrics returned');
|
||||||
+11
-11
@@ -1,12 +1,12 @@
|
|||||||
import loading from '../../components/loading/loading';
|
import loading from 'components/loading/loading';
|
||||||
import libraryBrowser from '../../scripts/libraryBrowser';
|
import libraryBrowser from 'scripts/libraryBrowser';
|
||||||
import { AlphaPicker } from '../../components/alphaPicker/alphaPicker';
|
import { AlphaPicker } from 'components/alphaPicker/alphaPicker';
|
||||||
import imageLoader from '../../components/images/imageLoader';
|
import imageLoader from 'components/images/imageLoader';
|
||||||
import listView from '../../components/listview/listview';
|
import listView from 'components/listview/listview';
|
||||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||||
import * as userSettings from '../../scripts/settings/userSettings';
|
import * as userSettings from 'scripts/settings/userSettings';
|
||||||
import globalize from '../../lib/globalize';
|
import globalize from 'lib/globalize';
|
||||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
|
||||||
export default function (view, params, tabContent) {
|
export default function (view, params, tabContent) {
|
||||||
function getPageData() {
|
function getPageData() {
|
||||||
@@ -189,7 +189,7 @@ export default function (view, params, tabContent) {
|
|||||||
loading.hide();
|
loading.hide();
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
|
||||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||||
autoFocuser.autoFocus(page);
|
autoFocuser.autoFocus(page);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -242,7 +242,7 @@ export default function (view, params, tabContent) {
|
|||||||
reloadItems(tabElement);
|
reloadItems(tabElement);
|
||||||
});
|
});
|
||||||
tabElement.querySelector('.btnNewCollection').addEventListener('click', () => {
|
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 serverId = ApiClient.serverInfo().Id;
|
||||||
const collectionEditor = new CollectionEditor();
|
const collectionEditor = new CollectionEditor();
|
||||||
collectionEditor.show({
|
collectionEditor.show({
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import loading from '../../components/loading/loading';
|
import loading from 'components/loading/loading';
|
||||||
import * as userSettings from '../../scripts/settings/userSettings';
|
import * as userSettings from 'scripts/settings/userSettings';
|
||||||
import libraryBrowser from '../../scripts/libraryBrowser';
|
import libraryBrowser from 'scripts/libraryBrowser';
|
||||||
import { AlphaPicker } from '../../components/alphaPicker/alphaPicker';
|
import { AlphaPicker } from 'components/alphaPicker/alphaPicker';
|
||||||
import listView from '../../components/listview/listview';
|
import listView from 'components/listview/listview';
|
||||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||||
import globalize from '../../lib/globalize';
|
import globalize from 'lib/globalize';
|
||||||
import Events from '../../utils/events.ts';
|
import Events from 'utils/events';
|
||||||
import { playbackManager } from '../../components/playback/playbackmanager';
|
import { playbackManager } from 'components/playback/playbackmanager';
|
||||||
import { getFilterStatus, setFilterStatus } from 'components/filterdialog/filterIndicator';
|
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) {
|
export default function (view, params, tabContent, options) {
|
||||||
const onViewStyleChange = () => {
|
const onViewStyleChange = () => {
|
||||||
@@ -108,7 +108,7 @@ export default function (view, params, tabContent, options) {
|
|||||||
isLoading = false;
|
isLoading = false;
|
||||||
loading.hide();
|
loading.hide();
|
||||||
|
|
||||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||||
autoFocuser.autoFocus(tabContent);
|
autoFocuser.autoFocus(tabContent);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -307,7 +307,7 @@ export default function (view, params, tabContent, options) {
|
|||||||
query = userSettings.loadQuerySettings(savedQueryKey, query);
|
query = userSettings.loadQuerySettings(savedQueryKey, query);
|
||||||
|
|
||||||
this.showFilterMenu = function () {
|
this.showFilterMenu = function () {
|
||||||
import('../../components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
|
import('components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
|
||||||
const filterDialog = new FilterDialog({
|
const filterDialog = new FilterDialog({
|
||||||
query: query,
|
query: query,
|
||||||
mode: 'movies',
|
mode: 'movies',
|
||||||
+4
-4
@@ -149,7 +149,7 @@ function getRecommendationHtml(recommendation) {
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadSuggestions(page, userId) {
|
function loadSuggestions(page, userId, parentId) {
|
||||||
const screenWidth = dom.getWindowSize().innerWidth;
|
const screenWidth = dom.getWindowSize().innerWidth;
|
||||||
let itemLimit = 5;
|
let itemLimit = 5;
|
||||||
if (screenWidth >= 1600) {
|
if (screenWidth >= 1600) {
|
||||||
@@ -160,6 +160,7 @@ function loadSuggestions(page, userId) {
|
|||||||
|
|
||||||
const url = ApiClient.getUrl('Movies/Recommendations', {
|
const url = ApiClient.getUrl('Movies/Recommendations', {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
ParentId: parentId,
|
||||||
categoryLimit: 6,
|
categoryLimit: 6,
|
||||||
ItemLimit: itemLimit,
|
ItemLimit: itemLimit,
|
||||||
Fields: 'PrimaryImageAspectRatio,MediaSourceCount',
|
Fields: 'PrimaryImageAspectRatio,MediaSourceCount',
|
||||||
@@ -185,7 +186,7 @@ function loadSuggestions(page, userId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function autoFocus(page) {
|
function autoFocus(page) {
|
||||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||||
autoFocuser.autoFocus(page);
|
autoFocuser.autoFocus(page);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -223,7 +224,7 @@ function loadSuggestionsTab(view, params, tabContent) {
|
|||||||
const userId = ApiClient.getCurrentUserId();
|
const userId = ApiClient.getCurrentUserId();
|
||||||
loadResume(tabContent, userId, parentId);
|
loadResume(tabContent, userId, parentId);
|
||||||
loadLatest(tabContent, userId, parentId);
|
loadLatest(tabContent, userId, parentId);
|
||||||
loadSuggestions(tabContent, userId);
|
loadSuggestions(tabContent, userId, parentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTabs() {
|
function getTabs() {
|
||||||
@@ -403,4 +404,3 @@ export default function (view, params) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+13
-14
@@ -1,16 +1,16 @@
|
|||||||
import { playbackManager } from '../../components/playback/playbackmanager';
|
import { playbackManager } from 'components/playback/playbackmanager';
|
||||||
import loading from '../../components/loading/loading';
|
import loading from 'components/loading/loading';
|
||||||
import libraryBrowser from '../../scripts/libraryBrowser';
|
import libraryBrowser from 'scripts/libraryBrowser';
|
||||||
import imageLoader from '../../components/images/imageLoader';
|
import imageLoader from 'components/images/imageLoader';
|
||||||
import AlphaPicker from '../../components/alphaPicker/alphaPicker';
|
import AlphaPicker from 'components/alphaPicker/alphaPicker';
|
||||||
import listView from '../../components/listview/listview';
|
import listView from 'components/listview/listview';
|
||||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||||
import * as userSettings from '../../scripts/settings/userSettings';
|
import * as userSettings from 'scripts/settings/userSettings';
|
||||||
import globalize from '../../lib/globalize';
|
import globalize from 'lib/globalize';
|
||||||
import Events from '../../utils/events.ts';
|
import Events from 'utils/events';
|
||||||
import { setFilterStatus } from 'components/filterdialog/filterIndicator';
|
import { setFilterStatus } from 'components/filterdialog/filterIndicator';
|
||||||
|
|
||||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
|
||||||
export default function (view, params, tabContent) {
|
export default function (view, params, tabContent) {
|
||||||
function playAll() {
|
function playAll() {
|
||||||
@@ -177,7 +177,7 @@ export default function (view, params, tabContent) {
|
|||||||
loading.hide();
|
loading.hide();
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
|
||||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||||
autoFocuser.autoFocus(tabContent);
|
autoFocuser.autoFocus(tabContent);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -187,7 +187,7 @@ export default function (view, params, tabContent) {
|
|||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
|
|
||||||
this.showFilterMenu = function () {
|
this.showFilterMenu = function () {
|
||||||
import('../../components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
|
import('components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
|
||||||
const filterDialog = new FilterDialog({
|
const filterDialog = new FilterDialog({
|
||||||
query: getQuery(),
|
query: getQuery(),
|
||||||
mode: 'albums',
|
mode: 'albums',
|
||||||
@@ -296,4 +296,3 @@ export default function (view, params, tabContent) {
|
|||||||
this.alphaPicker?.updateControls(getQuery());
|
this.alphaPicker?.updateControls(getQuery());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
+11
-12
@@ -1,14 +1,14 @@
|
|||||||
import loading from '../../components/loading/loading';
|
import loading from 'components/loading/loading';
|
||||||
import libraryBrowser from '../../scripts/libraryBrowser';
|
import libraryBrowser from 'scripts/libraryBrowser';
|
||||||
import imageLoader from '../../components/images/imageLoader';
|
import imageLoader from 'components/images/imageLoader';
|
||||||
import { AlphaPicker } from '../../components/alphaPicker/alphaPicker';
|
import { AlphaPicker } from 'components/alphaPicker/alphaPicker';
|
||||||
import listView from '../../components/listview/listview';
|
import listView from 'components/listview/listview';
|
||||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||||
import * as userSettings from '../../scripts/settings/userSettings';
|
import * as userSettings from 'scripts/settings/userSettings';
|
||||||
import Events from '../../utils/events.ts';
|
import Events from 'utils/events';
|
||||||
import { setFilterStatus } from 'components/filterdialog/filterIndicator';
|
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) {
|
export default function (view, params, tabContent, options) {
|
||||||
function getPageData() {
|
function getPageData() {
|
||||||
@@ -159,7 +159,7 @@ export default function (view, params, tabContent, options) {
|
|||||||
loading.hide();
|
loading.hide();
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
|
||||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||||
autoFocuser.autoFocus(tabContent);
|
autoFocuser.autoFocus(tabContent);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -169,7 +169,7 @@ export default function (view, params, tabContent, options) {
|
|||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
|
|
||||||
this.showFilterMenu = function () {
|
this.showFilterMenu = function () {
|
||||||
import('../../components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
|
import('components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
|
||||||
const filterDialog = new FilterDialog({
|
const filterDialog = new FilterDialog({
|
||||||
query: getQuery(),
|
query: getQuery(),
|
||||||
mode: options.mode,
|
mode: options.mode,
|
||||||
@@ -238,4 +238,3 @@ export default function (view, params, tabContent, options) {
|
|||||||
this.alphaPicker?.updateControls(getQuery());
|
this.alphaPicker?.updateControls(getQuery());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
+5
-5
@@ -1,7 +1,7 @@
|
|||||||
import * as userSettings from '../../scripts/settings/userSettings';
|
import * as userSettings from 'scripts/settings/userSettings';
|
||||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||||
import imageLoader from '../../components/images/imageLoader';
|
import imageLoader from 'components/images/imageLoader';
|
||||||
import loading from '../../components/loading/loading';
|
import loading from 'components/loading/loading';
|
||||||
|
|
||||||
export default function (view, params, tabContent) {
|
export default function (view, params, tabContent) {
|
||||||
function getPageData() {
|
function getPageData() {
|
||||||
@@ -90,7 +90,7 @@ export default function (view, params, tabContent) {
|
|||||||
userSettings.saveQuerySettings(getSavedQueryKey(), query);
|
userSettings.saveQuerySettings(getSavedQueryKey(), query);
|
||||||
loading.hide();
|
loading.hide();
|
||||||
|
|
||||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||||
autoFocuser.autoFocus(context);
|
autoFocuser.autoFocus(context);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+5
-5
@@ -1,7 +1,7 @@
|
|||||||
import * as userSettings from '../../scripts/settings/userSettings';
|
import * as userSettings from 'scripts/settings/userSettings';
|
||||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||||
import imageLoader from '../../components/images/imageLoader';
|
import imageLoader from 'components/images/imageLoader';
|
||||||
import loading from '../../components/loading/loading';
|
import loading from 'components/loading/loading';
|
||||||
|
|
||||||
export default function (view, params, tabContent) {
|
export default function (view, params, tabContent) {
|
||||||
function getPageData() {
|
function getPageData() {
|
||||||
@@ -60,7 +60,7 @@ export default function (view, params, tabContent) {
|
|||||||
userSettings.saveQuerySettings(getSavedQueryKey(), query);
|
userSettings.saveQuerySettings(getSavedQueryKey(), query);
|
||||||
loading.hide();
|
loading.hide();
|
||||||
|
|
||||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||||
autoFocuser.autoFocus(context);
|
autoFocuser.autoFocus(context);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+2
-2
@@ -73,7 +73,7 @@ function loadLatest(page, parentId) {
|
|||||||
imageLoader.lazyChildren(elem);
|
imageLoader.lazyChildren(elem);
|
||||||
loading.hide();
|
loading.hide();
|
||||||
|
|
||||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
import('components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||||
autoFocuser.autoFocus(page);
|
autoFocuser.autoFocus(page);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -169,7 +169,7 @@ function loadSuggestionsTab(page, tabContent, parentId) {
|
|||||||
loadRecentlyPlayed(tabContent, parentId);
|
loadRecentlyPlayed(tabContent, parentId);
|
||||||
loadFrequentlyPlayed(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']);
|
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