Compare commits
189 Commits
sortmedias
...
release-10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cae5c2646 | ||
|
|
5195006f7c | ||
|
|
6f203b9d1d | ||
|
|
2682098f61 | ||
|
|
455db67286 | ||
|
|
6b1352a855 | ||
|
|
a1721ddd17 | ||
|
|
5051ee2d8e | ||
|
|
7d30057c37 | ||
|
|
2b1f3470f4 | ||
|
|
d2e09f9cae | ||
|
|
b9925ebf73 | ||
|
|
2ebf0c9fe4 | ||
|
|
838e14e89f | ||
|
|
b9760eac75 | ||
|
|
61eb481d20 | ||
|
|
b1a6fd5d4e | ||
|
|
20ea6041a7 | ||
|
|
ef00d439b1 | ||
|
|
7807f8f062 | ||
|
|
7949ff4f0a | ||
|
|
d47023855e | ||
|
|
6a8f21e462 | ||
|
|
90236c25ee | ||
|
|
2305240cb9 | ||
|
|
8bc954468a | ||
|
|
76a28e125e | ||
|
|
2e4e4050cd | ||
|
|
4071c44437 | ||
|
|
44afbc2357 | ||
|
|
a0e6da790c | ||
|
|
f1ecb967bf | ||
|
|
fc4f396808 | ||
|
|
7f575d724e | ||
|
|
0c5a433bbf | ||
|
|
219cda9c06 | ||
|
|
4598d66688 | ||
|
|
3eeebf9bd2 | ||
|
|
4de2a05264 | ||
|
|
7acdb66e14 | ||
|
|
665678d5d7 | ||
|
|
7991d15177 | ||
|
|
f6acb157c6 | ||
|
|
8ddd9ecd9d | ||
|
|
1adaf00cb3 | ||
|
|
3235e2e594 | ||
|
|
b6844e61e2 | ||
|
|
91d40a0c4e | ||
|
|
c98822a7c6 | ||
|
|
83503936cc | ||
|
|
ebe3f0feb7 | ||
|
|
f3bb9f2eef | ||
|
|
8c06742d2d | ||
|
|
2d68f94ec6 | ||
|
|
9501c5097b | ||
|
|
86ff77924e | ||
|
|
9e7ad28eaf | ||
|
|
765394b6f2 | ||
|
|
798b408bd7 | ||
|
|
e669a9be02 | ||
|
|
e0a0c92b43 | ||
|
|
2d2d5bef94 | ||
|
|
0b34c1812e | ||
|
|
edd32297ee | ||
|
|
7372e837ee | ||
|
|
71d4f7083d | ||
|
|
bc8b83be5e | ||
|
|
c89846c039 | ||
|
|
4ffa90cdd7 | ||
|
|
21ced03987 | ||
|
|
18061ce247 | ||
|
|
f507bfb016 | ||
|
|
98207228d6 | ||
|
|
30c1926e4e | ||
|
|
84e7b59e03 | ||
|
|
cb085ff955 | ||
|
|
4bb0c67340 | ||
|
|
4ec0e2f086 | ||
|
|
674b0b118f | ||
|
|
aed4ffa2cd | ||
|
|
a031aab622 | ||
|
|
fa4b109037 | ||
|
|
3bb9d44f85 | ||
|
|
7e20d3032f | ||
|
|
a0c2202e64 | ||
|
|
5495ef220a | ||
|
|
7a88d5f02d | ||
|
|
24f4833742 | ||
|
|
d898afdf10 | ||
|
|
238b44f1bb | ||
|
|
52aa8ebd49 | ||
|
|
7865170eb6 | ||
|
|
2a110f6b5d | ||
|
|
5680c18ade | ||
|
|
07bbe67927 | ||
|
|
611922d260 | ||
|
|
c35cec5c77 | ||
|
|
ab781678c1 | ||
|
|
574eddada8 | ||
|
|
7d057c58cf | ||
|
|
4dd44dfd9f | ||
|
|
74a3bd8768 | ||
|
|
7854c4b20b | ||
|
|
ba36747dbb | ||
|
|
cbedc384b3 | ||
|
|
d9c5440864 | ||
|
|
60af8a68f8 | ||
|
|
2a9892db85 | ||
|
|
4129676ed8 | ||
|
|
75ef961530 | ||
|
|
4959a777c9 | ||
|
|
003bc94e02 | ||
|
|
40e7dc9007 | ||
|
|
c135be012c | ||
|
|
ee6909325d | ||
|
|
3a0be7d345 | ||
|
|
a67fd2e5ac | ||
|
|
472fc09a50 | ||
|
|
4eeb79b3e1 | ||
|
|
a35c81a0eb | ||
|
|
adcea4467d | ||
|
|
6feb46fecb | ||
|
|
20e29b81b5 | ||
|
|
2b59a9f998 | ||
|
|
a66e4d6d1a | ||
|
|
ea1cadf4b6 | ||
|
|
788ce37c43 | ||
|
|
c1db082629 | ||
|
|
a51d700eff | ||
|
|
61976b8101 | ||
|
|
35e4fe497e | ||
|
|
1ea598968c | ||
|
|
167515dbf0 | ||
|
|
f7f5ac99b0 | ||
|
|
a88d03fe8f | ||
|
|
3630ac0436 | ||
|
|
a71fe63684 | ||
|
|
b49eb09a08 | ||
|
|
bd03c43716 | ||
|
|
e56c46a913 | ||
|
|
9e34ae8b42 | ||
|
|
7342e43bd4 | ||
|
|
b5fda71a27 | ||
|
|
14075c641a | ||
|
|
e53c78a8ff | ||
|
|
adb662eb0b | ||
|
|
bb9b4ce8bb | ||
|
|
6da3dd7c86 | ||
|
|
9d9b69edd5 | ||
|
|
fb87dfbf5e | ||
|
|
1342bedad0 | ||
|
|
292240df46 | ||
|
|
add01e332b | ||
|
|
2280d98785 | ||
|
|
11e3bf395e | ||
|
|
0271ae42c0 | ||
|
|
68bac17a46 | ||
|
|
7eb54e029f | ||
|
|
70b9aa4611 | ||
|
|
3dcb42daac | ||
|
|
b8a7cf214d | ||
|
|
c0b86a39c7 | ||
|
|
a806eeb3a7 | ||
|
|
ab70cc07a8 | ||
|
|
ed321c4cdb | ||
|
|
7ce8c070b3 | ||
|
|
3402f1beba | ||
|
|
1e035d5867 | ||
|
|
91961de0ce | ||
|
|
ea1d069e90 | ||
|
|
bdce41c3ae | ||
|
|
b17ca028f8 | ||
|
|
37b1d5cbea | ||
|
|
1a172bdb1b | ||
|
|
7de4ebf33a | ||
|
|
32a91eabf1 | ||
|
|
25b1bcab50 | ||
|
|
703ec1b488 | ||
|
|
c0467b1f13 | ||
|
|
0fcb1ff983 | ||
|
|
1ad7dfb5c0 | ||
|
|
a358d34ea9 | ||
|
|
ea8ceaa727 | ||
|
|
d17c35acc3 | ||
|
|
df26f36a09 | ||
|
|
f980e38530 | ||
|
|
1d883f445b | ||
|
|
ac8c2239ca | ||
|
|
7dc9c1d7aa |
@@ -79,7 +79,6 @@ module.exports = {
|
||||
'operator-linebreak': ['error', 'before', { overrides: { '?': 'after', ':': 'after', '=': 'after' } }],
|
||||
'padded-blocks': ['error', 'never'],
|
||||
'prefer-const': ['error', { 'destructuring': 'all' }],
|
||||
'prefer-promise-reject-errors': ['warn', { 'allowEmptyReject': true }],
|
||||
'@typescript-eslint/prefer-for-of': ['error'],
|
||||
'@typescript-eslint/prefer-optional-chain': ['error'],
|
||||
'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
|
||||
@@ -264,7 +263,6 @@ module.exports = {
|
||||
'UserParentalControlPage': 'writable',
|
||||
'Windows': 'readonly',
|
||||
// Build time definitions
|
||||
__COMMIT_SHA__: 'readonly',
|
||||
__JF_BUILD_VERSION__: 'readonly',
|
||||
__PACKAGE_JSON_NAME__: 'readonly',
|
||||
__PACKAGE_JSON_VERSION__: 'readonly',
|
||||
|
||||
14
.github/workflows/automation.yml
vendored
14
.github/workflows/automation.yml
vendored
@@ -1,22 +1,20 @@
|
||||
name: Automation
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
name: 'Automation'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request_target:
|
||||
types:
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
conflicts:
|
||||
name: Merge conflict labeling
|
||||
triage:
|
||||
name: 'Merge conflict labeling'
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
steps:
|
||||
- uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
|
||||
- uses: eps1lon/actions-label-merge-conflict@6d74047dcef155976a15e4a124dde2c7fe0c5522 # v3.0.1
|
||||
with:
|
||||
dirtyLabel: 'merge conflict'
|
||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
||||
|
||||
93
.github/workflows/build.yml
vendored
93
.github/workflows/build.yml
vendored
@@ -7,7 +7,7 @@ concurrency:
|
||||
on:
|
||||
push:
|
||||
branches: [ master, release* ]
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
branches: [ master, release* ]
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -18,12 +18,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
@@ -43,87 +41,8 @@ jobs:
|
||||
mv dist/config.tmp.json dist/config.json
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: jellyfin-web__prod
|
||||
path: dist
|
||||
|
||||
publish:
|
||||
name: Deploy to Cloudflare Pages
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
needs:
|
||||
- run-build-prod
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
|
||||
steps:
|
||||
- name: Add comment
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
if: ${{ github.event_name == 'pull_request_target' }}
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
message: |
|
||||
## Cloudflare Pages deployment
|
||||
|
||||
| **Latest commit** | <code>${{ github.event.pull_request.head.sha || github.sha }}</code> |
|
||||
|-------------------|:-:|
|
||||
| **Status** | 🔄 Deploying... |
|
||||
| **Preview URL** | Not available |
|
||||
| **Type** | 🔀 Preview |
|
||||
pr_number: ${{ github.event.pull_request.number }}
|
||||
comment_tag: CFPages-deployment
|
||||
mode: recreate
|
||||
|
||||
- name: Download workflow artifact
|
||||
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
|
||||
with:
|
||||
name: jellyfin-web__prod
|
||||
path: dist
|
||||
|
||||
- name: Publish to Cloudflare
|
||||
id: cf
|
||||
uses: cloudflare/wrangler-action@f84a562284fc78278ff9052435d9526f9c718361 # v3.7.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy dist --project-name=jellyfin-web --branch=${{
|
||||
(github.event_name != 'pull_request_target' || github.event.pull_request.head.repo.full_name == github.repository)
|
||||
&& (github.event.pull_request.head.ref || github.ref_name)
|
||||
|| format('{0}/{1}', github.event.pull_request.head.repo.full_name, github.event.pull_request.head.ref)
|
||||
}} --commit-hash=${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Update status comment (Success)
|
||||
if: ${{ github.event_name == 'pull_request_target' && success() }}
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
message: |
|
||||
## Cloudflare Pages deployment
|
||||
|
||||
| **Latest commit** | <code>${{ github.event.pull_request.head.sha || github.sha }}</code> |
|
||||
|-------------------|:-:|
|
||||
| **Status** | ✅ Deployed! |
|
||||
| **Preview URL** | ${{ steps.cf.outputs.deployment-url != '' && steps.cf.outputs.deployment-url || 'Not available' }} |
|
||||
| **Type** | 🔀 Preview |
|
||||
pr_number: ${{ github.event.pull_request.number }}
|
||||
comment_tag: CFPages-deployment
|
||||
mode: recreate
|
||||
|
||||
- name: Update status comment (Failure)
|
||||
if: ${{ github.event_name == 'pull_request_target' && failure() }}
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
message: |
|
||||
## Cloudflare Pages deployment
|
||||
|
||||
| **Latest commit** | <code>${{ github.event.pull_request.head.sha || github.sha }}</code> |
|
||||
|-------------------|:-:|
|
||||
| **Status** | ❌ Failure. Check workflow logs for details |
|
||||
| **Preview URL** | Not available |
|
||||
| **Type** | 🔀 Preview |
|
||||
pr_number: ${{ github.event.pull_request.number }}
|
||||
comment_tag: CFPages-deployment
|
||||
mode: recreate
|
||||
path: |
|
||||
dist
|
||||
|
||||
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
@@ -19,16 +19,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
|
||||
uses: github/codeql-action/init@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||
with:
|
||||
languages: javascript
|
||||
queries: +security-extended
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
|
||||
uses: github/codeql-action/autobuild@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
|
||||
uses: github/codeql-action/analyze@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||
|
||||
2
.github/workflows/commands.yml
vendored
2
.github/workflows/commands.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reactions: '+1'
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
65
.github/workflows/job-messages.yml
vendored
Normal file
65
.github/workflows/job-messages.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
name: Job messages
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
branch:
|
||||
required: false
|
||||
type: string
|
||||
commit:
|
||||
required: true
|
||||
type: string
|
||||
preview_url:
|
||||
required: false
|
||||
type: string
|
||||
build_workflow_run_id:
|
||||
required: false
|
||||
type: number
|
||||
commenting_workflow_run_id:
|
||||
required: true
|
||||
type: string
|
||||
in_progress:
|
||||
required: true
|
||||
type: boolean
|
||||
outputs:
|
||||
msg:
|
||||
description: The composed message
|
||||
value: ${{ jobs.msg.outputs.msg }}
|
||||
marker:
|
||||
description: Hidden marker to detect PR comments composed by the bot
|
||||
value: "CFPages-deployment"
|
||||
|
||||
jobs:
|
||||
msg:
|
||||
name: Deployment status
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
msg: ${{ env.msg }}
|
||||
|
||||
steps:
|
||||
- name: Compose message
|
||||
if: ${{ always() }}
|
||||
id: compose
|
||||
env:
|
||||
COMMIT: ${{ inputs.commit }}
|
||||
PREVIEW_URL: ${{ inputs.preview_url != '' && (inputs.branch != 'master' && inputs.preview_url || format('https://jellyfin-web.pages.dev ({0})', inputs.preview_url)) || 'Not available' }}
|
||||
DEPLOY_STATUS: ${{ inputs.in_progress && '🔄 Deploying...' || (inputs.preview_url != '' && '✅ Deployed!' || '❌ Failure. Check workflow logs for details') }}
|
||||
DEPLOYMENT_TYPE: ${{ inputs.branch != 'master' && '🔀 Preview' || '⚙️ Production' }}
|
||||
BUILD_WORKFLOW_RUN: ${{ !inputs.in_progress && format('**[View build logs](https://github.com/{0}/actions/runs/{1})**', 'jellyfin/jellyfin-web', inputs.build_workflow_run_id) || '' }}
|
||||
COMMENTING_WORKFLOW_RUN: ${{ format('**[View bot logs](https://github.com/{0}/actions/runs/{1})**', 'jellyfin/jellyfin-web', inputs.commenting_workflow_run_id) }}
|
||||
# EOF is needed for multiline environment variables in a GitHub Actions context
|
||||
run: |
|
||||
echo "## Cloudflare Pages deployment" > $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Latest commit** | <code>${COMMIT::7}</code> |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------------------------- |:----------------------------: |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Status** | $DEPLOY_STATUS |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Preview URL** | $PREVIEW_URL |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Type** | $DEPLOYMENT_TYPE |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "$BUILD_WORKFLOW_RUN" >> $GITHUB_STEP_SUMMARY
|
||||
echo "$COMMENTING_WORKFLOW_RUN" >> $GITHUB_STEP_SUMMARY
|
||||
COMPOSED_MSG=$(cat $GITHUB_STEP_SUMMARY)
|
||||
echo "msg<<EOF" >> $GITHUB_ENV
|
||||
echo "$COMPOSED_MSG" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
8
.github/workflows/pr-suggestions.yml
vendored
8
.github/workflows/pr-suggestions.yml
vendored
@@ -7,6 +7,8 @@ concurrency:
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: [ master, release* ]
|
||||
types:
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
run-eslint:
|
||||
@@ -15,12 +17,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
@@ -31,6 +33,6 @@ jobs:
|
||||
|
||||
- name: Run eslint
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
uses: CatChen/eslint-suggestion-action@bc82950fa97bb3e46d9cca16a8bf2ad3e3c010fc # v4.1.5
|
||||
uses: CatChen/eslint-suggestion-action@b110ac684564c7b73e47cc223eb7a5266ec83fd3 # v4.1.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
84
.github/workflows/publish.yml
vendored
Normal file
84
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Build
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Deploy to Cloudflare Pages
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
# We set the environment variable here (and as an output) because,
|
||||
# given no real runner is dispatched in compose-comment job (it's dispatched in the reusable workflow) in this workflow definition,
|
||||
# the env. context is not valid.
|
||||
env:
|
||||
TARGET_BRANCH: |
|
||||
${{
|
||||
github.event.workflow_run.head_repository.full_name == github.repository
|
||||
&& github.event.workflow_run.head_branch
|
||||
|| format('{0}/{1}', github.event.workflow_run.head_repository.full_name, github.event.workflow_run.head_branch)
|
||||
}}
|
||||
outputs:
|
||||
url: ${{ steps.cf.outputs.url }}
|
||||
branch: ${{ env.TARGET_BRANCH }}
|
||||
|
||||
steps:
|
||||
- name: Download workflow artifact
|
||||
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
|
||||
with:
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: jellyfin-web__prod
|
||||
path: dist
|
||||
|
||||
- name: Publish
|
||||
id: cf
|
||||
uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # 1
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: jellyfin-web
|
||||
branch: ${{ env.TARGET_BRANCH }}
|
||||
directory: dist
|
||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
compose-comment:
|
||||
name: Compose comment
|
||||
if: ${{ always() }}
|
||||
uses: ./.github/workflows/job-messages.yml
|
||||
needs:
|
||||
- publish
|
||||
|
||||
with:
|
||||
branch: ${{ needs.publish.outputs.branch }}
|
||||
commit: ${{ github.event.workflow_run.head_commit.id }}
|
||||
preview_url: ${{ needs.publish.outputs.url }}
|
||||
build_workflow_run_id: ${{ github.event.workflow_run.id }}
|
||||
commenting_workflow_run_id: ${{ github.run_id }}
|
||||
in_progress: false
|
||||
|
||||
comment-status:
|
||||
name: Create comment status
|
||||
if: |
|
||||
always() &&
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.pull_requests[0].number != ''
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- compose-comment
|
||||
|
||||
steps:
|
||||
- name: Update job summary in PR comment
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
message: ${{ needs.compose-comment.outputs.msg }}
|
||||
pr_number: ${{ github.event.workflow_run.pull_requests[0].number }}
|
||||
comment_tag: ${{ needs.compose-comment.outputs.marker }}
|
||||
mode: recreate
|
||||
20
.github/workflows/quality.yml
vendored
20
.github/workflows/quality.yml
vendored
@@ -17,10 +17,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
@@ -41,10 +41,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
@@ -62,10 +62,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
@@ -86,10 +86,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
@@ -107,10 +107,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
|
||||
6
.github/workflows/update-sdk.yml
vendored
6
.github/workflows/update-sdk.yml
vendored
@@ -16,13 +16,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
ref: master
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
echo "JF_SDK_VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Open a pull request
|
||||
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
|
||||
uses: peter-evans/create-pull-request@6d6857d36972b65feb161a90e484f2984215f83e # v6.0.5
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
commit-message: Update @jellyfin/sdk to ${{env.JF_SDK_VERSION}}
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -3,9 +3,6 @@ dist
|
||||
web
|
||||
node_modules
|
||||
|
||||
# test coverage
|
||||
coverage
|
||||
|
||||
# config
|
||||
config.json
|
||||
|
||||
@@ -13,6 +10,12 @@ config.json
|
||||
.idea
|
||||
.vs
|
||||
|
||||
# log
|
||||
yarn-error.log
|
||||
|
||||
# vim
|
||||
*.sw?
|
||||
|
||||
# build artifacts
|
||||
fedora/jellyfin-web-*.src.rpm
|
||||
fedora/jellyfin-web-*.tar.gz
|
||||
|
||||
@@ -87,11 +87,9 @@
|
||||
- [JPUC1143](https://github.com/Jpuc1143)
|
||||
- [David Angel](https://github.com/davidangel)
|
||||
- [Pithaya](https://github.com/Pithaya)
|
||||
- [Peter Santos](https://github.com/prsantos-com)
|
||||
- [Chaitanya Shahare](https://github.com/Chaitanya-Shahare)
|
||||
- [Venkat Karasani](https://github.com/venkat-karasani)
|
||||
- [Connor Smith](https://github.com/ConnorS1110)
|
||||
- [iFraan](https://github.com/iFraan)
|
||||
- [Venkat Karasani](https://github.com/venkat-karasani)
|
||||
|
||||
## Emby Contributors
|
||||
|
||||
|
||||
@@ -85,11 +85,8 @@ Jellyfin Web is the frontend used for most of the clients available for end user
|
||||
├── controllers # Legacy page views and controllers 🧹
|
||||
├── elements # Basic webcomponents and React wrappers 🧹
|
||||
├── hooks # Custom React hooks
|
||||
├── lib # Reusable libraries
|
||||
│ ├── globalize # Custom localization library
|
||||
│ ├── legacy # Polyfills for legacy browsers
|
||||
│ ├── navdrawer # Navigation drawer library for classic layout
|
||||
│ └── scroller # Content scrolling library
|
||||
├── legacy # Polyfills for legacy browsers
|
||||
├── libraries # Third party libraries 🧹
|
||||
├── plugins # Client plugins
|
||||
├── scripts # Random assortment of visual components and utilities 🐉
|
||||
├── strings # Translation files
|
||||
|
||||
@@ -15,8 +15,8 @@ module.exports = {
|
||||
'@babel/preset-react'
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-transform-class-properties',
|
||||
'@babel/plugin-transform-private-methods',
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
'@babel/plugin-proposal-private-methods',
|
||||
'babel-plugin-dynamic-import-polyfill'
|
||||
]
|
||||
};
|
||||
|
||||
10846
package-lock.json
generated
10846
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
121
package.json
121
package.json
@@ -1,28 +1,26 @@
|
||||
{
|
||||
"name": "jellyfin-web",
|
||||
"version": "10.10.0",
|
||||
"version": "10.9.11",
|
||||
"description": "Web interface for Jellyfin",
|
||||
"repository": "https://github.com/jellyfin/jellyfin-web",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.24.9",
|
||||
"@babel/plugin-transform-class-properties": "7.24.7",
|
||||
"@babel/plugin-transform-modules-umd": "7.24.7",
|
||||
"@babel/plugin-transform-private-methods": "7.24.7",
|
||||
"@babel/preset-env": "7.24.8",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@types/dompurify": "3.0.5",
|
||||
"@babel/core": "7.24.3",
|
||||
"@babel/plugin-proposal-class-properties": "7.18.6",
|
||||
"@babel/plugin-proposal-private-methods": "7.18.6",
|
||||
"@babel/plugin-transform-modules-umd": "7.24.1",
|
||||
"@babel/preset-env": "7.24.3",
|
||||
"@babel/preset-react": "7.24.1",
|
||||
"@types/escape-html": "1.0.4",
|
||||
"@types/loadable__component": "5.13.9",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/markdown-it": "14.1.2",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/markdown-it": "13.0.7",
|
||||
"@types/react": "17.0.79",
|
||||
"@types/react-dom": "17.0.25",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "5.62.0",
|
||||
"@typescript-eslint/parser": "5.62.0",
|
||||
"@uupaa/dynamic-import-polyfill": "1.0.2",
|
||||
"@vitest/coverage-v8": "2.0.5",
|
||||
"autoprefixer": "10.4.19",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-dynamic-import-polyfill": "1.0.0",
|
||||
@@ -30,70 +28,71 @@
|
||||
"confusing-browser-globals": "1.0.11",
|
||||
"copy-webpack-plugin": "12.0.2",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "7.1.2",
|
||||
"cssnano": "7.0.4",
|
||||
"es-check": "7.2.1",
|
||||
"css-loader": "6.10.0",
|
||||
"cssnano": "6.1.2",
|
||||
"es-check": "7.1.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-compat": "4.2.0",
|
||||
"eslint-plugin-eslint-comments": "3.2.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "6.9.0",
|
||||
"eslint-plugin-react": "7.35.0",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-sonarjs": "0.25.1",
|
||||
"expose-loader": "5.0.0",
|
||||
"eslint-plugin-jsx-a11y": "6.8.0",
|
||||
"eslint-plugin-react": "7.34.1",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-sonarjs": "0.24.0",
|
||||
"expose-loader": "4.1.0",
|
||||
"fork-ts-checker-webpack-plugin": "9.0.2",
|
||||
"html-loader": "5.1.0",
|
||||
"html-loader": "4.2.0",
|
||||
"html-webpack-plugin": "5.6.0",
|
||||
"jsdom": "24.1.1",
|
||||
"mini-css-extract-plugin": "2.9.0",
|
||||
"postcss": "8.4.40",
|
||||
"postcss-loader": "8.1.1",
|
||||
"postcss-preset-env": "9.6.0",
|
||||
"jsdom": "23.2.0",
|
||||
"mini-css-extract-plugin": "2.8.1",
|
||||
"postcss": "8.4.38",
|
||||
"postcss-loader": "7.3.4",
|
||||
"postcss-preset-env": "9.5.2",
|
||||
"postcss-scss": "4.0.9",
|
||||
"sass": "1.77.8",
|
||||
"sass-loader": "15.0.0",
|
||||
"source-map-loader": "5.0.0",
|
||||
"sass": "1.72.0",
|
||||
"sass-loader": "13.3.3",
|
||||
"source-map-loader": "4.0.2",
|
||||
"speed-measure-webpack-plugin": "1.5.0",
|
||||
"style-loader": "4.0.0",
|
||||
"style-loader": "3.3.4",
|
||||
"stylelint": "15.11.0",
|
||||
"stylelint-config-rational-order": "0.1.2",
|
||||
"stylelint-no-browser-hacks": "1.3.0",
|
||||
"stylelint-order": "6.0.4",
|
||||
"stylelint-scss": "5.3.2",
|
||||
"ts-loader": "9.5.1",
|
||||
"typescript": "5.5.4",
|
||||
"vitest": "2.0.5",
|
||||
"webpack": "5.93.0",
|
||||
"webpack-bundle-analyzer": "4.10.2",
|
||||
"typescript": "5.4.3",
|
||||
"vitest": "1.4.0",
|
||||
"webpack": "5.91.0",
|
||||
"webpack-bundle-analyzer": "4.10.1",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.0.4",
|
||||
"webpack-merge": "6.0.1",
|
||||
"webpack-dev-server": "4.15.2",
|
||||
"webpack-merge": "5.10.0",
|
||||
"worker-loader": "3.0.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.13.0",
|
||||
"@emotion/styled": "11.13.0",
|
||||
"@fontsource/noto-sans": "5.0.22",
|
||||
"@fontsource/noto-sans-hk": "5.0.20",
|
||||
"@fontsource/noto-sans-jp": "5.0.19",
|
||||
"@fontsource/noto-sans-kr": "5.0.19",
|
||||
"@fontsource/noto-sans-sc": "5.0.20",
|
||||
"@fontsource/noto-sans-tc": "5.0.20",
|
||||
"@jellyfin/libass-wasm": "4.2.2",
|
||||
"@jellyfin/sdk": "0.0.0-unstable.202408050429",
|
||||
"@mui/icons-material": "5.15.19",
|
||||
"@mui/material": "5.15.19",
|
||||
"@mui/x-data-grid": "7.6.1",
|
||||
"@react-hook/resize-observer": "2.0.1",
|
||||
"@tanstack/react-query": "5.51.11",
|
||||
"@tanstack/react-query-devtools": "5.51.11",
|
||||
"@types/react-lazy-load-image-component": "1.6.4",
|
||||
"@emotion/react": "11.11.4",
|
||||
"@emotion/styled": "11.11.0",
|
||||
"@fontsource/noto-sans": "5.0.21",
|
||||
"@fontsource/noto-sans-hk": "5.0.18",
|
||||
"@fontsource/noto-sans-jp": "5.0.18",
|
||||
"@fontsource/noto-sans-kr": "5.0.18",
|
||||
"@fontsource/noto-sans-sc": "5.0.18",
|
||||
"@fontsource/noto-sans-tc": "5.0.18",
|
||||
"@jellyfin/libass-wasm": "4.2.1",
|
||||
"@jellyfin/sdk": "0.9.0",
|
||||
"@loadable/component": "5.16.3",
|
||||
"@mui/icons-material": "5.15.11",
|
||||
"@mui/material": "5.15.11",
|
||||
"@mui/x-data-grid": "6.19.5",
|
||||
"@react-hook/resize-observer": "1.2.6",
|
||||
"@tanstack/react-query": "4.36.1",
|
||||
"@tanstack/react-query-devtools": "4.36.1",
|
||||
"@types/react-lazy-load-image-component": "1.6.3",
|
||||
"abortcontroller-polyfill": "1.7.5",
|
||||
"blurhash": "2.0.5",
|
||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.37.1",
|
||||
"core-js": "3.36.1",
|
||||
"date-fns": "2.30.0",
|
||||
"dompurify": "3.0.1",
|
||||
"epubjs": "0.3.93",
|
||||
@@ -102,7 +101,7 @@
|
||||
"flv.js": "1.6.2",
|
||||
"headroom.js": "0.12.0",
|
||||
"history": "5.3.0",
|
||||
"hls.js": "1.5.13",
|
||||
"hls.js": "1.5.7",
|
||||
"intersection-observer": "0.12.2",
|
||||
"jellyfin-apiclient": "1.11.0",
|
||||
"jquery": "3.7.1",
|
||||
@@ -113,16 +112,16 @@
|
||||
"material-design-icons-iconfont": "6.7.0",
|
||||
"native-promise-only": "0.8.1",
|
||||
"pdfjs-dist": "3.11.174",
|
||||
"react": "18.3.1",
|
||||
"react": "17.0.2",
|
||||
"react-blurhash": "0.3.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-lazy-load-image-component": "1.6.2",
|
||||
"react-router-dom": "6.25.1",
|
||||
"react-dom": "17.0.2",
|
||||
"react-lazy-load-image-component": "1.6.0",
|
||||
"react-router-dom": "6.22.3",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"screenfull": "6.0.2",
|
||||
"sortablejs": "1.15.2",
|
||||
"swiper": "11.1.7",
|
||||
"usehooks-ts": "3.1.0",
|
||||
"swiper": "11.0.7",
|
||||
"usehooks-ts": "2.16.0",
|
||||
"webcomponents.js": "0.7.24",
|
||||
"whatwg-fetch": "3.6.20"
|
||||
},
|
||||
|
||||
@@ -6,17 +6,19 @@ import { ApiProvider } from 'hooks/useApi';
|
||||
import { WebConfigProvider } from 'hooks/useWebConfig';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
import RootAppRouter from 'RootAppRouter';
|
||||
import RootAppRouter from './RootAppRouter';
|
||||
|
||||
const RootApp = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ApiProvider>
|
||||
<WebConfigProvider>
|
||||
<RootAppRouter />
|
||||
</WebConfigProvider>
|
||||
</ApiProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
const RootApp = () => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ApiProvider>
|
||||
<WebConfigProvider>
|
||||
<RootAppRouter />
|
||||
</WebConfigProvider>
|
||||
</ApiProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default RootApp;
|
||||
|
||||
@@ -13,20 +13,23 @@ import { useApi } from 'hooks/useApi';
|
||||
|
||||
import AppTabs from './components/AppTabs';
|
||||
import AppDrawer from './components/drawer/AppDrawer';
|
||||
import { DASHBOARD_APP_PATHS } from './routes/routes';
|
||||
|
||||
import './AppOverrides.scss';
|
||||
|
||||
const DRAWERLESS_PATHS = [ DASHBOARD_APP_PATHS.MetadataManager ];
|
||||
interface AppLayoutProps {
|
||||
drawerlessPaths: string[]
|
||||
}
|
||||
|
||||
export const Component: FC = () => {
|
||||
const AppLayout: FC<AppLayoutProps> = ({
|
||||
drawerlessPaths
|
||||
}) => {
|
||||
const [ isDrawerActive, setIsDrawerActive ] = useState(false);
|
||||
const location = useLocation();
|
||||
const { user } = useApi();
|
||||
|
||||
const isMediumScreen = useMediaQuery((t: Theme) => t.breakpoints.up('md'));
|
||||
const isDrawerAvailable = Boolean(user)
|
||||
&& !DRAWERLESS_PATHS.some(path => location.pathname.startsWith(`/${path}`));
|
||||
&& !drawerlessPaths.some(path => location.pathname.startsWith(`/${path}`));
|
||||
const isDrawerOpen = isDrawerActive && isDrawerAvailable;
|
||||
|
||||
const onToggleDrawer = useCallback(() => {
|
||||
@@ -92,3 +95,5 @@ export const Component: FC = () => {
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import React from 'react';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
const LogLevelChip = ({ level }: { level: LogLevel }) => {
|
||||
let color: 'info' | 'warning' | 'error' | undefined;
|
||||
|
||||
@@ -29,8 +29,8 @@ const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
||||
<ServerDrawerSection />
|
||||
<DevicesDrawerSection />
|
||||
<LiveTvDrawerSection />
|
||||
<PluginDrawerSection />
|
||||
<AdvancedDrawerSection />
|
||||
<PluginDrawerSection />
|
||||
</ResponsiveDrawer>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
import Article from '@mui/icons-material/Article';
|
||||
import EditNotifications from '@mui/icons-material/EditNotifications';
|
||||
import ExpandLess from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
import Extension from '@mui/icons-material/Extension';
|
||||
import Lan from '@mui/icons-material/Lan';
|
||||
import Schedule from '@mui/icons-material/Schedule';
|
||||
import VpnKey from '@mui/icons-material/VpnKey';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
const PLUGIN_PATHS = [
|
||||
'/dashboard/plugins',
|
||||
'/dashboard/plugins/catalog',
|
||||
'/dashboard/plugins/repositories',
|
||||
'/dashboard/plugins/add',
|
||||
'/configurationpage'
|
||||
];
|
||||
|
||||
const AdvancedDrawerSection = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const isPluginSectionOpen = PLUGIN_PATHS.includes(location.pathname);
|
||||
|
||||
return (
|
||||
<List
|
||||
aria-labelledby='advanced-subheader'
|
||||
@@ -46,6 +64,36 @@ const AdvancedDrawerSection = () => {
|
||||
<ListItemText primary={globalize.translate('TabLogs')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/notifications'>
|
||||
<ListItemIcon>
|
||||
<EditNotifications />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('Notifications')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/plugins' selected={false}>
|
||||
<ListItemIcon>
|
||||
<Extension />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabPlugins')} />
|
||||
{isPluginSectionOpen ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<Collapse in={isPluginSectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/dashboard/plugins' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabMyPlugins')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dashboard/plugins/catalog' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabCatalog')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dashboard/plugins/repositories' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabRepositories')} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
</Collapse>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/tasks'>
|
||||
<ListItemIcon>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Devices, Analytics } from '@mui/icons-material';
|
||||
import { Devices, Analytics, Input } from '@mui/icons-material';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
@@ -7,7 +7,7 @@ import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React from 'react';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
const DevicesDrawerSection = () => {
|
||||
return (
|
||||
@@ -35,6 +35,14 @@ const DevicesDrawerSection = () => {
|
||||
<ListItemText primary={globalize.translate('HeaderActivity')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/dlna'>
|
||||
<ListItemIcon>
|
||||
<Input />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={'DLNA'} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React from 'react';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
const LiveTvDrawerSection = () => {
|
||||
return (
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
import Extension from '@mui/icons-material/Extension';
|
||||
import Folder from '@mui/icons-material/Folder';
|
||||
import Public from '@mui/icons-material/Public';
|
||||
import { ConfigurationPageInfo } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { getDashboardApi } from '@jellyfin/sdk/lib/utils/api/dashboard-api';
|
||||
import { Folder } from '@mui/icons-material';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'lib/globalize';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import globalize from 'scripts/globalize';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import { useConfigurationPages } from 'apps/dashboard/features/plugins/api/useConfigurationPages';
|
||||
|
||||
const PluginDrawerSection = () => {
|
||||
const {
|
||||
data: pagesInfo,
|
||||
error
|
||||
} = useConfigurationPages({ enableInMainMenu: true });
|
||||
const { api } = useApi();
|
||||
const [ pagesInfo, setPagesInfo ] = useState<ConfigurationPageInfo[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) console.error('[PluginDrawerSection] unable to fetch plugin config pages', error);
|
||||
}, [ error ]);
|
||||
const fetchPluginPages = async () => {
|
||||
if (!api) return;
|
||||
|
||||
const pagesResponse = await getDashboardApi(api)
|
||||
.getConfigurationPages({ enableInMainMenu: true });
|
||||
|
||||
setPagesInfo(pagesResponse.data);
|
||||
};
|
||||
|
||||
fetchPluginPages()
|
||||
.catch(err => {
|
||||
console.error('[PluginDrawerSection] unable to fetch plugin config pages', err);
|
||||
});
|
||||
}, [ api ]);
|
||||
|
||||
if (!api || pagesInfo.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<List
|
||||
@@ -31,39 +46,19 @@ const PluginDrawerSection = () => {
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItemLink
|
||||
to='/dashboard/plugins'
|
||||
includePaths={[ '/configurationpage' ]}
|
||||
excludePaths={pagesInfo?.map(p => `/${Dashboard.getPluginUrl(p.Name)}`)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Extension />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabMyPlugins')} />
|
||||
</ListItemLink>
|
||||
|
||||
<ListItemLink
|
||||
to='/dashboard/plugins/catalog'
|
||||
includePaths={[ '/dashboard/plugins/repositories' ]}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Public />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabCatalog')} />
|
||||
</ListItemLink>
|
||||
|
||||
{pagesInfo?.map(pageInfo => (
|
||||
<ListItemLink
|
||||
key={pageInfo.PluginId}
|
||||
to={`/${Dashboard.getPluginUrl(pageInfo.Name)}`}
|
||||
>
|
||||
<ListItemIcon>
|
||||
{/* TODO: Support different icons? */}
|
||||
<Folder />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={pageInfo.DisplayName} />
|
||||
</ListItemLink>
|
||||
))}
|
||||
{
|
||||
pagesInfo.map(pageInfo => (
|
||||
<ListItem key={pageInfo.PluginId} disablePadding>
|
||||
<ListItemLink to={`/${Dashboard.getPluginUrl(pageInfo.Name)}`}>
|
||||
<ListItemIcon>
|
||||
{/* TODO: Support different icons? */}
|
||||
<Folder />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={pageInfo.DisplayName} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
))
|
||||
}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
const LIBRARY_PATHS = [
|
||||
'/dashboard/libraries',
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { ConfigurationPageInfo } from '@jellyfin/sdk/lib/generated-client/models/configuration-page-info';
|
||||
|
||||
export const findBestConfigurationPage = (
|
||||
configurationPages: ConfigurationPageInfo[],
|
||||
pluginId: string
|
||||
) => {
|
||||
// Find candidates matching the plugin id
|
||||
const candidates = configurationPages.filter(c => c.PluginId === pluginId);
|
||||
|
||||
// If none are found, return undefined
|
||||
if (candidates.length === 0) return;
|
||||
// If only one is found, return it
|
||||
if (candidates.length === 1) return candidates[0];
|
||||
|
||||
// Prefer the first candidate with the EnableInMainMenu flag for consistency
|
||||
const menuCandidate = candidates.find(c => !!c.EnableInMainMenu);
|
||||
if (menuCandidate) return menuCandidate;
|
||||
|
||||
// Fallback to the first match
|
||||
return candidates[0];
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { PluginInfo } from '@jellyfin/sdk/lib/generated-client/models/plugin-info';
|
||||
import { PluginStatus } from '@jellyfin/sdk/lib/generated-client/models/plugin-status';
|
||||
|
||||
/**
|
||||
* HACK: The Plugins API is returning garbage data in some cases,
|
||||
* so we need to try to find the "best" match if multiple exist.
|
||||
*/
|
||||
export const findBestPluginInfo = (
|
||||
pluginId: string,
|
||||
plugins?: PluginInfo[]
|
||||
) => {
|
||||
if (!plugins) return;
|
||||
// Find all plugin entries with a matching ID
|
||||
const matches = plugins.filter(p => p.Id === pluginId);
|
||||
// Get the first match (or undefined if none)
|
||||
const firstMatch = matches?.[0];
|
||||
|
||||
if (matches.length > 1) {
|
||||
return matches.find(p => p.Status === PluginStatus.Disabled) // Disabled entries take priority
|
||||
|| matches.find(p => p.Status === PluginStatus.Restart) // Then entries specifying restart is needed
|
||||
|| firstMatch; // Fallback to the first match
|
||||
}
|
||||
|
||||
return firstMatch;
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
export enum QueryKey {
|
||||
ConfigurationPages = 'ConfigurationPages',
|
||||
PackageInfo = 'PackageInfo',
|
||||
Plugins = 'Plugins'
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { Api } from '@jellyfin/sdk';
|
||||
import type { DashboardApiGetConfigurationPagesRequest } from '@jellyfin/sdk/lib/generated-client/api/dashboard-api';
|
||||
import { getDashboardApi } from '@jellyfin/sdk/lib/utils/api/dashboard-api';
|
||||
import { queryOptions, useQuery } from '@tanstack/react-query';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
import { QueryKey } from './queryKey';
|
||||
|
||||
const fetchConfigurationPages = async (
|
||||
api?: Api,
|
||||
params?: DashboardApiGetConfigurationPagesRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
if (!api) {
|
||||
console.warn('[fetchConfigurationPages] No API instance available');
|
||||
return [];
|
||||
}
|
||||
|
||||
const response = await getDashboardApi(api)
|
||||
.getConfigurationPages(params, options);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getConfigurationPagesQuery = (
|
||||
api?: Api,
|
||||
params?: DashboardApiGetConfigurationPagesRequest
|
||||
) => queryOptions({
|
||||
queryKey: [ QueryKey.ConfigurationPages, params?.enableInMainMenu ],
|
||||
queryFn: ({ signal }) => fetchConfigurationPages(api, params, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
|
||||
export const useConfigurationPages = (
|
||||
params?: DashboardApiGetConfigurationPagesRequest
|
||||
) => {
|
||||
const { api } = useApi();
|
||||
return useQuery(getConfigurationPagesQuery(api, params));
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { PluginsApiDisablePluginRequest } from '@jellyfin/sdk/lib/generated-client/api/plugins-api';
|
||||
import { getPluginsApi } from '@jellyfin/sdk/lib/utils/api/plugins-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
import { QueryKey } from './queryKey';
|
||||
|
||||
export const useDisablePlugin = () => {
|
||||
const { api } = useApi();
|
||||
return useMutation({
|
||||
mutationFn: (params: PluginsApiDisablePluginRequest) => (
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
getPluginsApi(api!)
|
||||
.disablePlugin(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QueryKey.Plugins ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { PluginsApiEnablePluginRequest } from '@jellyfin/sdk/lib/generated-client/api/plugins-api';
|
||||
import { getPluginsApi } from '@jellyfin/sdk/lib/utils/api/plugins-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
import { QueryKey } from './queryKey';
|
||||
|
||||
export const useEnablePlugin = () => {
|
||||
const { api } = useApi();
|
||||
return useMutation({
|
||||
mutationFn: (params: PluginsApiEnablePluginRequest) => (
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
getPluginsApi(api!)
|
||||
.enablePlugin(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QueryKey.Plugins ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { PackageApiInstallPackageRequest } from '@jellyfin/sdk/lib/generated-client/api/package-api';
|
||||
import { getPackageApi } from '@jellyfin/sdk/lib/utils/api/package-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
import { QueryKey } from './queryKey';
|
||||
|
||||
export const useInstallPackage = () => {
|
||||
const { api } = useApi();
|
||||
return useMutation({
|
||||
mutationFn: (params: PackageApiInstallPackageRequest) => (
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
getPackageApi(api!)
|
||||
.installPackage(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QueryKey.ConfigurationPages ]
|
||||
});
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QueryKey.Plugins ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
import { queryOptions, useQuery } from '@tanstack/react-query';
|
||||
import type { Api } from '@jellyfin/sdk';
|
||||
import type { PackageApiGetPackageInfoRequest } from '@jellyfin/sdk/lib/generated-client/api/package-api';
|
||||
import { getPackageApi } from '@jellyfin/sdk/lib/utils/api/package-api';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
import { QueryKey } from './queryKey';
|
||||
|
||||
const fetchPackageInfo = async (
|
||||
api?: Api,
|
||||
params?: PackageApiGetPackageInfoRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
if (!api) {
|
||||
console.warn('[fetchPackageInfo] No API instance available');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!params) {
|
||||
console.warn('[fetchPackageInfo] Missing request params');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await getPackageApi(api)
|
||||
.getPackageInfo(params, options);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getPackageInfoQuery = (
|
||||
api?: Api,
|
||||
params?: PackageApiGetPackageInfoRequest
|
||||
) => queryOptions({
|
||||
// Don't retry since requests for plugins not available in repos fail
|
||||
retry: false,
|
||||
queryKey: [ QueryKey.PackageInfo, params?.name, params?.assemblyGuid ],
|
||||
queryFn: ({ signal }) => fetchPackageInfo(api, params, { signal }),
|
||||
enabled: !!api && !!params?.name
|
||||
});
|
||||
|
||||
export const usePackageInfo = (
|
||||
params?: PackageApiGetPackageInfoRequest
|
||||
) => {
|
||||
const { api } = useApi();
|
||||
return useQuery(getPackageInfoQuery(api, params));
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { Api } from '@jellyfin/sdk';
|
||||
import { getPluginsApi } from '@jellyfin/sdk/lib/utils/api/plugins-api';
|
||||
import { queryOptions, useQuery } from '@tanstack/react-query';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
import { QueryKey } from './queryKey';
|
||||
|
||||
const fetchPlugins = async (
|
||||
api?: Api,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
if (!api) {
|
||||
console.warn('[fetchPlugins] No API instance available');
|
||||
return [];
|
||||
}
|
||||
|
||||
const response = await getPluginsApi(api)
|
||||
.getPlugins(options);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getPluginsQuery = (
|
||||
api?: Api
|
||||
) => queryOptions({
|
||||
queryKey: [ QueryKey.Plugins ],
|
||||
queryFn: ({ signal }) => fetchPlugins(api, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
|
||||
export const usePlugins = () => {
|
||||
const { api } = useApi();
|
||||
return useQuery(getPluginsQuery(api));
|
||||
};
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { PluginsApiUninstallPluginByVersionRequest } from '@jellyfin/sdk/lib/generated-client/api/plugins-api';
|
||||
import { getPluginsApi } from '@jellyfin/sdk/lib/utils/api/plugins-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
import { QueryKey } from './queryKey';
|
||||
|
||||
export const useUninstallPlugin = () => {
|
||||
const { api } = useApi();
|
||||
return useMutation({
|
||||
mutationFn: (params: PluginsApiUninstallPluginByVersionRequest) => (
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
getPluginsApi(api!)
|
||||
.uninstallPluginByVersion(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QueryKey.Plugins ]
|
||||
});
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QueryKey.ConfigurationPages ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
import Link from '@mui/material/Link/Link';
|
||||
import Paper, { type PaperProps } from '@mui/material/Paper/Paper';
|
||||
import Skeleton from '@mui/material/Skeleton/Skeleton';
|
||||
import Table from '@mui/material/Table/Table';
|
||||
import TableBody from '@mui/material/TableBody/TableBody';
|
||||
import TableCell from '@mui/material/TableCell/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer/TableContainer';
|
||||
import TableRow from '@mui/material/TableRow/TableRow';
|
||||
import React, { FC } from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
import type { PluginDetails } from '../types/PluginDetails';
|
||||
|
||||
interface PluginDetailsTableProps extends PaperProps {
|
||||
isPluginLoading: boolean
|
||||
isRepositoryLoading: boolean
|
||||
pluginDetails?: PluginDetails
|
||||
}
|
||||
|
||||
const PluginDetailsTable: FC<PluginDetailsTableProps> = ({
|
||||
isPluginLoading,
|
||||
isRepositoryLoading,
|
||||
pluginDetails,
|
||||
...paperProps
|
||||
}) => (
|
||||
<TableContainer component={Paper} {...paperProps}>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell variant='head'>
|
||||
{globalize.translate('LabelStatus')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{
|
||||
(isPluginLoading && <Skeleton />)
|
||||
|| pluginDetails?.status
|
||||
|| globalize.translate('LabelNotInstalled')
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell variant='head'>
|
||||
{globalize.translate('LabelVersion')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{
|
||||
(isPluginLoading && <Skeleton />)
|
||||
|| pluginDetails?.version?.version
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell variant='head'>
|
||||
{globalize.translate('LabelDeveloper')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{
|
||||
(isRepositoryLoading && <Skeleton />)
|
||||
|| pluginDetails?.owner
|
||||
|| globalize.translate('Unknown')
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<TableCell variant='head'>
|
||||
{globalize.translate('LabelRepository')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{
|
||||
(isRepositoryLoading && <Skeleton />)
|
||||
|| (pluginDetails?.version?.repositoryUrl && (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={pluginDetails.version.repositoryUrl}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
{pluginDetails.version.repositoryName}
|
||||
</Link>
|
||||
))
|
||||
|| globalize.translate('Unknown')
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
export default PluginDetailsTable;
|
||||
@@ -1,34 +0,0 @@
|
||||
import Paper from '@mui/material/Paper/Paper';
|
||||
import Skeleton from '@mui/material/Skeleton/Skeleton';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
interface PluginImageProps {
|
||||
isLoading: boolean
|
||||
alt?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
const PluginImage: FC<PluginImageProps> = ({
|
||||
isLoading,
|
||||
alt,
|
||||
url
|
||||
}) => (
|
||||
<Paper sx={{ width: '100%', aspectRatio: 16 / 9, overflow: 'hidden' }}>
|
||||
{isLoading && (
|
||||
<Skeleton
|
||||
variant='rectangular'
|
||||
width='100%'
|
||||
height='100%'
|
||||
/>
|
||||
)}
|
||||
{url && (
|
||||
<img
|
||||
src={url}
|
||||
alt={alt}
|
||||
width='100%'
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
|
||||
export default PluginImage;
|
||||
@@ -1,67 +0,0 @@
|
||||
import Download from '@mui/icons-material/Download';
|
||||
import DownloadDone from '@mui/icons-material/DownloadDone';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
import Accordion from '@mui/material/Accordion/Accordion';
|
||||
import AccordionDetails from '@mui/material/AccordionDetails/AccordionDetails';
|
||||
import AccordionSummary from '@mui/material/AccordionSummary/AccordionSummary';
|
||||
import Button from '@mui/material/Button/Button';
|
||||
import Stack from '@mui/material/Stack/Stack';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import MarkdownBox from 'components/MarkdownBox';
|
||||
import { parseISO8601Date, toLocaleString } from 'scripts/datetime';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
import type { PluginDetails } from '../types/PluginDetails';
|
||||
import { VersionInfo } from '@jellyfin/sdk/lib/generated-client';
|
||||
|
||||
interface PluginRevisionsProps {
|
||||
pluginDetails?: PluginDetails,
|
||||
onInstall: (version?: VersionInfo) => () => void
|
||||
}
|
||||
|
||||
const PluginRevisions: FC<PluginRevisionsProps> = ({
|
||||
pluginDetails,
|
||||
onInstall
|
||||
}) => (
|
||||
pluginDetails?.versions?.map(version => (
|
||||
<Accordion key={version.checksum}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMore />}
|
||||
>
|
||||
{version.version}
|
||||
{version.timestamp && (<>
|
||||
—
|
||||
{toLocaleString(parseISO8601Date(version.timestamp))}
|
||||
</>)}
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Stack spacing={2}>
|
||||
<MarkdownBox
|
||||
fallback={globalize.translate('LabelNoChangelog')}
|
||||
markdown={version.changelog}
|
||||
/>
|
||||
{pluginDetails.status && version.version === pluginDetails.version?.version ? (
|
||||
<Button
|
||||
disabled
|
||||
startIcon={<DownloadDone />}
|
||||
variant='outlined'
|
||||
>
|
||||
{globalize.translate('LabelInstalled')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
startIcon={<Download />}
|
||||
variant='outlined'
|
||||
onClick={onInstall(version)}
|
||||
>
|
||||
{globalize.translate('HeaderInstall')}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))
|
||||
);
|
||||
|
||||
export default PluginRevisions;
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { ConfigurationPageInfo, PluginStatus, VersionInfo } from '@jellyfin/sdk/lib/generated-client';
|
||||
|
||||
export interface PluginDetails {
|
||||
canUninstall: boolean
|
||||
description?: string
|
||||
id: string
|
||||
imageUrl?: string
|
||||
isEnabled: boolean
|
||||
name?: string
|
||||
owner?: string
|
||||
configurationPage?: ConfigurationPageInfo
|
||||
status?: PluginStatus
|
||||
version?: VersionInfo
|
||||
versions: VersionInfo[]
|
||||
}
|
||||
@@ -2,12 +2,13 @@ import { AsyncRouteType, type AsyncRoute } from 'components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'activity', type: AsyncRouteType.Dashboard },
|
||||
{ path: 'playback/trickplay', type: AsyncRouteType.Dashboard },
|
||||
{ path: 'plugins/:pluginId', page: 'plugins/plugin', type: AsyncRouteType.Dashboard },
|
||||
{ path: 'dlna', type: AsyncRouteType.Dashboard },
|
||||
{ path: 'notifications', type: AsyncRouteType.Dashboard },
|
||||
{ path: 'users', type: AsyncRouteType.Dashboard },
|
||||
{ path: 'users/access', type: AsyncRouteType.Dashboard },
|
||||
{ path: 'users/add', type: AsyncRouteType.Dashboard },
|
||||
{ path: 'users/parentalcontrol', type: AsyncRouteType.Dashboard },
|
||||
{ path: 'users/password', type: AsyncRouteType.Dashboard },
|
||||
{ path: 'users/profile', type: AsyncRouteType.Dashboard }
|
||||
{ path: 'users/profile', type: AsyncRouteType.Dashboard },
|
||||
{ path: 'playback/trickplay', type: AsyncRouteType.Dashboard }
|
||||
];
|
||||
|
||||
@@ -31,6 +31,12 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||
controller: 'dashboard/devices/device',
|
||||
view: 'dashboard/devices/device.html'
|
||||
}
|
||||
}, {
|
||||
path: 'plugins/add',
|
||||
pageProps: {
|
||||
controller: 'dashboard/plugins/add/index',
|
||||
view: 'dashboard/plugins/add/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'libraries',
|
||||
pageProps: {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import type { Redirect } from 'components/router/Redirect';
|
||||
|
||||
export const REDIRECTS: Redirect[] = [
|
||||
{ from: 'addplugin.html', to: '/dashboard/plugins/add' },
|
||||
{ from: 'apikeys.html', to: '/dashboard/keys' },
|
||||
{ from: 'availableplugins.html', to: '/dashboard/plugins/catalog' },
|
||||
{ from: 'dashboard.html', to: '/dashboard' },
|
||||
{ from: 'dashboardgeneral.html', to: '/dashboard/settings' },
|
||||
{ from: 'device.html', to: '/dashboard/devices/edit' },
|
||||
{ from: 'devices.html', to: '/dashboard/devices' },
|
||||
{ from: 'dlnaprofile.html', to: '/dashboard/dlna' },
|
||||
{ from: 'dlnaprofiles.html', to: '/dashboard/dlna' },
|
||||
{ from: 'dlnasettings.html', to: '/dashboard/dlna' },
|
||||
{ from: 'edititemmetadata.html', to: '/metadata' },
|
||||
{ from: 'encodingsettings.html', to: '/dashboard/playback/transcoding' },
|
||||
{ from: 'installedplugins.html', to: '/dashboard/plugins' },
|
||||
@@ -20,6 +24,7 @@ export const REDIRECTS: Redirect[] = [
|
||||
{ from: 'metadataimages.html', to: '/dashboard/libraries/metadata' },
|
||||
{ from: 'metadatanfo.html', to: '/dashboard/libraries/nfo' },
|
||||
{ from: 'networking.html', to: '/dashboard/networking' },
|
||||
{ from: 'notificationsettings.html', to: '/dashboard/notifications' },
|
||||
{ from: 'playbackconfiguration.html', to: '/dashboard/playback/resume' },
|
||||
{ from: 'repositories.html', to: '/dashboard/plugins/repositories' },
|
||||
{ from: 'scheduledtask.html', to: '/dashboard/tasks/edit' },
|
||||
|
||||
@@ -16,7 +16,7 @@ import Page from 'components/Page';
|
||||
import UserAvatar from 'components/UserAvatar';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { parseISO8601Date, toLocaleDateString, toLocaleTimeString } from 'scripts/datetime';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { toBoolean } from 'utils/string';
|
||||
|
||||
import LogLevelChip from '../components/activityTable/LogLevelChip';
|
||||
@@ -60,7 +60,7 @@ const Activity = () => {
|
||||
field: 'User',
|
||||
headerName: globalize.translate('LabelUser'),
|
||||
width: 60,
|
||||
valueGetter: ( value, row ) => users[row.UserId]?.Name,
|
||||
valueGetter: ({ row }) => users[row.UserId]?.Name,
|
||||
renderCell: ({ row }) => (
|
||||
<IconButton
|
||||
size='large'
|
||||
@@ -82,16 +82,16 @@ const Activity = () => {
|
||||
headerName: globalize.translate('LabelDate'),
|
||||
width: 90,
|
||||
type: 'date',
|
||||
valueGetter: ( value ) => parseISO8601Date(value),
|
||||
valueFormatter: ( value ) => toLocaleDateString(value)
|
||||
valueGetter: ({ value }) => parseISO8601Date(value),
|
||||
valueFormatter: ({ value }) => toLocaleDateString(value)
|
||||
},
|
||||
{
|
||||
field: 'Time',
|
||||
headerName: globalize.translate('LabelTime'),
|
||||
width: 100,
|
||||
type: 'dateTime',
|
||||
valueGetter: ( value, row ) => parseISO8601Date(row.Date),
|
||||
valueFormatter: ( value ) => toLocaleTimeString(value)
|
||||
valueGetter: ({ row }) => parseISO8601Date(row.Date),
|
||||
valueFormatter: ({ value }) => toLocaleTimeString(value)
|
||||
},
|
||||
{
|
||||
field: 'Severity',
|
||||
@@ -113,7 +113,7 @@ const Activity = () => {
|
||||
field: 'Overview',
|
||||
headerName: globalize.translate('LabelOverview'),
|
||||
width: 200,
|
||||
valueGetter: ( value, row ) => row.ShortOverview ?? row.Overview,
|
||||
valueGetter: ({ row }) => row.ShortOverview ?? row.Overview,
|
||||
renderCell: ({ row }) => (
|
||||
<OverviewCell {...row} />
|
||||
)
|
||||
@@ -147,7 +147,7 @@ const Activity = () => {
|
||||
}
|
||||
];
|
||||
|
||||
const onViewChange = useCallback((_e: React.MouseEvent<HTMLElement, MouseEvent>, newView: ActivityView | null) => {
|
||||
const onViewChange = useCallback((_e, newView: ActivityView | null) => {
|
||||
if (newView !== null) {
|
||||
setActivityView(newView);
|
||||
}
|
||||
|
||||
33
src/apps/dashboard/routes/dlna.tsx
Normal file
33
src/apps/dashboard/routes/dlna.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import Alert from '@mui/material/Alert/Alert';
|
||||
import Box from '@mui/material/Box/Box';
|
||||
import Button from '@mui/material/Button/Button';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Page from 'components/Page';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
const DlnaPage = () => (
|
||||
<Page
|
||||
id='dlnaSettingsPage'
|
||||
title='DLNA'
|
||||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<div className='content-primary'>
|
||||
<h2>DLNA</h2>
|
||||
<Alert severity='info'>
|
||||
<Box sx={{ marginBottom: 2 }}>
|
||||
{globalize.translate('DlnaMovedMessage')}
|
||||
</Box>
|
||||
<Button
|
||||
component={Link}
|
||||
to='/dashboard/plugins/add?name=DLNA&guid=33eba9cd7da14720967fdd7dae7b74a1'
|
||||
>
|
||||
{globalize.translate('GetThePlugin')}
|
||||
</Button>
|
||||
</Alert>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
|
||||
export default DlnaPage;
|
||||
34
src/apps/dashboard/routes/notifications.tsx
Normal file
34
src/apps/dashboard/routes/notifications.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import Alert from '@mui/material/Alert/Alert';
|
||||
import Box from '@mui/material/Box/Box';
|
||||
import Button from '@mui/material/Button/Button';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Page from 'components/Page';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
const NotificationsPage = () => (
|
||||
<Page
|
||||
id='notificationSettingPage'
|
||||
title={globalize.translate('Notifications')}
|
||||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<div className='content-primary'>
|
||||
<h2>{globalize.translate('Notifications')}</h2>
|
||||
|
||||
<Alert severity='info'>
|
||||
<Box sx={{ marginBottom: 2 }}>
|
||||
{globalize.translate('NotificationsMovedMessage')}
|
||||
</Box>
|
||||
<Button
|
||||
component={Link}
|
||||
to='/dashboard/plugins/add?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173'
|
||||
>
|
||||
{globalize.translate('GetThePlugin')}
|
||||
</Button>
|
||||
</Alert>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
|
||||
export default NotificationsPage;
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { ServerConfiguration } from '@jellyfin/sdk/lib/generated-client/models/server-configuration';
|
||||
import { TrickplayScanBehavior } from '@jellyfin/sdk/lib/generated-client/models/trickplay-scan-behavior';
|
||||
import { ProcessPriorityClass } from '@jellyfin/sdk/lib/generated-client/models/process-priority-class';
|
||||
import React, { type FC, useCallback, useEffect, useRef } from 'react';
|
||||
import type { ProcessPriorityClass, ServerConfiguration, TrickplayScanBehavior } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { type FunctionComponent, useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import globalize from '../../../../lib/globalize';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import Page from '../../../../components/Page';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
@@ -19,10 +17,10 @@ function onSaveComplete() {
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
const PlaybackTrickplay: FC = () => {
|
||||
const PlaybackTrickplay: FunctionComponent = () => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const loadConfig = useCallback((config: ServerConfiguration) => {
|
||||
const loadConfig = useCallback((config) => {
|
||||
const page = element.current;
|
||||
const options = config.TrickplayOptions;
|
||||
|
||||
@@ -31,17 +29,17 @@ const PlaybackTrickplay: FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
(page.querySelector('.chkEnableHwAcceleration') as HTMLInputElement).checked = options?.EnableHwAcceleration || false;
|
||||
(page.querySelector('.chkEnableHwEncoding') as HTMLInputElement).checked = options?.EnableHwEncoding || false;
|
||||
(page.querySelector('#selectScanBehavior') as HTMLSelectElement).value = (options?.ScanBehavior || TrickplayScanBehavior.NonBlocking);
|
||||
(page.querySelector('#selectProcessPriority') as HTMLSelectElement).value = (options?.ProcessPriority || ProcessPriorityClass.Normal);
|
||||
(page.querySelector('#txtInterval') as HTMLInputElement).value = options?.Interval?.toString() || '10000';
|
||||
(page.querySelector('#txtWidthResolutions') as HTMLInputElement).value = options?.WidthResolutions?.join(',') || '';
|
||||
(page.querySelector('#txtTileWidth') as HTMLInputElement).value = options?.TileWidth?.toString() || '10';
|
||||
(page.querySelector('#txtTileHeight') as HTMLInputElement).value = options?.TileHeight?.toString() || '10';
|
||||
(page.querySelector('#txtQscale') as HTMLInputElement).value = options?.Qscale?.toString() || '4';
|
||||
(page.querySelector('#txtJpegQuality') as HTMLInputElement).value = options?.JpegQuality?.toString() || '90';
|
||||
(page.querySelector('#txtProcessThreads') as HTMLInputElement).value = options?.ProcessThreads?.toString() || '1';
|
||||
(page.querySelector('.chkEnableHwAcceleration') as HTMLInputElement).checked = options.EnableHwAcceleration;
|
||||
(page.querySelector('.chkEnableHwEncoding') as HTMLInputElement).checked = options.EnableHwEncoding;
|
||||
(page.querySelector('#selectScanBehavior') as HTMLSelectElement).value = options.ScanBehavior;
|
||||
(page.querySelector('#selectProcessPriority') as HTMLSelectElement).value = options.ProcessPriority;
|
||||
(page.querySelector('#txtInterval') as HTMLInputElement).value = options.Interval;
|
||||
(page.querySelector('#txtWidthResolutions') as HTMLInputElement).value = options.WidthResolutions.join(',');
|
||||
(page.querySelector('#txtTileWidth') as HTMLInputElement).value = options.TileWidth;
|
||||
(page.querySelector('#txtTileHeight') as HTMLInputElement).value = options.TileHeight;
|
||||
(page.querySelector('#txtQscale') as HTMLInputElement).value = options.Qscale;
|
||||
(page.querySelector('#txtJpegQuality') as HTMLInputElement).value = options.JpegQuality;
|
||||
(page.querySelector('#txtProcessThreads') as HTMLInputElement).value = options.ProcessThreads;
|
||||
|
||||
loading.hide();
|
||||
}, []);
|
||||
|
||||
@@ -1,443 +0,0 @@
|
||||
import { PluginStatus } from '@jellyfin/sdk/lib/generated-client/models/plugin-status';
|
||||
import type { VersionInfo } from '@jellyfin/sdk/lib/generated-client/models/version-info';
|
||||
import Alert from '@mui/material/Alert/Alert';
|
||||
import Button from '@mui/material/Button/Button';
|
||||
import Container from '@mui/material/Container/Container';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel/FormControlLabel';
|
||||
import FormGroup from '@mui/material/FormGroup/FormGroup';
|
||||
import Grid from '@mui/material/Grid/Grid';
|
||||
import Skeleton from '@mui/material/Skeleton/Skeleton';
|
||||
import Stack from '@mui/material/Stack/Stack';
|
||||
import Switch from '@mui/material/Switch/Switch';
|
||||
import Typography from '@mui/material/Typography/Typography';
|
||||
import Delete from '@mui/icons-material/Delete';
|
||||
import Download from '@mui/icons-material/Download';
|
||||
import Settings from '@mui/icons-material/Settings';
|
||||
import React, { type FC, useState, useCallback, useMemo } from 'react';
|
||||
import { useSearchParams, Link as RouterLink, useParams } from 'react-router-dom';
|
||||
|
||||
import { findBestConfigurationPage } from 'apps/dashboard/features/plugins/api/configurationPage';
|
||||
import { findBestPluginInfo } from 'apps/dashboard/features/plugins/api/pluginInfo';
|
||||
import { useConfigurationPages } from 'apps/dashboard/features/plugins/api/useConfigurationPages';
|
||||
import { useDisablePlugin } from 'apps/dashboard/features/plugins/api/useDisablePlugin';
|
||||
import { useEnablePlugin } from 'apps/dashboard/features/plugins/api/useEnablePlugin';
|
||||
import { useInstallPackage } from 'apps/dashboard/features/plugins/api/useInstallPackage';
|
||||
import { usePackageInfo } from 'apps/dashboard/features/plugins/api/usePackageInfo';
|
||||
import { usePlugins } from 'apps/dashboard/features/plugins/api/usePlugins';
|
||||
import { useUninstallPlugin } from 'apps/dashboard/features/plugins/api/useUninstallPlugin';
|
||||
import PluginImage from 'apps/dashboard/features/plugins/components/PluginImage';
|
||||
import PluginDetailsTable from 'apps/dashboard/features/plugins/components/PluginDetailsTable';
|
||||
import PluginRevisions from 'apps/dashboard/features/plugins/components/PluginRevisions';
|
||||
import type { PluginDetails } from 'apps/dashboard/features/plugins/types/PluginDetails';
|
||||
|
||||
import ConfirmDialog from 'components/ConfirmDialog';
|
||||
import Page from 'components/Page';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import globalize from 'lib/globalize';
|
||||
import { getPluginUrl } from 'utils/dashboard';
|
||||
import { getUri } from 'utils/api';
|
||||
|
||||
interface AlertMessage {
|
||||
severity?: 'success' | 'info' | 'warning' | 'error'
|
||||
messageKey: string
|
||||
}
|
||||
|
||||
// Plugins from this url will be trusted and not prompt for confirmation when installing
|
||||
const TRUSTED_REPO_URL = 'https://repo.jellyfin.org/';
|
||||
|
||||
const PluginPage: FC = () => {
|
||||
const { api } = useApi();
|
||||
const { pluginId } = useParams();
|
||||
const [ searchParams ] = useSearchParams();
|
||||
const disablePlugin = useDisablePlugin();
|
||||
const enablePlugin = useEnablePlugin();
|
||||
const installPlugin = useInstallPackage();
|
||||
const uninstallPlugin = useUninstallPlugin();
|
||||
|
||||
const [ isEnabledOverride, setIsEnabledOverride ] = useState<boolean>();
|
||||
const [ isInstallConfirmOpen, setIsInstallConfirmOpen ] = useState(false);
|
||||
const [ isUninstallConfirmOpen, setIsUninstallConfirmOpen ] = useState(false);
|
||||
const [ pendingInstallVersion, setPendingInstallVersion ] = useState<VersionInfo>();
|
||||
|
||||
const pluginName = searchParams.get('name') ?? undefined;
|
||||
|
||||
const {
|
||||
data: configurationPages,
|
||||
isError: isConfigurationPagesError,
|
||||
isLoading: isConfigurationPagesLoading
|
||||
} = useConfigurationPages();
|
||||
|
||||
const {
|
||||
data: packageInfo,
|
||||
isError: isPackageInfoError,
|
||||
isLoading: isPackageInfoLoading
|
||||
} = usePackageInfo(pluginName ? {
|
||||
name: pluginName,
|
||||
assemblyGuid: pluginId
|
||||
} : undefined);
|
||||
|
||||
const {
|
||||
data: plugins,
|
||||
isLoading: isPluginsLoading,
|
||||
isError: isPluginsError
|
||||
} = usePlugins();
|
||||
|
||||
const isLoading =
|
||||
isConfigurationPagesLoading || isPackageInfoLoading || isPluginsLoading;
|
||||
|
||||
const pluginDetails = useMemo<PluginDetails | undefined>(() => {
|
||||
if (pluginId && !isPluginsLoading) {
|
||||
const pluginInfo = findBestPluginInfo(pluginId, plugins);
|
||||
|
||||
let version;
|
||||
if (pluginInfo) {
|
||||
// Find the installed version
|
||||
const repoVersion = packageInfo?.versions?.find(v => v.version === pluginInfo.Version);
|
||||
version = repoVersion || {
|
||||
version: pluginInfo.Version,
|
||||
VersionNumber: pluginInfo.Version
|
||||
};
|
||||
} else {
|
||||
// Use the latest version
|
||||
version = packageInfo?.versions?.[0];
|
||||
}
|
||||
|
||||
let imageUrl;
|
||||
if (pluginInfo?.HasImage) {
|
||||
imageUrl = getUri(`/Plugins/${pluginInfo.Id}/${pluginInfo.Version}/Image`, api);
|
||||
}
|
||||
|
||||
return {
|
||||
canUninstall: !!pluginInfo?.CanUninstall,
|
||||
description: pluginInfo?.Description || packageInfo?.description || packageInfo?.overview,
|
||||
id: pluginId,
|
||||
imageUrl: imageUrl || packageInfo?.imageUrl || undefined,
|
||||
isEnabled: (isEnabledOverride && pluginInfo?.Status === PluginStatus.Restart)
|
||||
?? pluginInfo?.Status !== PluginStatus.Disabled,
|
||||
name: pluginName || pluginInfo?.Name || packageInfo?.name,
|
||||
owner: packageInfo?.owner,
|
||||
status: pluginInfo?.Status,
|
||||
configurationPage: findBestConfigurationPage(configurationPages || [], pluginId),
|
||||
version,
|
||||
versions: packageInfo?.versions || []
|
||||
};
|
||||
}
|
||||
}, [
|
||||
api,
|
||||
configurationPages,
|
||||
isEnabledOverride,
|
||||
isPluginsLoading,
|
||||
packageInfo?.description,
|
||||
packageInfo?.imageUrl,
|
||||
packageInfo?.name,
|
||||
packageInfo?.overview,
|
||||
packageInfo?.owner,
|
||||
packageInfo?.versions,
|
||||
pluginId,
|
||||
pluginName,
|
||||
plugins
|
||||
]);
|
||||
|
||||
const alertMessages = useMemo(() => {
|
||||
const alerts: AlertMessage[] = [];
|
||||
|
||||
if (disablePlugin.isError) {
|
||||
alerts.push({ messageKey: 'PluginDisableError' });
|
||||
}
|
||||
|
||||
if (enablePlugin.isError) {
|
||||
alerts.push({ messageKey: 'PluginEnableError' });
|
||||
}
|
||||
|
||||
if (installPlugin.isSuccess) {
|
||||
alerts.push({
|
||||
severity: 'success',
|
||||
messageKey: 'MessagePluginInstalled'
|
||||
});
|
||||
}
|
||||
|
||||
if (installPlugin.isError) {
|
||||
alerts.push({ messageKey: 'MessagePluginInstallError' });
|
||||
}
|
||||
|
||||
if (uninstallPlugin.isError) {
|
||||
alerts.push({ messageKey: 'PluginUninstallError' });
|
||||
}
|
||||
|
||||
if (isConfigurationPagesError) {
|
||||
alerts.push({ messageKey: 'PluginLoadConfigError' });
|
||||
}
|
||||
|
||||
if (isPackageInfoError) {
|
||||
alerts.push({
|
||||
severity: 'warning',
|
||||
messageKey: 'PluginLoadRepoError'
|
||||
});
|
||||
}
|
||||
|
||||
if (isPluginsError) {
|
||||
alerts.push({ messageKey: 'MessageGetInstalledPluginsError' });
|
||||
}
|
||||
|
||||
return alerts;
|
||||
}, [
|
||||
disablePlugin.isError,
|
||||
enablePlugin.isError,
|
||||
installPlugin.isError,
|
||||
installPlugin.isSuccess,
|
||||
isConfigurationPagesError,
|
||||
isPackageInfoError,
|
||||
isPluginsError,
|
||||
uninstallPlugin.isError
|
||||
]);
|
||||
|
||||
/** Enable/disable the plugin */
|
||||
const toggleEnabled = useCallback(() => {
|
||||
if (!pluginDetails?.version?.version) return;
|
||||
|
||||
console.debug('[PluginPage] %s plugin', pluginDetails.isEnabled ? 'disabling' : 'enabling', pluginDetails);
|
||||
|
||||
if (pluginDetails.isEnabled) {
|
||||
disablePlugin.mutate({
|
||||
pluginId: pluginDetails.id,
|
||||
version: pluginDetails.version.version
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
setIsEnabledOverride(false);
|
||||
},
|
||||
onSettled: () => {
|
||||
installPlugin.reset();
|
||||
enablePlugin.reset();
|
||||
uninstallPlugin.reset();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
enablePlugin.mutate({
|
||||
pluginId: pluginDetails.id,
|
||||
version: pluginDetails.version.version
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
setIsEnabledOverride(true);
|
||||
},
|
||||
onSettled: () => {
|
||||
installPlugin.reset();
|
||||
disablePlugin.reset();
|
||||
uninstallPlugin.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [ disablePlugin, enablePlugin, installPlugin, pluginDetails, uninstallPlugin ]);
|
||||
|
||||
/** Install the plugin or prompt for confirmation if untrusted */
|
||||
const onInstall = useCallback((version?: VersionInfo, isConfirmed = false) => () => {
|
||||
if (!pluginDetails?.name) return;
|
||||
const installVersion = version || pluginDetails.version;
|
||||
if (!installVersion) return;
|
||||
|
||||
if (!isConfirmed && !installVersion.repositoryUrl?.startsWith(TRUSTED_REPO_URL)) {
|
||||
console.debug('[PluginPage] plugin install needs confirmed', installVersion);
|
||||
setPendingInstallVersion(installVersion);
|
||||
setIsInstallConfirmOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug('[PluginPage] installing plugin', installVersion);
|
||||
|
||||
installPlugin.mutate({
|
||||
name: pluginDetails.name,
|
||||
assemblyGuid: pluginDetails.id,
|
||||
version: installVersion.version,
|
||||
repositoryUrl: installVersion.repositoryUrl
|
||||
}, {
|
||||
onSettled: () => {
|
||||
setPendingInstallVersion(undefined);
|
||||
disablePlugin.reset();
|
||||
enablePlugin.reset();
|
||||
uninstallPlugin.reset();
|
||||
}
|
||||
});
|
||||
}, [ disablePlugin, enablePlugin, installPlugin, pluginDetails, uninstallPlugin ]);
|
||||
|
||||
/** Confirm and install the plugin */
|
||||
const onConfirmInstall = useCallback(() => {
|
||||
console.debug('[PluginPage] confirmed installing plugin', pendingInstallVersion);
|
||||
setIsInstallConfirmOpen(false);
|
||||
onInstall(pendingInstallVersion, true)();
|
||||
}, [ onInstall, pendingInstallVersion ]);
|
||||
|
||||
/** Close the install confirmation dialog */
|
||||
const onCloseInstallConfirmDialog = useCallback(() => {
|
||||
setPendingInstallVersion(undefined);
|
||||
setIsInstallConfirmOpen(false);
|
||||
}, []);
|
||||
|
||||
/** Show the uninstall confirmation dialog */
|
||||
const onConfirmUninstall = useCallback(() => {
|
||||
setIsUninstallConfirmOpen(true);
|
||||
}, []);
|
||||
|
||||
/** Uninstall the plugin */
|
||||
const onUninstall = useCallback(() => {
|
||||
if (!pluginDetails?.version?.version) return;
|
||||
|
||||
console.debug('[PluginPage] uninstalling plugin', pluginDetails);
|
||||
|
||||
setIsUninstallConfirmOpen(false);
|
||||
|
||||
uninstallPlugin.mutate({
|
||||
pluginId: pluginDetails.id,
|
||||
version: pluginDetails.version.version
|
||||
}, {
|
||||
onSettled: () => {
|
||||
disablePlugin.reset();
|
||||
enablePlugin.reset();
|
||||
installPlugin.reset();
|
||||
}
|
||||
});
|
||||
}, [ disablePlugin, enablePlugin, installPlugin, pluginDetails, uninstallPlugin ]);
|
||||
|
||||
/** Close the uninstall confirmation dialog */
|
||||
const onCloseUninstallConfirmDialog = useCallback(() => {
|
||||
setIsUninstallConfirmOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='addPluginPage'
|
||||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<Container className='content-primary'>
|
||||
|
||||
{alertMessages.map(({ severity = 'error', messageKey }) => (
|
||||
<Alert key={messageKey} severity={severity}>
|
||||
{globalize.translate(messageKey)}
|
||||
</Alert>
|
||||
))}
|
||||
|
||||
<Grid container spacing={2} sx={{ marginTop: 0 }}>
|
||||
<Grid item xs={12} lg={8}>
|
||||
<Stack spacing={2}>
|
||||
<Typography variant='h1'>
|
||||
{pluginDetails?.name || pluginName}
|
||||
</Typography>
|
||||
|
||||
<Typography sx={{ maxWidth: '80ch' }}>
|
||||
{isLoading && !pluginDetails?.description ? (
|
||||
<Skeleton />
|
||||
) : (
|
||||
pluginDetails?.description
|
||||
)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
<Grid item lg={4} sx={{ display: { xs: 'none', lg: 'initial' } }}>
|
||||
<PluginImage
|
||||
isLoading={isLoading}
|
||||
alt={pluginDetails?.name}
|
||||
url={pluginDetails?.imageUrl}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} lg={8} sx={{ order: { xs: 1, lg: 'initial' } }}>
|
||||
{!!pluginDetails?.versions.length && (
|
||||
<>
|
||||
<Typography variant='h3' sx={{ marginBottom: 2 }}>
|
||||
{globalize.translate('HeaderRevisionHistory')}
|
||||
</Typography>
|
||||
<PluginRevisions
|
||||
pluginDetails={pluginDetails}
|
||||
onInstall={onInstall}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} lg={4}>
|
||||
<Stack spacing={2} direction={{ xs: 'column', sm: 'row-reverse', lg: 'column' }}>
|
||||
<Stack spacing={1} sx={{ flexBasis: '50%' }}>
|
||||
{!isLoading && !pluginDetails?.status && (
|
||||
<>
|
||||
<Alert severity='info'>
|
||||
{globalize.translate('ServerRestartNeededAfterPluginInstall')}
|
||||
</Alert>
|
||||
|
||||
<Button
|
||||
startIcon={<Download />}
|
||||
onClick={onInstall()}
|
||||
>
|
||||
{globalize.translate('HeaderInstall')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isLoading && pluginDetails?.canUninstall && (
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={pluginDetails.isEnabled}
|
||||
onChange={toggleEnabled}
|
||||
disabled={pluginDetails.status === PluginStatus.Restart}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('LabelEnablePlugin')}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
{!isLoading && pluginDetails?.configurationPage?.Name && (
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to={`/${getPluginUrl(pluginDetails.configurationPage.Name)}`}
|
||||
startIcon={<Settings />}
|
||||
>
|
||||
{globalize.translate('Settings')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!isLoading && pluginDetails?.canUninstall && (
|
||||
<Button
|
||||
color='error'
|
||||
startIcon={<Delete />}
|
||||
onClick={onConfirmUninstall}
|
||||
>
|
||||
{globalize.translate('ButtonUninstall')}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<PluginDetailsTable
|
||||
isPluginLoading={isPluginsLoading}
|
||||
isRepositoryLoading={isPackageInfoLoading}
|
||||
pluginDetails={pluginDetails}
|
||||
sx={{ flexBasis: '50%' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
|
||||
<ConfirmDialog
|
||||
open={isInstallConfirmOpen}
|
||||
title={globalize.translate('HeaderConfirmPluginInstallation')}
|
||||
text={globalize.translate('MessagePluginInstallDisclaimer')}
|
||||
onCancel={onCloseInstallConfirmDialog}
|
||||
onConfirm={onConfirmInstall}
|
||||
confirmButtonText={globalize.translate('HeaderInstall')}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={isUninstallConfirmOpen}
|
||||
title={globalize.translate('HeaderUninstallPlugin')}
|
||||
text={globalize.translate('UninstallPluginConfirmation', pluginName || '')}
|
||||
onCancel={onCloseUninstallConfirmDialog}
|
||||
onConfirm={onUninstall}
|
||||
confirmButtonColor='error'
|
||||
confirmButtonText={globalize.translate('ButtonUninstall')}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginPage;
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { RouteObject } from 'react-router-dom';
|
||||
import AppLayout from '../AppLayout';
|
||||
import ConnectionRequired from 'components/ConnectionRequired';
|
||||
import { ASYNC_ADMIN_ROUTES } from './_asyncRoutes';
|
||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||
import { LEGACY_ADMIN_ROUTES } from './_legacyRoutes';
|
||||
import ServerContentPage from 'components/ServerContentPage';
|
||||
import ErrorBoundary from 'components/router/ErrorBoundary';
|
||||
|
||||
export const DASHBOARD_APP_PATHS = {
|
||||
Dashboard: 'dashboard',
|
||||
@@ -19,15 +19,14 @@ export const DASHBOARD_APP_ROUTES: RouteObject[] = [
|
||||
element: <ConnectionRequired isAdminRequired />,
|
||||
children: [
|
||||
{
|
||||
lazy: () => import('../AppLayout'),
|
||||
element: <AppLayout drawerlessPaths={[ DASHBOARD_APP_PATHS.MetadataManager ]} />,
|
||||
children: [
|
||||
{
|
||||
path: DASHBOARD_APP_PATHS.Dashboard,
|
||||
children: [
|
||||
...ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute),
|
||||
...LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)
|
||||
],
|
||||
errorElement: <ErrorBoundary pageClasses={[ 'type-interior' ]} />
|
||||
]
|
||||
},
|
||||
|
||||
/* NOTE: The metadata editor might deserve a dedicated app in the future */
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { BaseItemDto, DeviceInfo, UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import libraryMenu from '../../../../scripts/libraryMenu';
|
||||
import globalize from '../../../../lib/globalize';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
@@ -14,13 +14,13 @@ import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type ItemsArr = {
|
||||
Name?: string | null;
|
||||
Id?: string | null;
|
||||
AppName?: string | null;
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
AppName?: string;
|
||||
checkedAttribute?: string
|
||||
};
|
||||
|
||||
const UserLibraryAccess = () => {
|
||||
const UserLibraryAccess: FunctionComponent = () => {
|
||||
const [ searchParams ] = useSearchParams();
|
||||
const userId = searchParams.get('userId');
|
||||
const [ userName, setUserName ] = useState('');
|
||||
@@ -35,7 +35,7 @@ const UserLibraryAccess = () => {
|
||||
select.dispatchEvent(evt);
|
||||
};
|
||||
|
||||
const loadMediaFolders = useCallback((user: UserDto, mediaFolders: BaseItemDto[]) => {
|
||||
const loadMediaFolders = useCallback((user, mediaFolders) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
@@ -46,7 +46,7 @@ const UserLibraryAccess = () => {
|
||||
const itemsArr: ItemsArr[] = [];
|
||||
|
||||
for (const folder of mediaFolders) {
|
||||
const isChecked = user.Policy?.EnableAllFolders || user.Policy?.EnabledFolders?.indexOf(folder.Id || '') != -1;
|
||||
const isChecked = user.Policy.EnableAllFolders || user.Policy.EnabledFolders.indexOf(folder.Id) != -1;
|
||||
const checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
Id: folder.Id,
|
||||
@@ -58,11 +58,11 @@ const UserLibraryAccess = () => {
|
||||
setMediaFoldersItems(itemsArr);
|
||||
|
||||
const chkEnableAllFolders = page.querySelector('.chkEnableAllFolders') as HTMLInputElement;
|
||||
chkEnableAllFolders.checked = Boolean(user.Policy?.EnableAllFolders);
|
||||
chkEnableAllFolders.checked = user.Policy.EnableAllFolders;
|
||||
triggerChange(chkEnableAllFolders);
|
||||
}, []);
|
||||
|
||||
const loadChannels = useCallback((user: UserDto, channels: BaseItemDto[]) => {
|
||||
const loadChannels = useCallback((user, channels) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
@@ -73,7 +73,7 @@ const UserLibraryAccess = () => {
|
||||
const itemsArr: ItemsArr[] = [];
|
||||
|
||||
for (const folder of channels) {
|
||||
const isChecked = user.Policy?.EnableAllChannels || user.Policy?.EnabledChannels?.indexOf(folder.Id || '') != -1;
|
||||
const isChecked = user.Policy.EnableAllChannels || user.Policy.EnabledChannels.indexOf(folder.Id) != -1;
|
||||
const checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
Id: folder.Id,
|
||||
@@ -91,11 +91,11 @@ const UserLibraryAccess = () => {
|
||||
}
|
||||
|
||||
const chkEnableAllChannels = page.querySelector('.chkEnableAllChannels') as HTMLInputElement;
|
||||
chkEnableAllChannels.checked = Boolean(user.Policy?.EnableAllChannels);
|
||||
chkEnableAllChannels.checked = user.Policy.EnableAllChannels;
|
||||
triggerChange(chkEnableAllChannels);
|
||||
}, []);
|
||||
|
||||
const loadDevices = useCallback((user: UserDto, devices: DeviceInfo[]) => {
|
||||
const loadDevices = useCallback((user, devices) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
@@ -106,7 +106,7 @@ const UserLibraryAccess = () => {
|
||||
const itemsArr: ItemsArr[] = [];
|
||||
|
||||
for (const device of devices) {
|
||||
const isChecked = user.Policy?.EnableAllDevices || user.Policy?.EnabledDevices?.indexOf(device.Id || '') != -1;
|
||||
const isChecked = user.Policy.EnableAllDevices || user.Policy.EnabledDevices.indexOf(device.Id) != -1;
|
||||
const checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
Id: device.Id,
|
||||
@@ -119,18 +119,18 @@ const UserLibraryAccess = () => {
|
||||
setDevicesItems(itemsArr);
|
||||
|
||||
const chkEnableAllDevices = page.querySelector('.chkEnableAllDevices') as HTMLInputElement;
|
||||
chkEnableAllDevices.checked = Boolean(user.Policy?.EnableAllDevices);
|
||||
chkEnableAllDevices.checked = user.Policy.EnableAllDevices;
|
||||
triggerChange(chkEnableAllDevices);
|
||||
|
||||
if (user.Policy?.IsAdministrator) {
|
||||
if (user.Policy.IsAdministrator) {
|
||||
(page.querySelector('.deviceAccessContainer') as HTMLDivElement).classList.add('hide');
|
||||
} else {
|
||||
(page.querySelector('.deviceAccessContainer') as HTMLDivElement).classList.remove('hide');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadUser = useCallback((user: UserDto, mediaFolders: BaseItemDto[], channels: BaseItemDto[], devices: DeviceInfo[]) => {
|
||||
setUserName(user.Name || '');
|
||||
const loadUser = useCallback((user, mediaFolders, channels, devices) => {
|
||||
setUserName(user.Name);
|
||||
libraryMenu.setTitle(user.Name);
|
||||
loadChannels(user, channels);
|
||||
loadMediaFolders(user, mediaFolders);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../lib/globalize';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
@@ -18,16 +17,16 @@ type userInput = {
|
||||
};
|
||||
|
||||
type ItemsArr = {
|
||||
Name?: string | null;
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
};
|
||||
|
||||
const UserNew = () => {
|
||||
const UserNew: FunctionComponent = () => {
|
||||
const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]);
|
||||
const [ mediaFoldersItems, setMediaFoldersItems ] = useState<ItemsArr[]>([]);
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const getItemsResult = (items: BaseItemDto[]) => {
|
||||
const getItemsResult = (items: ItemsArr[]) => {
|
||||
return items.map(item =>
|
||||
({
|
||||
Id: item.Id,
|
||||
@@ -36,7 +35,7 @@ const UserNew = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const loadMediaFolders = useCallback((result: BaseItemDto[]) => {
|
||||
const loadMediaFolders = useCallback((result) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
@@ -54,7 +53,7 @@ const UserNew = () => {
|
||||
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked = false;
|
||||
}, []);
|
||||
|
||||
const loadChannels = useCallback((result: BaseItemDto[]) => {
|
||||
const loadChannels = useCallback((result) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import React, { FunctionComponent, useEffect, useState, useRef } from 'react';
|
||||
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../lib/globalize';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import dom from '../../../../scripts/dom';
|
||||
import confirm from '../../../../components/confirm/confirm';
|
||||
@@ -21,7 +21,7 @@ type MenuEntry = {
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
const UserProfiles = () => {
|
||||
const UserProfiles: FunctionComponent = () => {
|
||||
const [ users, setUsers ] = useState<UserDto[]>([]);
|
||||
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { AccessSchedule, ParentalRating, UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { UnratedItem } from '@jellyfin/sdk/lib/generated-client/models/unrated-item';
|
||||
import { DynamicDayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/dynamic-day-of-week';
|
||||
import escapeHTML from 'escape-html';
|
||||
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import globalize from '../../../../lib/globalize';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import LibraryMenu from '../../../../scripts/libraryMenu';
|
||||
import AccessScheduleList from '../../../../components/dashboard/users/AccessScheduleList';
|
||||
import TagList from '../../../../components/dashboard/users/TagList';
|
||||
@@ -20,12 +19,9 @@ import Page from '../../../../components/Page';
|
||||
import prompt from '../../../../components/prompt/prompt';
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
|
||||
type NamedItem = {
|
||||
type UnratedItem = {
|
||||
name: string;
|
||||
value: UnratedItem;
|
||||
};
|
||||
|
||||
type UnratedNamedItem = NamedItem & {
|
||||
value: string;
|
||||
checkedAttribute: string
|
||||
};
|
||||
|
||||
@@ -60,19 +56,19 @@ function handleSaveUser(
|
||||
};
|
||||
}
|
||||
|
||||
const UserParentalControl = () => {
|
||||
const UserParentalControl: FunctionComponent = () => {
|
||||
const [ searchParams ] = useSearchParams();
|
||||
const userId = searchParams.get('userId');
|
||||
const [ userName, setUserName ] = useState('');
|
||||
const [ parentalRatings, setParentalRatings ] = useState<ParentalRating[]>([]);
|
||||
const [ unratedItems, setUnratedItems ] = useState<UnratedNamedItem[]>([]);
|
||||
const [ unratedItems, setUnratedItems ] = useState<UnratedItem[]>([]);
|
||||
const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]);
|
||||
const [ allowedTags, setAllowedTags ] = useState<string[]>([]);
|
||||
const [ blockedTags, setBlockedTags ] = useState<string[]>([]);
|
||||
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const populateRatings = useCallback((allParentalRatings: ParentalRating[]) => {
|
||||
const populateRatings = useCallback((allParentalRatings) => {
|
||||
let rating;
|
||||
const ratings: ParentalRating[] = [];
|
||||
|
||||
@@ -97,7 +93,7 @@ const UserParentalControl = () => {
|
||||
setParentalRatings(ratings);
|
||||
}, []);
|
||||
|
||||
const loadUnratedItems = useCallback((user: UserDto) => {
|
||||
const loadUnratedItems = useCallback((user) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
@@ -105,42 +101,42 @@ const UserParentalControl = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const items: NamedItem[] = [{
|
||||
const items = [{
|
||||
name: globalize.translate('Books'),
|
||||
value: UnratedItem.Book
|
||||
value: 'Book'
|
||||
}, {
|
||||
name: globalize.translate('Channels'),
|
||||
value: UnratedItem.ChannelContent
|
||||
value: 'ChannelContent'
|
||||
}, {
|
||||
name: globalize.translate('LiveTV'),
|
||||
value: UnratedItem.LiveTvChannel
|
||||
value: 'LiveTvChannel'
|
||||
}, {
|
||||
name: globalize.translate('Movies'),
|
||||
value: UnratedItem.Movie
|
||||
value: 'Movie'
|
||||
}, {
|
||||
name: globalize.translate('Music'),
|
||||
value: UnratedItem.Music
|
||||
value: 'Music'
|
||||
}, {
|
||||
name: globalize.translate('Trailers'),
|
||||
value: UnratedItem.Trailer
|
||||
value: 'Trailer'
|
||||
}, {
|
||||
name: globalize.translate('Shows'),
|
||||
value: UnratedItem.Series
|
||||
value: 'Series'
|
||||
}];
|
||||
|
||||
const unratedNamedItem: UnratedNamedItem[] = [];
|
||||
const itemsArr: UnratedItem[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const isChecked = user.Policy?.BlockUnratedItems?.indexOf(item.value) != -1;
|
||||
const isChecked = user.Policy.BlockUnratedItems.indexOf(item.value) != -1;
|
||||
const checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
unratedNamedItem.push({
|
||||
itemsArr.push({
|
||||
value: item.value,
|
||||
name: item.name,
|
||||
checkedAttribute: checkedAttribute
|
||||
});
|
||||
}
|
||||
|
||||
setUnratedItems(unratedNamedItem);
|
||||
setUnratedItems(itemsArr);
|
||||
|
||||
const blockUnratedItems = page.querySelector('.blockUnratedItems') as HTMLDivElement;
|
||||
blockUnratedItems.dispatchEvent(new CustomEvent('create'));
|
||||
@@ -188,7 +184,7 @@ const UserParentalControl = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderAccessSchedule = useCallback((schedules: AccessSchedule[]) => {
|
||||
const renderAccessSchedule = useCallback((schedules) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
@@ -204,7 +200,7 @@ const UserParentalControl = () => {
|
||||
btnDelete.addEventListener('click', function () {
|
||||
const index = parseInt(btnDelete.getAttribute('data-index') ?? '0', 10);
|
||||
schedules.splice(index, 1);
|
||||
const newindex = schedules.filter((_, i) => i != index);
|
||||
const newindex = schedules.filter((i: number) => i != index);
|
||||
renderAccessSchedule(newindex);
|
||||
});
|
||||
}
|
||||
@@ -235,7 +231,7 @@ const UserParentalControl = () => {
|
||||
});
|
||||
}
|
||||
|
||||
(page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value = String(ratingValue);
|
||||
(page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value = ratingValue;
|
||||
|
||||
if (user.Policy?.IsAdministrator) {
|
||||
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
@@ -7,7 +7,7 @@ import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import Page from '../../../../components/Page';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
|
||||
const UserPassword = () => {
|
||||
const UserPassword: FunctionComponent = () => {
|
||||
const [ searchParams ] = useSearchParams();
|
||||
const userId = searchParams.get('userId');
|
||||
const [ userName, setUserName ] = useState('');
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { BaseItemDto, NameIdPair, SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import escapeHTML from 'escape-html';
|
||||
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../lib/globalize';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import LibraryMenu from '../../../../scripts/libraryMenu';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||
@@ -17,10 +17,15 @@ import toast from '../../../../components/toast/toast';
|
||||
import SelectElement from '../../../../elements/SelectElement';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type ResetProvider = BaseItemDto & {
|
||||
type ResetProvider = AuthProvider & {
|
||||
checkedAttribute: string
|
||||
};
|
||||
|
||||
type AuthProvider = {
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
};
|
||||
|
||||
const getCheckedElementDataIds = (elements: NodeListOf<Element>) => (
|
||||
Array.prototype.filter.call(elements, e => e.checked)
|
||||
.map(e => e.getAttribute('data-id'))
|
||||
@@ -35,13 +40,13 @@ function onSaveComplete() {
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
const UserEdit = () => {
|
||||
const UserEdit: FunctionComponent = () => {
|
||||
const [ searchParams ] = useSearchParams();
|
||||
const userId = searchParams.get('userId');
|
||||
const [ userName, setUserName ] = useState('');
|
||||
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ResetProvider[]>([]);
|
||||
const [ authProviders, setAuthProviders ] = useState<NameIdPair[]>([]);
|
||||
const [ passwordResetProviders, setPasswordResetProviders ] = useState<NameIdPair[]>([]);
|
||||
const [ authProviders, setAuthProviders ] = useState<AuthProvider[]>([]);
|
||||
const [ passwordResetProviders, setPasswordResetProviders ] = useState<ResetProvider[]>([]);
|
||||
|
||||
const [ authenticationProviderId, setAuthenticationProviderId ] = useState('');
|
||||
const [ passwordResetProviderId, setPasswordResetProviderId ] = useState('');
|
||||
@@ -58,27 +63,48 @@ const UserEdit = () => {
|
||||
return window.ApiClient.getUser(userId);
|
||||
};
|
||||
|
||||
const loadAuthProviders = useCallback((page: HTMLDivElement, user: UserDto, providers: NameIdPair[]) => {
|
||||
const loadAuthProviders = useCallback((user, providers) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
const fldSelectLoginProvider = page.querySelector('.fldSelectLoginProvider') as HTMLDivElement;
|
||||
fldSelectLoginProvider.classList.toggle('hide', providers.length <= 1);
|
||||
|
||||
setAuthProviders(providers);
|
||||
|
||||
const currentProviderId = user.Policy?.AuthenticationProviderId || '';
|
||||
const currentProviderId = user.Policy.AuthenticationProviderId;
|
||||
setAuthenticationProviderId(currentProviderId);
|
||||
}, []);
|
||||
|
||||
const loadPasswordResetProviders = useCallback((page: HTMLDivElement, user: UserDto, providers: NameIdPair[]) => {
|
||||
const loadPasswordResetProviders = useCallback((user, providers) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
const fldSelectPasswordResetProvider = page.querySelector('.fldSelectPasswordResetProvider') as HTMLDivElement;
|
||||
fldSelectPasswordResetProvider.classList.toggle('hide', providers.length <= 1);
|
||||
|
||||
setPasswordResetProviders(providers);
|
||||
|
||||
const currentProviderId = user.Policy?.PasswordResetProviderId || '';
|
||||
const currentProviderId = user.Policy.PasswordResetProviderId;
|
||||
setPasswordResetProviderId(currentProviderId);
|
||||
}, []);
|
||||
|
||||
const loadDeleteFolders = useCallback((page: HTMLDivElement, user: UserDto, mediaFolders: BaseItemDto[]) => {
|
||||
const loadDeleteFolders = useCallback((user, mediaFolders) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Channels', {
|
||||
SupportsMediaDeletion: true
|
||||
})).then(function (channelsResult) {
|
||||
@@ -86,20 +112,22 @@ const UserEdit = () => {
|
||||
let checkedAttribute;
|
||||
const itemsArr: ResetProvider[] = [];
|
||||
|
||||
for (const mediaFolder of mediaFolders) {
|
||||
isChecked = user.Policy?.EnableContentDeletion || user.Policy?.EnableContentDeletionFromFolders?.indexOf(mediaFolder.Id || '') != -1;
|
||||
for (const folder of mediaFolders) {
|
||||
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
|
||||
checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
...mediaFolder,
|
||||
Id: folder.Id,
|
||||
Name: folder.Name,
|
||||
checkedAttribute: checkedAttribute
|
||||
});
|
||||
}
|
||||
|
||||
for (const channel of channelsResult.Items) {
|
||||
isChecked = user.Policy?.EnableContentDeletion || user.Policy?.EnableContentDeletionFromFolders?.indexOf(channel.Id || '') != -1;
|
||||
for (const folder of channelsResult.Items) {
|
||||
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
|
||||
checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
...channel,
|
||||
Id: folder.Id,
|
||||
Name: folder.Name,
|
||||
checkedAttribute: checkedAttribute
|
||||
});
|
||||
}
|
||||
@@ -107,14 +135,14 @@ const UserEdit = () => {
|
||||
setDeleteFoldersAccess(itemsArr);
|
||||
|
||||
const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement;
|
||||
chkEnableDeleteAllFolders.checked = user.Policy?.EnableContentDeletion || false;
|
||||
chkEnableDeleteAllFolders.checked = user.Policy.EnableContentDeletion;
|
||||
triggerChange(chkEnableDeleteAllFolders);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch channels', err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const loadUser = useCallback((user: UserDto) => {
|
||||
const loadUser = useCallback((user) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
@@ -123,25 +151,25 @@ const UserEdit = () => {
|
||||
}
|
||||
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/Providers')).then(function (providers) {
|
||||
loadAuthProviders(page, user, providers);
|
||||
loadAuthProviders(user, providers);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch auth providers', err);
|
||||
});
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) {
|
||||
loadPasswordResetProviders(page, user, providers);
|
||||
loadPasswordResetProviders(user, providers);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch password reset providers', err);
|
||||
});
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
|
||||
IsHidden: false
|
||||
})).then(function (folders) {
|
||||
loadDeleteFolders(page, user, folders.Items);
|
||||
loadDeleteFolders(user, folders.Items);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch media folders', err);
|
||||
});
|
||||
|
||||
const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement;
|
||||
disabledUserBanner.classList.toggle('hide', !user.Policy?.IsDisabled);
|
||||
disabledUserBanner.classList.toggle('hide', !user.Policy.IsDisabled);
|
||||
|
||||
const txtUserName = page.querySelector('#txtUserName') as HTMLInputElement;
|
||||
txtUserName.disabled = false;
|
||||
@@ -150,30 +178,30 @@ const UserEdit = () => {
|
||||
const lnkEditUserPreferences = page.querySelector('.lnkEditUserPreferences') as HTMLDivElement;
|
||||
lnkEditUserPreferences.setAttribute('href', 'mypreferencesmenu.html?userId=' + user.Id);
|
||||
LibraryMenu.setTitle(user.Name);
|
||||
setUserName(user.Name || '');
|
||||
(page.querySelector('#txtUserName') as HTMLInputElement).value = user.Name || '';
|
||||
(page.querySelector('.chkIsAdmin') as HTMLInputElement).checked = !!user.Policy?.IsAdministrator;
|
||||
(page.querySelector('.chkDisabled') as HTMLInputElement).checked = !!user.Policy?.IsDisabled;
|
||||
(page.querySelector('.chkIsHidden') as HTMLInputElement).checked = !!user.Policy?.IsHidden;
|
||||
(page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked = !!user.Policy?.EnableCollectionManagement;
|
||||
(page.querySelector('.chkEnableSubtitleManagement') as HTMLInputElement).checked = !!user.Policy?.EnableSubtitleManagement;
|
||||
(page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = !!user.Policy?.EnableSharedDeviceControl;
|
||||
(page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = !!user.Policy?.EnableRemoteControlOfOtherUsers;
|
||||
(page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = !!user.Policy?.EnableContentDownloading;
|
||||
(page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked = !!user.Policy?.EnableLiveTvManagement;
|
||||
(page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked = !!user.Policy?.EnableLiveTvAccess;
|
||||
(page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked = !!user.Policy?.EnableMediaPlayback;
|
||||
(page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked = !!user.Policy?.EnableAudioPlaybackTranscoding;
|
||||
(page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked = !!user.Policy?.EnableVideoPlaybackTranscoding;
|
||||
(page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked = !!user.Policy?.EnablePlaybackRemuxing;
|
||||
(page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked = !!user.Policy?.ForceRemoteSourceTranscoding;
|
||||
(page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked = user.Policy?.EnableRemoteAccess == null || user.Policy?.EnableRemoteAccess;
|
||||
(page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value = user.Policy?.RemoteClientBitrateLimit && user.Policy?.RemoteClientBitrateLimit > 0 ?
|
||||
(user.Policy?.RemoteClientBitrateLimit / 1e6).toLocaleString(undefined, { maximumFractionDigits: 6 }) : '';
|
||||
(page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value = String(user.Policy?.MaxActiveSessions) || '0';
|
||||
(page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = String(user.Policy?.SyncPlayAccess) || '0';
|
||||
setUserName(user.Name);
|
||||
(page.querySelector('#txtUserName') as HTMLInputElement).value = user.Name;
|
||||
(page.querySelector('.chkIsAdmin') as HTMLInputElement).checked = user.Policy.IsAdministrator;
|
||||
(page.querySelector('.chkDisabled') as HTMLInputElement).checked = user.Policy.IsDisabled;
|
||||
(page.querySelector('.chkIsHidden') as HTMLInputElement).checked = user.Policy.IsHidden;
|
||||
(page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked = user.Policy.EnableCollectionManagement;
|
||||
(page.querySelector('.chkEnableSubtitleManagement') as HTMLInputElement).checked = user.Policy.EnableSubtitleManagement;
|
||||
(page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = user.Policy.EnableSharedDeviceControl;
|
||||
(page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = user.Policy.EnableRemoteControlOfOtherUsers;
|
||||
(page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = user.Policy.EnableContentDownloading;
|
||||
(page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked = user.Policy.EnableLiveTvManagement;
|
||||
(page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked = user.Policy.EnableLiveTvAccess;
|
||||
(page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked = user.Policy.EnableMediaPlayback;
|
||||
(page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked = user.Policy.EnableAudioPlaybackTranscoding;
|
||||
(page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked = user.Policy.EnableVideoPlaybackTranscoding;
|
||||
(page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked = user.Policy.EnablePlaybackRemuxing;
|
||||
(page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked = user.Policy.ForceRemoteSourceTranscoding;
|
||||
(page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked = user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess;
|
||||
(page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value = user.Policy.RemoteClientBitrateLimit > 0 ?
|
||||
(user.Policy.RemoteClientBitrateLimit / 1e6).toLocaleString(undefined, { maximumFractionDigits: 6 }) : '';
|
||||
(page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value = user.Policy.LoginAttemptsBeforeLockout || '0';
|
||||
(page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = user.Policy.MaxActiveSessions || '0';
|
||||
if (window.ApiClient.isMinServerVersion('10.6.0')) {
|
||||
(page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value = String(user.Policy?.SyncPlayAccess);
|
||||
(page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value = user.Policy.SyncPlayAccess;
|
||||
}
|
||||
loading.hide();
|
||||
}, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]);
|
||||
|
||||
@@ -15,7 +15,7 @@ import AppDrawer, { isDrawerPath } from './components/drawers/AppDrawer';
|
||||
|
||||
import './AppOverrides.scss';
|
||||
|
||||
export const Component = () => {
|
||||
const AppLayout = () => {
|
||||
const [ isDrawerActive, setIsDrawerActive ] = useState(false);
|
||||
const { user } = useApi();
|
||||
const location = useLocation();
|
||||
@@ -29,7 +29,7 @@ export const Component = () => {
|
||||
}, [ isDrawerActive, setIsDrawerActive ]);
|
||||
|
||||
return (
|
||||
<Box sx={{ position: 'relative', display: 'flex' }}>
|
||||
<Box sx={{ position: 'relative', display: 'flex', height: '100%' }}>
|
||||
<ElevationScroll elevate={false}>
|
||||
<AppBar
|
||||
position='fixed'
|
||||
@@ -76,3 +76,5 @@ export const Component = () => {
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
|
||||
@@ -7,6 +7,10 @@ $mui-bp-xl: 1536px;
|
||||
|
||||
$drawer-width: 240px;
|
||||
|
||||
#reactRoot {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// Fix main pages layout to work with drawer
|
||||
.mainAnimatedPage {
|
||||
@media all and (min-width: $mui-bp-md) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useTheme } from '@mui/material/styles';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import Events from 'utils/events';
|
||||
|
||||
import RemotePlayMenu, { ID } from './menus/RemotePlayMenu';
|
||||
@@ -33,7 +33,7 @@ const RemotePlayButton = () => {
|
||||
const [ remotePlayMenuAnchorEl, setRemotePlayMenuAnchorEl ] = useState<null | HTMLElement>(null);
|
||||
const isRemotePlayMenuOpen = Boolean(remotePlayMenuAnchorEl);
|
||||
|
||||
const onRemotePlayButtonClick = useCallback((event: React.MouseEvent<HTMLElement>) => {
|
||||
const onRemotePlayButtonClick = useCallback((event) => {
|
||||
setRemotePlayMenuAnchorEl(event.currentTarget);
|
||||
}, [ setRemotePlayMenuAnchorEl ]);
|
||||
|
||||
@@ -44,7 +44,7 @@ const RemotePlayButton = () => {
|
||||
const [ remotePlayActiveMenuAnchorEl, setRemotePlayActiveMenuAnchorEl ] = useState<null | HTMLElement>(null);
|
||||
const isRemotePlayActiveMenuOpen = Boolean(remotePlayActiveMenuAnchorEl);
|
||||
|
||||
const onRemotePlayActiveButtonClick = useCallback((event: React.MouseEvent<HTMLElement>) => {
|
||||
const onRemotePlayActiveButtonClick = useCallback((event) => {
|
||||
setRemotePlayActiveMenuAnchorEl(event.currentTarget);
|
||||
}, [ setRemotePlayActiveMenuAnchorEl ]);
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import React, { type FC } from 'react';
|
||||
import {
|
||||
Link,
|
||||
URLSearchParamsInit,
|
||||
createSearchParams,
|
||||
useLocation,
|
||||
useSearchParams
|
||||
} from 'react-router-dom';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
const getUrlParams = (searchParams: URLSearchParams) => {
|
||||
const parentId =
|
||||
searchParams.get('parentId') || searchParams.get('topParentId');
|
||||
const collectionType = searchParams.get('collectionType');
|
||||
const params: URLSearchParamsInit = {};
|
||||
|
||||
if (parentId) {
|
||||
params.parentId = parentId;
|
||||
}
|
||||
|
||||
if (collectionType) {
|
||||
params.collectionType = collectionType;
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
interface SearchButtonProps {
|
||||
isTabsAvailable: boolean;
|
||||
}
|
||||
|
||||
const SearchButton: FC<SearchButtonProps> = ({ isTabsAvailable }) => {
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const isSearchPath = location.pathname === '/search.html';
|
||||
const createSearchLink = isTabsAvailable ?
|
||||
{
|
||||
pathname: '/search.html',
|
||||
search: `?${createSearchParams(getUrlParams(searchParams))}`
|
||||
} :
|
||||
'/search.html';
|
||||
|
||||
return (
|
||||
<Tooltip title={globalize.translate('Search')}>
|
||||
<IconButton
|
||||
size='large'
|
||||
aria-label={globalize.translate('Search')}
|
||||
color='inherit'
|
||||
component={Link}
|
||||
disabled={isSearchPath}
|
||||
to={createSearchLink}
|
||||
>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchButton;
|
||||
@@ -6,7 +6,7 @@ import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { pluginManager } from 'components/pluginManager';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { PluginType } from 'types/plugin';
|
||||
|
||||
import AppSyncPlayMenu, { ID } from './menus/SyncPlayMenu';
|
||||
@@ -17,7 +17,7 @@ const SyncPlayButton = () => {
|
||||
const [ syncPlayMenuAnchorEl, setSyncPlayMenuAnchorEl ] = useState<null | HTMLElement>(null);
|
||||
const isSyncPlayMenuOpen = Boolean(syncPlayMenuAnchorEl);
|
||||
|
||||
const onSyncPlayButtonClick = useCallback((event: React.MouseEvent<HTMLElement>) => {
|
||||
const onSyncPlayButtonClick = useCallback((event) => {
|
||||
setSyncPlayMenuAnchorEl(event.currentTarget);
|
||||
}, [ setSyncPlayMenuAnchorEl ]);
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import React, { type FC } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import React, { FC } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
import AppToolbar from 'components/toolbar/AppToolbar';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import AppTabs from '../tabs/AppTabs';
|
||||
import RemotePlayButton from './RemotePlayButton';
|
||||
import SyncPlayButton from './SyncPlayButton';
|
||||
import SearchButton from './SearchButton';
|
||||
import { isTabPath } from '../tabs/tabRoutes';
|
||||
|
||||
interface AppToolbarProps {
|
||||
@@ -40,7 +45,18 @@ const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
|
||||
<>
|
||||
<SyncPlayButton />
|
||||
<RemotePlayButton />
|
||||
<SearchButton isTabsAvailable={isTabsAvailable} />
|
||||
|
||||
<Tooltip title={globalize.translate('Search')}>
|
||||
<IconButton
|
||||
size='large'
|
||||
aria-label={globalize.translate('Search')}
|
||||
color='inherit'
|
||||
component={Link}
|
||||
to='/search.html'
|
||||
>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
isDrawerAvailable={isDrawerAvailable}
|
||||
|
||||
@@ -13,7 +13,7 @@ import React, { FC, useCallback, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { enable, isEnabled } from 'scripts/autocast';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
interface RemotePlayActiveMenuProps extends MenuProps {
|
||||
onMenuClose: () => void
|
||||
|
||||
@@ -6,7 +6,7 @@ import Menu, { type MenuProps } from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import { pluginManager } from 'components/pluginManager';
|
||||
import type { PlayTarget } from 'types/playTarget';
|
||||
|
||||
@@ -20,9 +20,9 @@ import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { pluginManager } from 'components/pluginManager';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { useSyncPlayGroups } from 'hooks/useSyncPlayGroups';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { PluginType } from 'types/plugin';
|
||||
import Events, { Event } from 'utils/events';
|
||||
import Events from 'utils/events';
|
||||
|
||||
export const ID = 'app-sync-play-menu';
|
||||
|
||||
@@ -136,7 +136,7 @@ const SyncPlayMenu: FC<SyncPlayMenuProps> = ({
|
||||
}
|
||||
}, [ __legacyApiClient__, onMenuClose, syncPlay ]);
|
||||
|
||||
const updateSyncPlayGroup = useCallback((_e: Event, enabled: boolean) => {
|
||||
const updateSyncPlayGroup = useCallback((_e, enabled) => {
|
||||
if (syncPlay && enabled) {
|
||||
setCurrentGroup(syncPlay.Manager.getGroupInfo() ?? undefined);
|
||||
} else {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { appRouter } from 'components/router/appRouter';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { useUserViews } from 'hooks/useUserViews';
|
||||
import { useWebConfig } from 'hooks/useWebConfig';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import LibraryIcon from '../LibraryIcon';
|
||||
import DrawerHeaderLink from './DrawerHeaderLink';
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/bas
|
||||
import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||
import React, { FC } from 'react';
|
||||
import { useGetGenres } from 'hooks/useFetchItems';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import GenresSectionContainer from './GenresSectionContainer';
|
||||
import type { ParentId } from 'types/library';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { FC, useCallback } from 'react';
|
||||
import { ButtonGroup, IconButton } from '@mui/material';
|
||||
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
||||
import ViewListIcon from '@mui/icons-material/ViewList';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { LibraryViewSettings, ViewMode } from 'types/library';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
import ViewSettingsButton from './ViewSettingsButton';
|
||||
|
||||
@@ -72,7 +72,7 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||
const {
|
||||
isLoading,
|
||||
data: itemsResult,
|
||||
isPlaceholderData,
|
||||
isPreviousData,
|
||||
refetch
|
||||
} = useGetItemsViewByType(
|
||||
viewType,
|
||||
@@ -228,7 +228,7 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||
<Pagination
|
||||
totalRecordCount={totalRecordCount}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
isPlaceholderData={isPlaceholderData}
|
||||
isPreviousData={isPreviousData}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
)}
|
||||
@@ -312,7 +312,7 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||
<Pagination
|
||||
totalRecordCount={totalRecordCount}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
isPlaceholderData={isPlaceholderData}
|
||||
isPreviousData={isPreviousData}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { IconButton } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
const NewCollectionButton: FC = () => {
|
||||
const showCollectionEditor = useCallback(() => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import Box from '@mui/material/Box';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import * as userSettings from 'scripts/settings/userSettings';
|
||||
import { LibraryViewSettings } from 'types/library';
|
||||
|
||||
@@ -13,14 +13,14 @@ interface PaginationProps {
|
||||
libraryViewSettings: LibraryViewSettings;
|
||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||
totalRecordCount: number;
|
||||
isPlaceholderData: boolean
|
||||
isPreviousData: boolean
|
||||
}
|
||||
|
||||
const Pagination: FC<PaginationProps> = ({
|
||||
libraryViewSettings,
|
||||
setLibraryViewSettings,
|
||||
totalRecordCount,
|
||||
isPlaceholderData
|
||||
isPreviousData
|
||||
}) => {
|
||||
const limit = userSettings.libraryPageSize(undefined);
|
||||
const startIndex = libraryViewSettings.StartIndex ?? 0;
|
||||
@@ -65,7 +65,7 @@ const Pagination: FC<PaginationProps> = ({
|
||||
<IconButton
|
||||
title={globalize.translate('Previous')}
|
||||
className='paper-icon-button-light btnPreviousPage autoSize'
|
||||
disabled={startIndex == 0 || isPlaceholderData}
|
||||
disabled={startIndex == 0 || isPreviousData}
|
||||
onClick={onPreviousPageClick}
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
@@ -74,7 +74,7 @@ const Pagination: FC<PaginationProps> = ({
|
||||
<IconButton
|
||||
title={globalize.translate('Next')}
|
||||
className='paper-icon-button-light btnNextPage autoSize'
|
||||
disabled={startIndex + limit >= totalRecordCount || isPlaceholderData }
|
||||
disabled={startIndex + limit >= totalRecordCount || isPreviousData }
|
||||
onClick={onNextPageClick}
|
||||
>
|
||||
<ArrowForwardIcon />
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IconButton } from '@mui/material';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { getFiltersQuery } from 'utils/items';
|
||||
import { LibraryViewSettings } from 'types/library';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { type FC } from 'react';
|
||||
import { useGetProgramsSectionsWithItems, useGetTimers } from 'hooks/useFetchItems';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import SectionContainer from './SectionContainer';
|
||||
import { CardShape } from 'utils/card';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IconButton } from '@mui/material';
|
||||
import QueueIcon from '@mui/icons-material/Queue';
|
||||
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
interface QueueButtonProps {
|
||||
item: BaseItemDto | undefined
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IconButton } from '@mui/material';
|
||||
import ShuffleIcon from '@mui/icons-material/Shuffle';
|
||||
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { getFiltersQuery } from 'utils/items';
|
||||
import { LibraryViewSettings } from 'types/library';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
|
||||
@@ -10,7 +10,7 @@ import FormControl from '@mui/material/FormControl';
|
||||
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||
import SortByAlphaIcon from '@mui/icons-material/SortByAlpha';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { LibraryViewSettings } from 'types/library';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||
@@ -36,12 +36,6 @@ const movieOrFavoriteOptions = [
|
||||
{ label: 'Runtime', value: ItemSortBy.Runtime }
|
||||
];
|
||||
|
||||
const photosOrPhotoAlbumsOptions = [
|
||||
{ label: 'Name', value: ItemSortBy.SortName },
|
||||
{ label: 'OptionRandom', value: ItemSortBy.Random },
|
||||
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated }
|
||||
];
|
||||
|
||||
const sortOptionsMapping: SortOptionsMapping = {
|
||||
[LibraryTab.Movies]: movieOrFavoriteOptions,
|
||||
[LibraryTab.Trailers]: [
|
||||
@@ -95,16 +89,6 @@ const sortOptionsMapping: SortOptionsMapping = {
|
||||
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate },
|
||||
{ label: 'Runtime', value: ItemSortBy.Runtime },
|
||||
{ label: 'OptionRandom', value: ItemSortBy.Random }
|
||||
],
|
||||
[LibraryTab.PhotoAlbums]: photosOrPhotoAlbumsOptions,
|
||||
[LibraryTab.Photos]: photosOrPhotoAlbumsOptions,
|
||||
[LibraryTab.Videos]: [
|
||||
{ label: 'Name', value: ItemSortBy.SortName },
|
||||
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated },
|
||||
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed },
|
||||
{ label: 'OptionPlayCount', value: ItemSortBy.PlayCount },
|
||||
{ label: 'Runtime', value: ItemSortBy.Runtime },
|
||||
{ label: 'OptionRandom', value: ItemSortBy.Random }
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useGetSuggestionSectionsWithItems
|
||||
} from 'hooks/useFetchItems';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import SectionContainer from './SectionContainer';
|
||||
import { CardShape } from 'utils/card';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { type FC } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useGetGroupsUpcomingEpisodes } from 'hooks/useFetchItems';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import SectionContainer from './SectionContainer';
|
||||
import { CardShape } from 'utils/card';
|
||||
import type { LibraryViewProps } from 'types/library';
|
||||
|
||||
@@ -15,7 +15,7 @@ import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import ViewComfyIcon from '@mui/icons-material/ViewComfy';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { LibraryViewSettings } from 'types/library';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { styled } from '@mui/material/styles';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
import { useGetQueryFiltersLegacy, useGetStudios } from 'hooks/useFetchItems';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import FiltersFeatures from './FiltersFeatures';
|
||||
import FiltersGenres from './FiltersGenres';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { FC, useCallback } from 'react';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { EpisodeFilter, LibraryViewSettings } from 'types/library';
|
||||
|
||||
const episodeFilterOptions = [
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { FC, useCallback } from 'react';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { FeatureFilters, LibraryViewSettings } from 'types/library';
|
||||
|
||||
const featuresOptions = [
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { FC, useCallback } from 'react';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { LibraryViewSettings } from 'types/library';
|
||||
import { SeriesStatus } from '@jellyfin/sdk/lib/generated-client';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { FC, useCallback } from 'react';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { LibraryViewSettings } from 'types/library';
|
||||
import { ItemFilter } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
@@ -56,8 +56,6 @@ const FiltersStatus: FC<FiltersStatusProps> = ({
|
||||
&& viewType !== LibraryTab.AlbumArtists
|
||||
&& viewType !== LibraryTab.Songs
|
||||
&& viewType !== LibraryTab.Channels
|
||||
&& viewType !== LibraryTab.PhotoAlbums
|
||||
&& viewType !== LibraryTab.Photos
|
||||
) {
|
||||
visibleFiltersStatus.push(ItemFilter.IsUnplayed);
|
||||
visibleFiltersStatus.push(ItemFilter.IsPlayed);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import * as userSettings from 'scripts/settings/userSettings';
|
||||
import { LibraryTab } from 'types/libraryTab';
|
||||
|
||||
@@ -184,28 +184,6 @@ const TabRoutes: TabRoute[] = [
|
||||
value: LibraryTab.Episodes
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/homevideos.html',
|
||||
tabs: [
|
||||
{
|
||||
index: 0,
|
||||
label: globalize.translate('Photos'),
|
||||
value: LibraryTab.Photos,
|
||||
isDefault: true
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
label: globalize.translate('HeaderPhotoAlbums'),
|
||||
value: LibraryTab.PhotoAlbums,
|
||||
isDefault: true
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
label: globalize.translate('HeaderVideos'),
|
||||
value: LibraryTab.Videos
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { AsyncRoute, AsyncRouteType } from '../../../../components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_USER_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'home.html', page: 'home', type: AsyncRouteType.Experimental },
|
||||
{ path: 'quickconnect', page: 'quickConnect' },
|
||||
{ path: 'search.html', page: 'search' },
|
||||
{ path: 'userprofile.html', page: 'user/userprofile' },
|
||||
{ path: 'home.html', page: 'home', type: AsyncRouteType.Experimental },
|
||||
{ path: 'movies.html', page: 'movies', type: AsyncRouteType.Experimental },
|
||||
{ path: 'tv.html', page: 'shows', type: AsyncRouteType.Experimental },
|
||||
{ path: 'music.html', page: 'music', type: AsyncRouteType.Experimental },
|
||||
{ path: 'livetv.html', page: 'livetv', type: AsyncRouteType.Experimental },
|
||||
{ path: 'mypreferencesdisplay.html', page: 'user/display', type: AsyncRouteType.Experimental },
|
||||
|
||||
{ path: 'homevideos.html', page: 'homevideos', type: AsyncRouteType.Experimental }
|
||||
{ path: 'mypreferencesdisplay.html', page: 'user/display', type: AsyncRouteType.Experimental }
|
||||
];
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import globalize from '../../../lib/globalize';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import LibraryMenu from '../../../scripts/libraryMenu';
|
||||
import { clearBackdrop } from '../../../components/backdrop/backdrop';
|
||||
import layoutManager from '../../../components/layoutManager';
|
||||
import Page from '../../../components/Page';
|
||||
|
||||
import * as mainTabsManager from '../../../components/maintabsmanager';
|
||||
import '../../../elements/emby-tabs/emby-tabs';
|
||||
import '../../../elements/emby-button/emby-button';
|
||||
import '../../../elements/emby-scroller/emby-scroller';
|
||||
import Page from '../../../components/Page';
|
||||
|
||||
type OnResumeOptions = {
|
||||
autoFocus?: boolean;
|
||||
@@ -24,18 +25,16 @@ type ControllerProps = {
|
||||
destroy: () => void;
|
||||
};
|
||||
|
||||
const Home = () => {
|
||||
const Home: FunctionComponent = () => {
|
||||
const [ searchParams ] = useSearchParams();
|
||||
const initialTabIndex = parseInt(searchParams.get('tab') ?? '0', 10);
|
||||
|
||||
const libraryMenu = useMemo(async () => ((await import('../../../scripts/libraryMenu')).default), []);
|
||||
const mainTabsManager = useMemo(() => import('../../../components/maintabsmanager'), []);
|
||||
const tabController = useRef<ControllerProps | null>();
|
||||
const tabControllers = useMemo<ControllerProps[]>(() => [], []);
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const setTitle = async () => {
|
||||
(await libraryMenu).setTitle(null);
|
||||
const setTitle = () => {
|
||||
LibraryMenu.setTitle(null);
|
||||
};
|
||||
|
||||
const getTabs = () => {
|
||||
@@ -79,6 +78,18 @@ const Home = () => {
|
||||
});
|
||||
}, [ tabControllers ]);
|
||||
|
||||
const onViewDestroy = useCallback(() => {
|
||||
if (tabControllers) {
|
||||
tabControllers.forEach(function (t) {
|
||||
if (t.destroy) {
|
||||
t.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tabController.current = null;
|
||||
}, [ tabControllers ]);
|
||||
|
||||
const loadTab = useCallback((index: number, previousIndex: number | null) => {
|
||||
getTabController(index).then((controller) => {
|
||||
const refresh = !controller.refreshed;
|
||||
@@ -107,23 +118,19 @@ const Home = () => {
|
||||
loadTab(newIndex, previousIndex);
|
||||
}, [ loadTab, tabControllers ]);
|
||||
|
||||
const onSetTabs = useCallback(async () => {
|
||||
(await mainTabsManager).setTabs(element.current, initialTabIndex, getTabs, getTabContainers, null, onTabChange, false);
|
||||
}, [ initialTabIndex, mainTabsManager, onTabChange ]);
|
||||
|
||||
const onResume = useCallback(async () => {
|
||||
void setTitle();
|
||||
const onResume = useCallback(() => {
|
||||
setTitle();
|
||||
clearBackdrop();
|
||||
|
||||
const currentTabController = tabController.current;
|
||||
|
||||
if (!currentTabController) {
|
||||
(await mainTabsManager).selectedTabIndex(initialTabIndex);
|
||||
mainTabsManager.selectedTabIndex(initialTabIndex);
|
||||
} else if (currentTabController?.onResume) {
|
||||
currentTabController.onResume({});
|
||||
}
|
||||
(document.querySelector('.skinHeader') as HTMLDivElement).classList.add('noHomeButtonHeader');
|
||||
}, [ initialTabIndex, mainTabsManager ]);
|
||||
}, [ initialTabIndex ]);
|
||||
|
||||
const onPause = useCallback(() => {
|
||||
const currentTabController = tabController.current;
|
||||
@@ -134,13 +141,13 @@ const Home = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void onSetTabs();
|
||||
mainTabsManager.setTabs(element.current, initialTabIndex, getTabs, getTabContainers, null, onTabChange, false);
|
||||
|
||||
void onResume();
|
||||
onResume();
|
||||
return () => {
|
||||
onPause();
|
||||
};
|
||||
}, [ onPause, onResume, onSetTabs ]);
|
||||
}, [ initialTabIndex, onPause, onResume, onTabChange, onViewDestroy ]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
|
||||
@@ -1,59 +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';
|
||||
|
||||
const photosTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Photos,
|
||||
collectionType: CollectionType.Homevideos,
|
||||
isBtnPlayAllEnabled: true,
|
||||
isBtnShuffleEnabled: true,
|
||||
itemType: [BaseItemKind.Photo]
|
||||
};
|
||||
|
||||
const photoAlbumsTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.PhotoAlbums,
|
||||
collectionType: CollectionType.Homevideos,
|
||||
isBtnPlayAllEnabled: true,
|
||||
isBtnShuffleEnabled: true,
|
||||
itemType: [BaseItemKind.PhotoAlbum]
|
||||
};
|
||||
|
||||
const videosTabContent: LibraryTabContent = {
|
||||
viewType: LibraryTab.Videos,
|
||||
collectionType: CollectionType.Homevideos,
|
||||
isBtnPlayAllEnabled: true,
|
||||
isBtnShuffleEnabled: true,
|
||||
itemType: [BaseItemKind.Video]
|
||||
};
|
||||
|
||||
const homevideosTabMapping: LibraryTabMapping = {
|
||||
0: photosTabContent,
|
||||
1: photoAlbumsTabContent,
|
||||
2: videosTabContent
|
||||
};
|
||||
|
||||
const HomeVideos: FC = () => {
|
||||
const { libraryId, activeTab } = useCurrentTab();
|
||||
const currentTab = homevideosTabMapping[activeTab];
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='homevideos'
|
||||
className='mainAnimatedPage libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs'
|
||||
backDropType='video, photo'
|
||||
>
|
||||
<PageTabContent
|
||||
key={`${currentTab.viewType} - ${libraryId}`}
|
||||
currentTab={currentTab}
|
||||
parentId={libraryId}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeVideos;
|
||||
@@ -4,19 +4,21 @@ import { Navigate, RouteObject } from 'react-router-dom';
|
||||
import { REDIRECTS } from 'apps/dashboard/routes/_redirects';
|
||||
import ConnectionRequired from 'components/ConnectionRequired';
|
||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||
import BangRedirect from 'components/router/BangRedirect';
|
||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||
import { toRedirectRoute } from 'components/router/Redirect';
|
||||
import ErrorBoundary from 'components/router/ErrorBoundary';
|
||||
|
||||
import { ASYNC_USER_ROUTES } from './asyncRoutes';
|
||||
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
|
||||
import VideoPage from './video';
|
||||
import loadable from '@loadable/component';
|
||||
import BangRedirect from 'components/router/BangRedirect';
|
||||
|
||||
const AppLayout = loadable(() => import('../AppLayout'));
|
||||
|
||||
export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [
|
||||
{
|
||||
path: '/*',
|
||||
lazy: () => import('../AppLayout'),
|
||||
element: <AppLayout />,
|
||||
children: [
|
||||
{
|
||||
/* User routes: Any child route of this layout is authenticated */
|
||||
@@ -30,8 +32,7 @@ export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [
|
||||
path: 'video',
|
||||
element: <VideoPage />
|
||||
}
|
||||
],
|
||||
ErrorBoundary
|
||||
]
|
||||
},
|
||||
|
||||
/* Public routes */
|
||||
|
||||
@@ -13,7 +13,7 @@ import React, { Fragment } from 'react';
|
||||
import { appHost } from 'components/apphost';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { useThemes } from 'hooks/useThemes';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import { DisplaySettingsValues } from './types';
|
||||
import { useScreensavers } from './hooks/useScreensavers';
|
||||
|
||||
@@ -6,7 +6,7 @@ import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import React from 'react';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { DisplaySettingsValues } from './types';
|
||||
|
||||
interface ItemDetailPreferencesProps {
|
||||
|
||||
@@ -7,7 +7,7 @@ import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import React from 'react';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { DisplaySettingsValues } from './types';
|
||||
|
||||
interface LibraryPreferencesProps {
|
||||
|
||||
@@ -10,7 +10,7 @@ import React from 'react';
|
||||
|
||||
import { appHost } from 'components/apphost';
|
||||
import datetime from 'scripts/datetime';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { DATE_LOCALE_OPTIONS, LANGUAGE_OPTIONS } from './constants';
|
||||
import { DisplaySettingsValues } from './types';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import React from 'react';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { DisplaySettingsValues } from './types';
|
||||
|
||||
interface NextUpPreferencesProps {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
export const LANGUAGE_OPTIONS = [
|
||||
{ value: 'auto', label: globalize.translate('Auto') },
|
||||
|
||||
@@ -2,15 +2,10 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import toast from 'components/toast/toast';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { DisplaySettingsValues } from '../types';
|
||||
import { useDisplaySettings } from './useDisplaySettings';
|
||||
|
||||
type UpdateField = {
|
||||
name: keyof DisplaySettingsValues;
|
||||
value: string | boolean;
|
||||
};
|
||||
|
||||
export function useDisplaySettingForm() {
|
||||
const [urlParams] = useSearchParams();
|
||||
const {
|
||||
@@ -26,7 +21,7 @@ export function useDisplaySettingForm() {
|
||||
}
|
||||
}, [formValues, loading, displaySettings]);
|
||||
|
||||
const updateField = useCallback(({ name, value }: UpdateField) => {
|
||||
const updateField = useCallback(({ name, value }) => {
|
||||
if (formValues) {
|
||||
setFormValues({
|
||||
...formValues,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
||||
|
||||
import { pluginManager } from 'components/pluginManager';
|
||||
import { Plugin, PluginType } from 'types/plugin';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
export function useScreensavers() {
|
||||
const screensavers = useMemo<Plugin[]>(() => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import Stack from '@mui/material/Stack';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import Page from 'components/Page';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { DisplayPreferences } from './DisplayPreferences';
|
||||
import { ItemDetailPreferences } from './ItemDetailPreferences';
|
||||
import { LibraryPreferences } from './LibraryPreferences';
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { FC, FormEvent, useCallback, useMemo, useState } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import Page from 'components/Page';
|
||||
import globalize from 'lib/globalize';
|
||||
import globalize from 'scripts/globalize';
|
||||
import InputElement from 'elements/InputElement';
|
||||
import ButtonElement from 'elements/ButtonElement';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
@@ -5,7 +5,6 @@ import ConnectionRequired from 'components/ConnectionRequired';
|
||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||
import { toRedirectRoute } from 'components/router/Redirect';
|
||||
import ErrorBoundary from 'components/router/ErrorBoundary';
|
||||
|
||||
import AppLayout from '../AppLayout';
|
||||
|
||||
@@ -17,7 +16,7 @@ import BangRedirect from 'components/router/BangRedirect';
|
||||
export const STABLE_APP_ROUTES: RouteObject[] = [
|
||||
{
|
||||
path: '/*',
|
||||
Component: AppLayout,
|
||||
element: <AppLayout />,
|
||||
children: [
|
||||
{
|
||||
/* User routes */
|
||||
@@ -25,8 +24,7 @@ export const STABLE_APP_ROUTES: RouteObject[] = [
|
||||
children: [
|
||||
...ASYNC_USER_ROUTES.map(toAsyncPageRoute),
|
||||
...LEGACY_USER_ROUTES.map(toViewManagerPageRoute)
|
||||
],
|
||||
ErrorBoundary
|
||||
]
|
||||
},
|
||||
|
||||
/* Public routes */
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import React, { type FC, useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useDebounceValue } from 'usehooks-ts';
|
||||
import { usePrevious } from 'hooks/usePrevious';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
import Page from 'components/Page';
|
||||
import SearchFields from 'components/search/SearchFields';
|
||||
import SearchSuggestions from 'components/search/SearchSuggestions';
|
||||
import SearchResults from 'components/search/SearchResults';
|
||||
import SearchSuggestions from 'components/search/SearchSuggestions';
|
||||
import LiveTVSearchResults from 'components/search/LiveTVSearchResults';
|
||||
import { usePrevious } from 'hooks/usePrevious';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
const COLLECTION_TYPE_PARAM = 'collectionType';
|
||||
const PARENT_ID_PARAM = 'parentId';
|
||||
const QUERY_PARAM = 'query';
|
||||
const SERVER_ID_PARAM = 'serverId';
|
||||
|
||||
const Search: FC = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const parentIdQuery = searchParams.get(PARENT_ID_PARAM) || undefined;
|
||||
const collectionTypeQuery = searchParams.get(COLLECTION_TYPE_PARAM) || undefined;
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
const urlQuery = searchParams.get(QUERY_PARAM) || '';
|
||||
const [query, setQuery] = useState(urlQuery);
|
||||
const [ query, setQuery ] = useState(urlQuery);
|
||||
const prevQuery = usePrevious(query, '');
|
||||
const [debouncedQuery] = useDebounceValue(query, 500);
|
||||
|
||||
useEffect(() => {
|
||||
if (query !== prevQuery) {
|
||||
@@ -50,17 +49,23 @@ const Search: FC = () => {
|
||||
className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage'
|
||||
>
|
||||
<SearchFields query={query} onSearch={setQuery} />
|
||||
{!query ? (
|
||||
<SearchSuggestions
|
||||
parentId={parentIdQuery}
|
||||
{!query
|
||||
&& <SearchSuggestions
|
||||
parentId={searchParams.get(PARENT_ID_PARAM)}
|
||||
/>
|
||||
) : (
|
||||
<SearchResults
|
||||
parentId={parentIdQuery}
|
||||
collectionType={collectionTypeQuery}
|
||||
query={debouncedQuery}
|
||||
/>
|
||||
)}
|
||||
}
|
||||
<SearchResults
|
||||
serverId={searchParams.get(SERVER_ID_PARAM) || window.ApiClient.serverId()}
|
||||
parentId={searchParams.get(PARENT_ID_PARAM)}
|
||||
collectionType={searchParams.get(COLLECTION_TYPE_PARAM)}
|
||||
query={query}
|
||||
/>
|
||||
<LiveTVSearchResults
|
||||
serverId={searchParams.get(SERVER_ID_PARAM) || window.ApiClient.serverId()}
|
||||
parentId={searchParams.get(PARENT_ID_PARAM)}
|
||||
collectionType={searchParams.get(COLLECTION_TYPE_PARAM)}
|
||||
query={query}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { FunctionComponent, useEffect, useState, useRef, useCallback } fr
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../lib/globalize';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import LibraryMenu from '../../../../scripts/libraryMenu';
|
||||
import { appHost } from '../../../../components/apphost';
|
||||
import confirm from '../../../../components/confirm/confirm';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user