Compare commits
189 Commits
renovate/g
...
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 |
2
.github/workflows/automation.yml
vendored
2
.github/workflows/automation.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
steps:
|
||||
- uses: eps1lon/actions-label-merge-conflict@e62d7a53ff8be8b97684bffb6cfbbf3fc1115e2e # v3.0.0
|
||||
- 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.'
|
||||
|
||||
29
.github/workflows/build.yml
vendored
29
.github/workflows/build.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
@@ -46,30 +46,3 @@ jobs:
|
||||
name: jellyfin-web__prod
|
||||
path: |
|
||||
dist
|
||||
|
||||
pr_context:
|
||||
name: Save PR context as artifact
|
||||
if: ${{ always() && !cancelled() && github.event_name == 'pull_request' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- run-build-prod
|
||||
|
||||
steps:
|
||||
- name: Save PR context
|
||||
env:
|
||||
PR_BRANCH: ${{ github.ref_name }}
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
PR_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
echo $PR_BRANCH > PR_branch
|
||||
echo $PR_NUMBER > PR_number
|
||||
echo $PR_SHA > PR_sha
|
||||
|
||||
- name: Upload PR number as artifact
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: PR_context
|
||||
path: |
|
||||
PR_branch
|
||||
PR_number
|
||||
PR_sha
|
||||
|
||||
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
@@ -19,16 +19,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 # v3.25.4
|
||||
uses: github/codeql-action/init@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||
with:
|
||||
languages: javascript
|
||||
queries: +security-extended
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 # v3.25.4
|
||||
uses: github/codeql-action/autobuild@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 # v3.25.4
|
||||
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@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
4
.github/workflows/pr-suggestions.yml
vendored
4
.github/workflows/pr-suggestions.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
@@ -33,6 +33,6 @@ jobs:
|
||||
|
||||
- name: Run eslint
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
uses: CatChen/eslint-suggestion-action@34e2a6c4193eba18a7a20710b5ae37850fc984c3 # v3.1.5
|
||||
uses: CatChen/eslint-suggestion-action@b110ac684564c7b73e47cc223eb7a5266ec83fd3 # v4.1.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
56
.github/workflows/publish.yml
vendored
56
.github/workflows/publish.yml
vendored
@@ -8,42 +8,26 @@ on:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
pr-context:
|
||||
name: PR context
|
||||
if: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
branch: ${{ env.pr_branch }}
|
||||
commit: ${{ env.pr_sha }}
|
||||
pr_number: ${{ env.pr_number }}
|
||||
|
||||
steps:
|
||||
- name: Get PR context
|
||||
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
|
||||
id: pr_context
|
||||
with:
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: PR_context
|
||||
|
||||
- name: Set PR context environment variables
|
||||
if: ${{ steps.pr_context.conclusion == 'success' }}
|
||||
run: |
|
||||
echo "pr_branch=$(cat PR_branch)" >> $GITHUB_ENV
|
||||
echo "pr_number=$(cat PR_number)" >> $GITHUB_ENV
|
||||
echo "pr_sha=$(cat PR_sha)" >> $GITHUB_ENV
|
||||
|
||||
publish:
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
|
||||
name: Deploy to Cloudflare Pages
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pr-context
|
||||
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
|
||||
@@ -60,7 +44,7 @@ jobs:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: jellyfin-web
|
||||
branch: ${{ needs.pr-context.outputs.branch || github.ref_name }}
|
||||
branch: ${{ env.TARGET_BRANCH }}
|
||||
directory: dist
|
||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -70,11 +54,10 @@ jobs:
|
||||
uses: ./.github/workflows/job-messages.yml
|
||||
needs:
|
||||
- publish
|
||||
- pr-context
|
||||
|
||||
with:
|
||||
branch: ${{ needs.pr-context.outputs.branch || github.ref_name }}
|
||||
commit: ${{ needs.pr-context.outputs.commit != '' && needs.pr-context.outputs.commit || github.event.workflow_run.head_sha }}
|
||||
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 }}
|
||||
@@ -85,11 +68,10 @@ jobs:
|
||||
if: |
|
||||
always() &&
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
needs.pr-context.outputs.pr_number != ''
|
||||
github.event.workflow_run.pull_requests[0].number != ''
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- compose-comment
|
||||
- pr-context
|
||||
|
||||
steps:
|
||||
- name: Update job summary in PR comment
|
||||
@@ -97,6 +79,6 @@ jobs:
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
message: ${{ needs.compose-comment.outputs.msg }}
|
||||
pr_number: ${{ needs.pr-context.outputs.pr_number }}
|
||||
pr_number: ${{ github.event.workflow_run.pull_requests[0].number }}
|
||||
comment_tag: ${{ needs.compose-comment.outputs.marker }}
|
||||
mode: recreate
|
||||
|
||||
10
.github/workflows/quality.yml
vendored
10
.github/workflows/quality.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
|
||||
2
.github/workflows/update-sdk.yml
vendored
2
.github/workflows/update-sdk.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
ref: master
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
@@ -87,6 +87,9 @@
|
||||
- [JPUC1143](https://github.com/Jpuc1143)
|
||||
- [David Angel](https://github.com/davidangel)
|
||||
- [Pithaya](https://github.com/Pithaya)
|
||||
- [Chaitanya Shahare](https://github.com/Chaitanya-Shahare)
|
||||
- [Connor Smith](https://github.com/ConnorS1110)
|
||||
- [Venkat Karasani](https://github.com/venkat-karasani)
|
||||
|
||||
## Emby Contributors
|
||||
|
||||
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jellyfin-web",
|
||||
"version": "10.9.0",
|
||||
"version": "10.9.11",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jellyfin-web",
|
||||
"version": "10.9.0",
|
||||
"version": "10.9.11",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.11.4",
|
||||
@@ -18,7 +18,7 @@
|
||||
"@fontsource/noto-sans-sc": "5.0.18",
|
||||
"@fontsource/noto-sans-tc": "5.0.18",
|
||||
"@jellyfin/libass-wasm": "4.2.1",
|
||||
"@jellyfin/sdk": "0.0.0-unstable.202405050501",
|
||||
"@jellyfin/sdk": "0.9.0",
|
||||
"@loadable/component": "5.16.3",
|
||||
"@mui/icons-material": "5.15.11",
|
||||
"@mui/material": "5.15.11",
|
||||
@@ -3728,9 +3728,9 @@
|
||||
"integrity": "sha512-oWK2yz8fFlMXkIuxUc9g/bqN2h56AB+8b6vF/Ikns6WZ/nmcGJ/5lcVaLI4csE83yWgmco4gHO3HyJDsM9EXcQ=="
|
||||
},
|
||||
"node_modules/@jellyfin/sdk": {
|
||||
"version": "0.0.0-unstable.202405050501",
|
||||
"resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.0.0-unstable.202405050501.tgz",
|
||||
"integrity": "sha512-d7TvTH3gGltNH7WrcuJsC+NiTV4HMCxKhzEeW1dGchA6aXRS1aEcnTqsR/ArONQDzlM6ac9Y+y9gfvJYJ6Bgyg==",
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.9.0.tgz",
|
||||
"integrity": "sha512-C8XmAE1LFIAJRYC8F9umlkjWW1lKrcQhCiILme5Da3XYhA8fvu57I1cucuOyFc5NqVPKeaQEOcoJMkuiNMejJw==",
|
||||
"peerDependencies": {
|
||||
"axios": "^1.3.4"
|
||||
}
|
||||
@@ -25587,9 +25587,9 @@
|
||||
"integrity": "sha512-oWK2yz8fFlMXkIuxUc9g/bqN2h56AB+8b6vF/Ikns6WZ/nmcGJ/5lcVaLI4csE83yWgmco4gHO3HyJDsM9EXcQ=="
|
||||
},
|
||||
"@jellyfin/sdk": {
|
||||
"version": "0.0.0-unstable.202405050501",
|
||||
"resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.0.0-unstable.202405050501.tgz",
|
||||
"integrity": "sha512-d7TvTH3gGltNH7WrcuJsC+NiTV4HMCxKhzEeW1dGchA6aXRS1aEcnTqsR/ArONQDzlM6ac9Y+y9gfvJYJ6Bgyg==",
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.9.0.tgz",
|
||||
"integrity": "sha512-C8XmAE1LFIAJRYC8F9umlkjWW1lKrcQhCiILme5Da3XYhA8fvu57I1cucuOyFc5NqVPKeaQEOcoJMkuiNMejJw==",
|
||||
"requires": {}
|
||||
},
|
||||
"@jest/schemas": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jellyfin-web",
|
||||
"version": "10.9.0",
|
||||
"version": "10.9.11",
|
||||
"description": "Web interface for Jellyfin",
|
||||
"repository": "https://github.com/jellyfin/jellyfin-web",
|
||||
"license": "GPL-2.0-or-later",
|
||||
@@ -79,7 +79,7 @@
|
||||
"@fontsource/noto-sans-sc": "5.0.18",
|
||||
"@fontsource/noto-sans-tc": "5.0.18",
|
||||
"@jellyfin/libass-wasm": "4.2.1",
|
||||
"@jellyfin/sdk": "0.0.0-unstable.202405050501",
|
||||
"@jellyfin/sdk": "0.9.0",
|
||||
"@loadable/component": "5.16.3",
|
||||
"@mui/icons-material": "5.15.11",
|
||||
"@mui/material": "5.15.11",
|
||||
|
||||
@@ -1,32 +1,19 @@
|
||||
import loadable from '@loadable/component';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { History } from '@remix-run/router';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import React from 'react';
|
||||
|
||||
import { ApiProvider } from 'hooks/useApi';
|
||||
import { WebConfigProvider } from 'hooks/useWebConfig';
|
||||
import theme from 'themes/theme';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
const StableAppRouter = loadable(() => import('./apps/stable/AppRouter'));
|
||||
const RootAppRouter = loadable(() => import('./RootAppRouter'));
|
||||
|
||||
const RootApp = ({ history }: Readonly<{ history: History }>) => {
|
||||
const layoutMode = localStorage.getItem('layout');
|
||||
const isExperimentalLayout = layoutMode === 'experimental';
|
||||
import RootAppRouter from './RootAppRouter';
|
||||
|
||||
const RootApp = () => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ApiProvider>
|
||||
<WebConfigProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
{isExperimentalLayout ?
|
||||
<RootAppRouter history={history} /> :
|
||||
<StableAppRouter history={history} />
|
||||
}
|
||||
</ThemeProvider>
|
||||
<RootAppRouter />
|
||||
</WebConfigProvider>
|
||||
</ApiProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
|
||||
import { History } from '@remix-run/router';
|
||||
import React from 'react';
|
||||
import {
|
||||
RouterProvider,
|
||||
createHashRouter,
|
||||
Outlet
|
||||
Outlet,
|
||||
useLocation
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { DASHBOARD_APP_PATHS, DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
||||
import { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/routes/routes';
|
||||
import { STABLE_APP_ROUTES } from 'apps/stable/routes/routes';
|
||||
import AppHeader from 'components/AppHeader';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
import { useLegacyRouterSync } from 'hooks/useLegacyRouterSync';
|
||||
import { DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
||||
import { createRouterHistory } from 'components/router/routerHistory';
|
||||
import UserThemeProvider from 'themes/UserThemeProvider';
|
||||
|
||||
const layoutMode = localStorage.getItem('layout');
|
||||
const isExperimentalLayout = layoutMode === 'experimental';
|
||||
|
||||
const router = createHashRouter([
|
||||
{
|
||||
element: <RootAppLayout />,
|
||||
children: [
|
||||
...EXPERIMENTAL_APP_ROUTES,
|
||||
...(isExperimentalLayout ? EXPERIMENTAL_APP_ROUTES : STABLE_APP_ROUTES),
|
||||
...DASHBOARD_APP_ROUTES
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
export default function RootAppRouter({ history }: Readonly<{ history: History}>) {
|
||||
useLegacyRouterSync({ router, history });
|
||||
export const history = createRouterHistory(router);
|
||||
|
||||
export default function RootAppRouter() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
@@ -34,12 +39,16 @@ export default function RootAppRouter({ history }: Readonly<{ history: History}>
|
||||
* NOTE: The app will crash if these get removed from the DOM.
|
||||
*/
|
||||
function RootAppLayout() {
|
||||
const location = useLocation();
|
||||
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
|
||||
.some(path => location.pathname.startsWith(`/${path}`));
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserThemeProvider>
|
||||
<Backdrop />
|
||||
<AppHeader isHidden />
|
||||
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
|
||||
|
||||
<Outlet />
|
||||
</>
|
||||
</UserThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import AppBar from '@mui/material/AppBar';
|
||||
import Box from '@mui/material/Box';
|
||||
import { type Theme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
|
||||
import AppBody from 'components/AppBody';
|
||||
@@ -11,6 +11,7 @@ import ElevationScroll from 'components/ElevationScroll';
|
||||
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
import AppTabs from './components/AppTabs';
|
||||
import AppDrawer from './components/drawer/AppDrawer';
|
||||
|
||||
import './AppOverrides.scss';
|
||||
@@ -35,6 +36,15 @@ const AppLayout: FC<AppLayoutProps> = ({
|
||||
setIsDrawerActive(!isDrawerActive);
|
||||
}, [ isDrawerActive, setIsDrawerActive ]);
|
||||
|
||||
// Update body class
|
||||
useEffect(() => {
|
||||
document.body.classList.add('dashboardDocument');
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove('dashboardDocument');
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<ElevationScroll elevate={false}>
|
||||
@@ -55,7 +65,9 @@ const AppLayout: FC<AppLayoutProps> = ({
|
||||
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
|
||||
isDrawerOpen={isDrawerOpen}
|
||||
onDrawerButtonClick={onToggleDrawer}
|
||||
/>
|
||||
>
|
||||
<AppTabs isDrawerOpen={isDrawerOpen} />
|
||||
</AppToolbar>
|
||||
</AppBar>
|
||||
</ElevationScroll>
|
||||
|
||||
|
||||
@@ -5,10 +5,14 @@ $mui-bp-md: 900px;
|
||||
$mui-bp-lg: 1200px;
|
||||
$mui-bp-xl: 1536px;
|
||||
|
||||
$drawer-width: 240px;
|
||||
|
||||
// Fix dashboard pages layout to work with drawer
|
||||
.dashboardDocument {
|
||||
.mainAnimatedPage {
|
||||
position: relative;
|
||||
@media all and (min-width: $mui-bp-md) {
|
||||
left: $drawer-width;
|
||||
}
|
||||
}
|
||||
|
||||
.skinBody {
|
||||
@@ -16,7 +20,15 @@ $mui-bp-xl: 1536px;
|
||||
}
|
||||
|
||||
// Fix the padding of dashboard pages
|
||||
.content-primary.content-primary {
|
||||
padding-top: 3.25rem !important;
|
||||
.content-primary {
|
||||
padding-top: 3.25rem;
|
||||
}
|
||||
// Tabbed pages
|
||||
.withTabs .content-primary {
|
||||
padding-top: 6.5rem;
|
||||
|
||||
@media all and (min-width: $mui-bp-lg) {
|
||||
padding-top: 3.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
96
src/apps/dashboard/components/AppTabs.tsx
Normal file
96
src/apps/dashboard/components/AppTabs.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { EventType } from 'types/eventType';
|
||||
import Events, { type Event } from 'utils/events';
|
||||
|
||||
interface AppTabsParams {
|
||||
isDrawerOpen: boolean
|
||||
}
|
||||
|
||||
interface TabDefinition {
|
||||
href: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const handleResize = debounce(() => window.dispatchEvent(new Event('resize')), 100);
|
||||
|
||||
const AppTabs: FC<AppTabsParams> = ({
|
||||
isDrawerOpen
|
||||
}) => {
|
||||
const documentRef = useRef<Document>(document);
|
||||
const [ activeIndex, setActiveIndex ] = useState(0);
|
||||
const [ tabs, setTabs ] = useState<TabDefinition[]>();
|
||||
|
||||
const isBigScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
|
||||
|
||||
const onTabsUpdate = useCallback((
|
||||
_e: Event,
|
||||
_newView?: string,
|
||||
newIndex: number | undefined = 0,
|
||||
newTabs?: TabDefinition[]
|
||||
) => {
|
||||
setActiveIndex(newIndex);
|
||||
|
||||
if (!isEqual(tabs, newTabs)) {
|
||||
setTabs(newTabs);
|
||||
}
|
||||
}, [ tabs ]);
|
||||
|
||||
useEffect(() => {
|
||||
const doc = documentRef.current;
|
||||
|
||||
if (doc) Events.on(doc, EventType.SET_TABS, onTabsUpdate);
|
||||
|
||||
return () => {
|
||||
if (doc) Events.off(doc, EventType.SET_TABS, onTabsUpdate);
|
||||
};
|
||||
}, [ onTabsUpdate ]);
|
||||
|
||||
// HACK: Force resizing to workaround upstream bug with tab resizing
|
||||
// https://github.com/mui/material-ui/issues/24011
|
||||
useEffect(() => {
|
||||
handleResize();
|
||||
}, [ isDrawerOpen ]);
|
||||
|
||||
if (!tabs?.length) return null;
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
value={activeIndex}
|
||||
sx={{
|
||||
width: '100%',
|
||||
flexShrink: {
|
||||
xs: 0,
|
||||
lg: 'unset'
|
||||
},
|
||||
order: {
|
||||
xs: 100,
|
||||
lg: 'unset'
|
||||
}
|
||||
}}
|
||||
variant={isBigScreen ? 'standard' : 'scrollable'}
|
||||
centered={isBigScreen}
|
||||
>
|
||||
{
|
||||
tabs.map(({ href, name }, index) => (
|
||||
<Tab
|
||||
key={`tab-${name}`}
|
||||
label={name}
|
||||
data-tab-index={`${index}`}
|
||||
component={Link}
|
||||
to={href}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppTabs;
|
||||
@@ -107,7 +107,7 @@ const Activity = () => {
|
||||
{
|
||||
field: 'Name',
|
||||
headerName: globalize.translate('LabelName'),
|
||||
width: 200
|
||||
width: 300
|
||||
},
|
||||
{
|
||||
field: 'Overview',
|
||||
@@ -121,11 +121,12 @@ const Activity = () => {
|
||||
{
|
||||
field: 'Type',
|
||||
headerName: globalize.translate('LabelType'),
|
||||
width: 120
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
type: 'actions',
|
||||
width: 50,
|
||||
getActions: ({ row }) => {
|
||||
const actions = [];
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ const PlaybackTrickplay: FunctionComponent = () => {
|
||||
<Page
|
||||
id='trickplayConfigurationPage'
|
||||
className='mainAnimatedPage type-interior playbackConfigurationPage'
|
||||
title={globalize.translate('Trickplay')}
|
||||
>
|
||||
<div ref={element} className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
@@ -7,7 +8,6 @@ import globalize from '../../../../scripts/globalize';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import AccessContainer from '../../../../components/dashboard/users/AccessContainer';
|
||||
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||
@@ -21,6 +21,8 @@ type ItemsArr = {
|
||||
};
|
||||
|
||||
const UserLibraryAccess: FunctionComponent = () => {
|
||||
const [ searchParams ] = useSearchParams();
|
||||
const userId = searchParams.get('userId');
|
||||
const [ userName, setUserName ] = useState('');
|
||||
const [channelsItems, setChannelsItems] = useState<ItemsArr[]>([]);
|
||||
const [mediaFoldersItems, setMediaFoldersItems] = useState<ItemsArr[]>([]);
|
||||
@@ -37,7 +39,7 @@ const UserLibraryAccess: FunctionComponent = () => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
console.error('[userlibraryaccess] Unexpected null page reference');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,7 +66,7 @@ const UserLibraryAccess: FunctionComponent = () => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
console.error('[userlibraryaccess] Unexpected null page reference');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -97,7 +99,7 @@ const UserLibraryAccess: FunctionComponent = () => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
console.error('[userlibraryaccess] Unexpected null page reference');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -138,7 +140,6 @@ const UserLibraryAccess: FunctionComponent = () => {
|
||||
|
||||
const loadData = useCallback(() => {
|
||||
loading.show();
|
||||
const userId = getParameterByName('userId');
|
||||
const promise1 = userId ? window.ApiClient.getUser(userId) : Promise.resolve({ Configuration: {} });
|
||||
const promise2 = window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
|
||||
IsHidden: false
|
||||
@@ -150,21 +151,25 @@ const UserLibraryAccess: FunctionComponent = () => {
|
||||
}).catch(err => {
|
||||
console.error('[userlibraryaccess] failed to load data', err);
|
||||
});
|
||||
}, [loadUser]);
|
||||
}, [loadUser, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
console.error('[userlibraryaccess] Unexpected null page reference');
|
||||
return;
|
||||
}
|
||||
|
||||
loadData();
|
||||
|
||||
const onSubmit = (e: Event) => {
|
||||
if (!userId) {
|
||||
console.error('[userlibraryaccess] missing user id');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.show();
|
||||
const userId = getParameterByName('userId');
|
||||
window.ApiClient.getUser(userId).then(function (result) {
|
||||
saveUser(result);
|
||||
}).catch(err => {
|
||||
|
||||
@@ -159,6 +159,7 @@ const UserProfiles: FunctionComponent = () => {
|
||||
<Page
|
||||
id='userProfilesPage'
|
||||
className='mainAnimatedPage type-interior userProfilesPage fullWidthContent'
|
||||
title={globalize.translate('HeaderUsers')}
|
||||
>
|
||||
<div ref={element} className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { AccessSchedule, ParentalRating, UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { DynamicDayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/dynamic-day-of-week';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import escapeHTML from 'escape-html';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import LibraryMenu from '../../../../scripts/libraryMenu';
|
||||
@@ -12,7 +13,6 @@ import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||
import SelectElement from '../../../../elements/SelectElement';
|
||||
import Page from '../../../../components/Page';
|
||||
@@ -57,6 +57,8 @@ function handleSaveUser(
|
||||
}
|
||||
|
||||
const UserParentalControl: FunctionComponent = () => {
|
||||
const [ searchParams ] = useSearchParams();
|
||||
const userId = searchParams.get('userId');
|
||||
const [ userName, setUserName ] = useState('');
|
||||
const [ parentalRatings, setParentalRatings ] = useState<ParentalRating[]>([]);
|
||||
const [ unratedItems, setUnratedItems ] = useState<UnratedItem[]>([]);
|
||||
@@ -95,7 +97,7 @@ const UserParentalControl: FunctionComponent = () => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
console.error('[userparentalcontrol] Unexpected null page reference');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -144,7 +146,7 @@ const UserParentalControl: FunctionComponent = () => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
console.error('[userparentalcontrol] Unexpected null page reference');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -165,7 +167,7 @@ const UserParentalControl: FunctionComponent = () => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
console.error('[userparentalcontrol] Unexpected null page reference');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -186,7 +188,7 @@ const UserParentalControl: FunctionComponent = () => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
console.error('[userparentalcontrol] Unexpected null page reference');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -208,7 +210,7 @@ const UserParentalControl: FunctionComponent = () => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
console.error('[userparentalcontrol] Unexpected null page reference');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -241,8 +243,12 @@ const UserParentalControl: FunctionComponent = () => {
|
||||
}, [loadAllowedTags, loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]);
|
||||
|
||||
const loadData = useCallback(() => {
|
||||
if (!userId) {
|
||||
console.error('[userparentalcontrol.loadData] missing user id');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.show();
|
||||
const userId = getParameterByName('userId');
|
||||
const promise1 = window.ApiClient.getUser(userId);
|
||||
const promise2 = window.ApiClient.getParentalRatings();
|
||||
Promise.all([promise1, promise2]).then(function (responses) {
|
||||
@@ -250,13 +256,13 @@ const UserParentalControl: FunctionComponent = () => {
|
||||
}).catch(err => {
|
||||
console.error('[userparentalcontrol] failed to load data', err);
|
||||
});
|
||||
}, [loadUser]);
|
||||
}, [loadUser, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
console.error('[userparentalcontrol] Unexpected null page reference');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -344,8 +350,12 @@ const UserParentalControl: FunctionComponent = () => {
|
||||
const saveUser = handleSaveUser(page, getSchedulesFromPage, getAllowedTagsFromPage, getBlockedTagsFromPage, onSaveComplete);
|
||||
|
||||
const onSubmit = (e: Event) => {
|
||||
if (!userId) {
|
||||
console.error('[userparentalcontrol.onSubmit] missing user id');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.show();
|
||||
const userId = getParameterByName('userId');
|
||||
window.ApiClient.getUser(userId).then(function (result) {
|
||||
saveUser(result);
|
||||
}).catch(err => {
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import Page from '../../../../components/Page';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
|
||||
const UserPassword: FunctionComponent = () => {
|
||||
const userId = getParameterByName('userId');
|
||||
const [ searchParams ] = useSearchParams();
|
||||
const userId = searchParams.get('userId');
|
||||
const [ userName, setUserName ] = useState('');
|
||||
|
||||
const loadUser = useCallback(() => {
|
||||
if (!userId) {
|
||||
console.error('[userpassword] missing user id');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.show();
|
||||
window.ApiClient.getUser(userId).then(function (user) {
|
||||
if (!user.Name) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import escapeHTML from 'escape-html';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
@@ -13,7 +14,6 @@ import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import SelectElement from '../../../../elements/SelectElement';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
@@ -41,6 +41,8 @@ function onSaveComplete() {
|
||||
}
|
||||
|
||||
const UserEdit: FunctionComponent = () => {
|
||||
const [ searchParams ] = useSearchParams();
|
||||
const userId = searchParams.get('userId');
|
||||
const [ userName, setUserName ] = useState('');
|
||||
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ResetProvider[]>([]);
|
||||
const [ authProviders, setAuthProviders ] = useState<AuthProvider[]>([]);
|
||||
@@ -57,7 +59,7 @@ const UserEdit: FunctionComponent = () => {
|
||||
};
|
||||
|
||||
const getUser = () => {
|
||||
const userId = getParameterByName('userId');
|
||||
if (!userId) throw new Error('missing user id');
|
||||
return window.ApiClient.getUser(userId);
|
||||
};
|
||||
|
||||
@@ -144,7 +146,7 @@ const UserEdit: FunctionComponent = () => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
console.error('[useredit] Unexpected null page reference');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -217,7 +219,7 @@ const UserEdit: FunctionComponent = () => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
console.error('[useredit] Unexpected null page reference');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ const AppLayout = () => {
|
||||
}, [ isDrawerActive, setIsDrawerActive ]);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Box sx={{ position: 'relative', display: 'flex', height: '100%' }}>
|
||||
<ElevationScroll elevate={false}>
|
||||
<AppBar
|
||||
position='fixed'
|
||||
|
||||
@@ -5,9 +5,17 @@ $mui-bp-md: 900px;
|
||||
$mui-bp-lg: 1200px;
|
||||
$mui-bp-xl: 1536px;
|
||||
|
||||
$drawer-width: 240px;
|
||||
|
||||
#reactRoot {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// Fix main pages layout to work with drawer
|
||||
.mainAnimatedPage {
|
||||
position: relative;
|
||||
@media all and (min-width: $mui-bp-md) {
|
||||
left: $drawer-width;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide some items from the user "settings" page that are in the drawer
|
||||
|
||||
@@ -18,17 +18,30 @@ interface AppToolbarProps {
|
||||
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
||||
}
|
||||
|
||||
const PUBLIC_PATHS = [
|
||||
'/addserver.html',
|
||||
'/selectserver.html',
|
||||
'/login.html',
|
||||
'/forgotpassword.html',
|
||||
'/forgotpasswordpin.html'
|
||||
];
|
||||
|
||||
const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
|
||||
isDrawerAvailable,
|
||||
isDrawerOpen,
|
||||
onDrawerButtonClick
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
|
||||
// The video osd does not show the standard toolbar
|
||||
if (location.pathname === '/video') return null;
|
||||
|
||||
const isTabsAvailable = isTabPath(location.pathname);
|
||||
const isPublicPath = PUBLIC_PATHS.includes(location.pathname);
|
||||
|
||||
return (
|
||||
<AppToolbar
|
||||
buttons={
|
||||
buttons={!isPublicPath && (
|
||||
<>
|
||||
<SyncPlayButton />
|
||||
<RemotePlayButton />
|
||||
@@ -45,10 +58,11 @@ const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
isDrawerAvailable={isDrawerAvailable}
|
||||
isDrawerOpen={isDrawerOpen}
|
||||
onDrawerButtonClick={onDrawerButtonClick}
|
||||
isUserMenuAvailable={!isPublicPath}
|
||||
>
|
||||
{isTabsAvailable && (<AppTabs isDrawerOpen={isDrawerOpen} />)}
|
||||
</AppToolbar>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { playbackManager } from 'components/playback/playbackmanager';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { enable, isEnabled, supported } from 'scripts/autocast';
|
||||
import { enable, isEnabled } from 'scripts/autocast';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
interface RemotePlayActiveMenuProps extends MenuProps {
|
||||
@@ -43,11 +43,10 @@ const RemotePlayActiveMenu: FC<RemotePlayActiveMenuProps> = ({
|
||||
}, [ isDisplayMirrorEnabled, setIsDisplayMirrorEnabled ]);
|
||||
|
||||
const [ isAutoCastEnabled, setIsAutoCastEnabled ] = useState(isEnabled());
|
||||
const isAutoCastSupported = supported();
|
||||
const toggleAutoCast = useCallback(() => {
|
||||
enable(!isAutoCastEnabled);
|
||||
setIsAutoCastEnabled(!isAutoCastEnabled);
|
||||
}, [ isAutoCastEnabled, setIsAutoCastEnabled ]);
|
||||
}, [ isAutoCastEnabled ]);
|
||||
|
||||
const remotePlayerName = playerInfo?.deviceName || playerInfo?.name;
|
||||
|
||||
@@ -117,20 +116,18 @@ const RemotePlayActiveMenu: FC<RemotePlayActiveMenuProps> = ({
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{isAutoCastSupported && (
|
||||
<MenuItem onClick={toggleAutoCast}>
|
||||
{isAutoCastEnabled && (
|
||||
<ListItemIcon>
|
||||
<Check />
|
||||
</ListItemIcon>
|
||||
)}
|
||||
<ListItemText inset={!isAutoCastEnabled}>
|
||||
{globalize.translate('EnableAutoCast')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={toggleAutoCast}>
|
||||
{isAutoCastEnabled && (
|
||||
<ListItemIcon>
|
||||
<Check />
|
||||
</ListItemIcon>
|
||||
)}
|
||||
<ListItemText inset={!isAutoCastEnabled}>
|
||||
{globalize.translate('EnableAutoCast')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
{(isDisplayMirrorSupported || isAutoCastSupported) && <Divider />}
|
||||
<Divider />
|
||||
|
||||
<MenuItem
|
||||
component={Link}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Guide from 'components/guide/guide';
|
||||
import 'material-design-icons-iconfont';
|
||||
import 'elements/emby-programcell/emby-programcell';
|
||||
@@ -45,7 +46,20 @@ const GuideView: FC = () => {
|
||||
};
|
||||
}, [initGuide]);
|
||||
|
||||
return <div ref={tvGuideContainerRef} />;
|
||||
return <Box
|
||||
ref={tvGuideContainerRef}
|
||||
className='absolutePageTabContent'
|
||||
sx={{
|
||||
display: 'flex !important',
|
||||
width: 'auto',
|
||||
paddingTop: '0',
|
||||
paddingBottom: '0 !important',
|
||||
top: {
|
||||
xs: '6.9em !important',
|
||||
lg: '4em !important'
|
||||
}
|
||||
}}
|
||||
/>;
|
||||
};
|
||||
|
||||
export default GuideView;
|
||||
|
||||
@@ -26,7 +26,7 @@ type SortOptionsMapping = Record<string, SortOption[]>;
|
||||
const movieOrFavoriteOptions = [
|
||||
{ label: 'Name', value: ItemSortBy.SortName },
|
||||
{ label: 'OptionRandom', value: ItemSortBy.Random },
|
||||
{ label: 'OptionImdbRating', value: ItemSortBy.CommunityRating },
|
||||
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating },
|
||||
{ label: 'OptionCriticRating', value: ItemSortBy.CriticRating },
|
||||
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated },
|
||||
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed },
|
||||
@@ -40,7 +40,7 @@ const sortOptionsMapping: SortOptionsMapping = {
|
||||
[LibraryTab.Movies]: movieOrFavoriteOptions,
|
||||
[LibraryTab.Trailers]: [
|
||||
{ label: 'Name', value: ItemSortBy.SortName },
|
||||
{ label: 'OptionImdbRating', value: ItemSortBy.CommunityRating },
|
||||
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating },
|
||||
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated },
|
||||
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed },
|
||||
{ label: 'OptionParentalRating', value: ItemSortBy.OfficialRating },
|
||||
@@ -51,7 +51,7 @@ const sortOptionsMapping: SortOptionsMapping = {
|
||||
[LibraryTab.Series]: [
|
||||
{ label: 'Name', value: ItemSortBy.SortName },
|
||||
{ label: 'OptionRandom', value: ItemSortBy.Random },
|
||||
{ label: 'OptionImdbRating', value: ItemSortBy.CommunityRating },
|
||||
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating },
|
||||
{ label: 'OptionDateShowAdded', value: ItemSortBy.DateCreated },
|
||||
{ label: 'OptionDateEpisodeAdded', value: ItemSortBy.DateLastContentAdded },
|
||||
{ label: 'OptionDatePlayed', value: ItemSortBy.SeriesDatePlayed },
|
||||
@@ -60,7 +60,7 @@ const sortOptionsMapping: SortOptionsMapping = {
|
||||
],
|
||||
[LibraryTab.Episodes]: [
|
||||
{ label: 'Name', value: ItemSortBy.SeriesSortName },
|
||||
{ label: 'OptionImdbRating', value: ItemSortBy.CommunityRating },
|
||||
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating },
|
||||
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated },
|
||||
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate },
|
||||
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed },
|
||||
@@ -73,7 +73,7 @@ const sortOptionsMapping: SortOptionsMapping = {
|
||||
{ label: 'Name', value: ItemSortBy.SortName },
|
||||
{ label: 'OptionRandom', value: ItemSortBy.Random },
|
||||
{ label: 'AlbumArtist', value: ItemSortBy.AlbumArtist },
|
||||
{ label: 'OptionImdbRating', value: ItemSortBy.CommunityRating },
|
||||
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating },
|
||||
{ label: 'OptionCriticRating', value: ItemSortBy.CriticRating },
|
||||
{ label: 'OptionReleaseDate', value: ItemSortBy.ProductionYear },
|
||||
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated }
|
||||
|
||||
@@ -49,16 +49,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
|
||||
controller: 'user/subtitles/index',
|
||||
view: 'user/subtitles/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'video',
|
||||
pageProps: {
|
||||
controller: 'playback/video/index',
|
||||
view: 'playback/video/index.html',
|
||||
type: 'video-osd',
|
||||
isFullscreen: true,
|
||||
isNowPlayingBarEnabled: false,
|
||||
isThemeMediaSupported: true
|
||||
}
|
||||
}, {
|
||||
path: 'queue',
|
||||
pageProps: {
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import React from 'react';
|
||||
import { RouteObject, redirect } from 'react-router-dom';
|
||||
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 { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||
import { toRedirectRoute } from 'components/router/Redirect';
|
||||
import AppLayout from '../AppLayout';
|
||||
|
||||
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[] = [
|
||||
{
|
||||
@@ -20,16 +25,27 @@ export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [
|
||||
element: <ConnectionRequired isUserRequired />,
|
||||
children: [
|
||||
...ASYNC_USER_ROUTES.map(toAsyncPageRoute),
|
||||
...LEGACY_USER_ROUTES.map(toViewManagerPageRoute)
|
||||
...LEGACY_USER_ROUTES.map(toViewManagerPageRoute),
|
||||
|
||||
// The video page is special since it combines new controls with the legacy view
|
||||
{
|
||||
path: 'video',
|
||||
element: <VideoPage />
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
/* Public routes */
|
||||
{ index: true, loader: () => redirect('/home.html') },
|
||||
{ index: true, element: <Navigate replace to='/home.html' /> },
|
||||
...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
path: '!/*',
|
||||
Component: BangRedirect
|
||||
},
|
||||
|
||||
/* Redirects for old paths */
|
||||
...REDIRECTS.map(toRedirectRoute)
|
||||
];
|
||||
|
||||
@@ -12,10 +12,11 @@ import React, { Fragment } from 'react';
|
||||
|
||||
import { appHost } from 'components/apphost';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { useThemes } from 'hooks/useThemes';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import { DisplaySettingsValues } from './types';
|
||||
import { useScreensavers } from './hooks/useScreensavers';
|
||||
import { useServerThemes } from './hooks/useServerThemes';
|
||||
|
||||
interface DisplayPreferencesProps {
|
||||
onChange: (event: SelectChangeEvent | React.SyntheticEvent) => void;
|
||||
@@ -25,7 +26,7 @@ interface DisplayPreferencesProps {
|
||||
export function DisplayPreferences({ onChange, values }: Readonly<DisplayPreferencesProps>) {
|
||||
const { user } = useApi();
|
||||
const { screensavers } = useScreensavers();
|
||||
const { themes } = useServerThemes();
|
||||
const { themes } = useThemes();
|
||||
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import themeManager from 'scripts/themeManager';
|
||||
import { Theme } from 'types/webConfig';
|
||||
|
||||
export function useServerThemes() {
|
||||
const [themes, setThemes] = useState<Theme[]>();
|
||||
|
||||
useEffect(() => {
|
||||
async function getServerThemes() {
|
||||
const loadedThemes = await themeManager.getThemes();
|
||||
|
||||
setThemes(loadedThemes ?? []);
|
||||
}
|
||||
|
||||
if (!themes) {
|
||||
void getServerThemes();
|
||||
}
|
||||
// We've intentionally left the dependency array here to ensure that the effect happens only once.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const defaultTheme = useMemo(() => {
|
||||
if (!themes) return null;
|
||||
return themes.find((theme) => theme.default);
|
||||
}, [themes]);
|
||||
|
||||
return {
|
||||
themes: themes ?? [],
|
||||
defaultTheme
|
||||
};
|
||||
}
|
||||
72
src/apps/experimental/routes/video/index.tsx
Normal file
72
src/apps/experimental/routes/video/index.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import Box from '@mui/material/Box/Box';
|
||||
import Fade from '@mui/material/Fade/Fade';
|
||||
import React, { useRef, type FC, useEffect, useState } from 'react';
|
||||
|
||||
import RemotePlayButton from 'apps/experimental/components/AppToolbar/RemotePlayButton';
|
||||
import SyncPlayButton from 'apps/experimental/components/AppToolbar/SyncPlayButton';
|
||||
import AppToolbar from 'components/toolbar/AppToolbar';
|
||||
import ViewManagerPage from 'components/viewManager/ViewManagerPage';
|
||||
import { EventType } from 'types/eventType';
|
||||
import Events, { type Event } from 'utils/events';
|
||||
|
||||
/**
|
||||
* Video player page component that renders mui controls for the top controls and the legacy view for everything else.
|
||||
*/
|
||||
const VideoPage: FC = () => {
|
||||
const documentRef = useRef<Document>(document);
|
||||
const [ isVisible, setIsVisible ] = useState(true);
|
||||
|
||||
const onShowVideoOsd = (_e: Event, isShowing: boolean) => {
|
||||
setIsVisible(isShowing);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const doc = documentRef.current;
|
||||
|
||||
if (doc) Events.on(doc, EventType.SHOW_VIDEO_OSD, onShowVideoOsd);
|
||||
|
||||
return () => {
|
||||
if (doc) Events.off(doc, EventType.SHOW_VIDEO_OSD, onShowVideoOsd);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Fade
|
||||
in={isVisible}
|
||||
easing='fade-out'
|
||||
>
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
color: 'white'
|
||||
}}>
|
||||
<AppToolbar
|
||||
isDrawerAvailable={false}
|
||||
isDrawerOpen={false}
|
||||
isUserMenuAvailable={false}
|
||||
buttons={
|
||||
<>
|
||||
<SyncPlayButton />
|
||||
<RemotePlayButton />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Fade>
|
||||
|
||||
<ViewManagerPage
|
||||
controller='playback/video/index'
|
||||
view='playback/video/index.html'
|
||||
type='video-osd'
|
||||
isFullscreen
|
||||
isNowPlayingBarEnabled={false}
|
||||
isThemeMediaSupported
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoPage;
|
||||
@@ -1,42 +0,0 @@
|
||||
import { History } from '@remix-run/router';
|
||||
import React from 'react';
|
||||
import { Outlet, RouterProvider, createHashRouter, useLocation } from 'react-router-dom';
|
||||
|
||||
import { useLegacyRouterSync } from 'hooks/useLegacyRouterSync';
|
||||
import { STABLE_APP_ROUTES } from './routes/routes';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
import AppHeader from 'components/AppHeader';
|
||||
import { DASHBOARD_APP_PATHS, DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
||||
|
||||
const router = createHashRouter([{
|
||||
element: <StableAppLayout />,
|
||||
children: [
|
||||
...STABLE_APP_ROUTES,
|
||||
...DASHBOARD_APP_ROUTES
|
||||
]
|
||||
}]);
|
||||
|
||||
export default function StableAppRouter({ history }: Readonly<{ history: History }>) {
|
||||
useLegacyRouterSync({ router, history });
|
||||
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout component that renders legacy components required on all pages.
|
||||
* NOTE: The app will crash if these get removed from the DOM.
|
||||
*/
|
||||
function StableAppLayout() {
|
||||
const location = useLocation();
|
||||
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
|
||||
.some(path => location.pathname.startsWith(`/${path}`));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Backdrop />
|
||||
<AppHeader isHidden={isNewLayoutPath} />
|
||||
|
||||
<Outlet />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RouteObject, redirect } from 'react-router-dom';
|
||||
import { Navigate, RouteObject } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
|
||||
import ConnectionRequired from 'components/ConnectionRequired';
|
||||
@@ -11,6 +11,7 @@ import AppLayout from '../AppLayout';
|
||||
import { REDIRECTS } from './_redirects';
|
||||
import { ASYNC_USER_ROUTES } from './asyncRoutes';
|
||||
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
|
||||
import BangRedirect from 'components/router/BangRedirect';
|
||||
|
||||
export const STABLE_APP_ROUTES: RouteObject[] = [
|
||||
{
|
||||
@@ -27,11 +28,16 @@ export const STABLE_APP_ROUTES: RouteObject[] = [
|
||||
},
|
||||
|
||||
/* Public routes */
|
||||
{ index: true, loader: () => redirect('/home.html') },
|
||||
{ index: true, element: <Navigate replace to='/home.html' /> },
|
||||
...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
path: '!/*',
|
||||
Component: BangRedirect
|
||||
},
|
||||
|
||||
/* Redirects for old paths */
|
||||
...REDIRECTS.map(toRedirectRoute)
|
||||
];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||
import React, { FunctionComponent, useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
@@ -11,11 +12,11 @@ import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
const UserProfile: FunctionComponent = () => {
|
||||
const userId = getParameterByName('userId');
|
||||
const [ searchParams ] = useSearchParams();
|
||||
const userId = searchParams.get('userId');
|
||||
const [ userName, setUserName ] = useState('');
|
||||
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
@@ -24,7 +25,12 @@ const UserProfile: FunctionComponent = () => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
console.error('[userprofile] Unexpected null page reference');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
console.error('[userprofile] missing user id');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -72,7 +78,7 @@ const UserProfile: FunctionComponent = () => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
console.error('[userprofile] Unexpected null page reference');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,6 +116,11 @@ const UserProfile: FunctionComponent = () => {
|
||||
reader.onerror = onFileReaderError;
|
||||
reader.onabort = onFileReaderAbort;
|
||||
reader.onload = () => {
|
||||
if (!userId) {
|
||||
console.error('[userprofile] missing user id');
|
||||
return;
|
||||
}
|
||||
|
||||
userImage.style.backgroundImage = 'url(' + reader.result + ')';
|
||||
window.ApiClient.uploadUserImage(userId, ImageType.Primary, file).then(function () {
|
||||
loading.hide();
|
||||
@@ -123,6 +134,11 @@ const UserProfile: FunctionComponent = () => {
|
||||
};
|
||||
|
||||
(page.querySelector('#btnDeleteImage') as HTMLButtonElement).addEventListener('click', function () {
|
||||
if (!userId) {
|
||||
console.error('[userprofile] missing user id');
|
||||
return;
|
||||
}
|
||||
|
||||
confirm(
|
||||
globalize.translate('DeleteImageConfirmation'),
|
||||
globalize.translate('DeleteImage')
|
||||
|
||||
@@ -83,7 +83,7 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
||||
if (firstConnection.State === ConnectionState.ServerSignIn) {
|
||||
// Verify the wizard is complete
|
||||
try {
|
||||
const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`);
|
||||
const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`, { cache: 'no-cache' });
|
||||
if (!infoResponse.ok) {
|
||||
throw new Error('Public system info request failed');
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MINIMUM_VERSION } from '@jellyfin/sdk/lib/versions';
|
||||
import { ConnectionManager, Credentials, ApiClient } from 'jellyfin-apiclient';
|
||||
|
||||
import { appHost } from './apphost';
|
||||
@@ -33,8 +34,13 @@ class ServerConnections extends ConnectionManager {
|
||||
super(...arguments);
|
||||
this.localApiClient = null;
|
||||
|
||||
// Set the apiclient minimum version to match the SDK
|
||||
this._minServerVersion = MINIMUM_VERSION;
|
||||
|
||||
Events.on(this, 'localusersignedout', (_e, logoutInfo) => {
|
||||
setUserInfo(null, null);
|
||||
// Ensure the updated credentials are persisted to storage
|
||||
credentialProvider.credentials(credentialProvider.credentials());
|
||||
|
||||
if (window.NativeShell && typeof window.NativeShell.onLocalUserSignedOut === 'function') {
|
||||
window.NativeShell.onLocalUserSignedOut(logoutInfo);
|
||||
@@ -59,7 +65,7 @@ class ServerConnections extends ConnectionManager {
|
||||
);
|
||||
|
||||
apiClient.enableAutomaticNetworking = false;
|
||||
apiClient.manualAddressOnly = false;
|
||||
apiClient.manualAddressOnly = true;
|
||||
|
||||
this.addApiClient(apiClient);
|
||||
|
||||
@@ -128,12 +134,12 @@ class ServerConnections extends ConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
const credentials = new Credentials();
|
||||
const credentialProvider = new Credentials();
|
||||
|
||||
const capabilities = Dashboard.capabilities(appHost);
|
||||
|
||||
export default new ServerConnections(
|
||||
credentials,
|
||||
credentialProvider,
|
||||
appHost.appName(),
|
||||
appHost.appVersion(),
|
||||
appHost.deviceName(),
|
||||
|
||||
@@ -242,7 +242,7 @@ const supportedFeatures = function () {
|
||||
features.push('fullscreenchange');
|
||||
}
|
||||
|
||||
if (browser.tv || browser.xboxOne || browser.ps4 || browser.mobile) {
|
||||
if (browser.tv || browser.xboxOne || browser.ps4 || browser.mobile || browser.ipad) {
|
||||
features.push('physicalvolumecontrol');
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export function setCardData(items, options) {
|
||||
options.coverImage = true;
|
||||
} else if (primaryImageAspectRatio >= 1.33) {
|
||||
options.shape = getBackdropShape(requestedShape === 'autooverflow');
|
||||
} else if (primaryImageAspectRatio > 0.71) {
|
||||
} else if (primaryImageAspectRatio > 0.8) {
|
||||
options.shape = getSquareShape(requestedShape === 'autooverflow');
|
||||
} else {
|
||||
options.shape = getPortraitShape(requestedShape === 'autooverflow');
|
||||
@@ -1139,7 +1139,9 @@ function getHoverMenuHtml(item, action) {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="cardOverlayContainer itemAction" data-action="' + action + '">';
|
||||
const url = appRouter.getRouteUrl(item);
|
||||
const url = appRouter.getRouteUrl(item, {
|
||||
serverId: item.ServerId || ServerConnections.currentApiClient().serverId()
|
||||
});
|
||||
html += '<a href="' + url + '" class="cardImageContainer"></a>';
|
||||
|
||||
const btnCssClass = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light';
|
||||
|
||||
@@ -454,15 +454,15 @@ describe('resolveMixedShapeByAspectRatio', () => {
|
||||
expect(resolveMixedShapeByAspectRatio(1.34)).toEqual('mixedBackdrop');
|
||||
});
|
||||
|
||||
test('primary aspect ratio is > 0.71', () => {
|
||||
expect(resolveMixedShapeByAspectRatio(0.72)).toEqual('mixedSquare');
|
||||
expect(resolveMixedShapeByAspectRatio(0.73)).toEqual('mixedSquare');
|
||||
test('primary aspect ratio is > 0.8', () => {
|
||||
expect(resolveMixedShapeByAspectRatio(0.81)).toEqual('mixedSquare');
|
||||
expect(resolveMixedShapeByAspectRatio(0.82)).toEqual('mixedSquare');
|
||||
expect(resolveMixedShapeByAspectRatio(1.32)).toEqual('mixedSquare');
|
||||
});
|
||||
|
||||
test('primary aspect ratio is <= 0.71', () => {
|
||||
expect(resolveMixedShapeByAspectRatio(0.71)).toEqual('mixedPortrait');
|
||||
expect(resolveMixedShapeByAspectRatio(0.70)).toEqual('mixedPortrait');
|
||||
test('primary aspect ratio is <= 0.8', () => {
|
||||
expect(resolveMixedShapeByAspectRatio(0.8)).toEqual('mixedPortrait');
|
||||
expect(resolveMixedShapeByAspectRatio(0.79)).toEqual('mixedPortrait');
|
||||
expect(resolveMixedShapeByAspectRatio(0.01)).toEqual('mixedPortrait');
|
||||
});
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export const resolveMixedShapeByAspectRatio = (primaryImageAspectRatio: number |
|
||||
|
||||
if (primaryImageAspectRatio >= 1.33) {
|
||||
return CardShape.MixedBackdrop;
|
||||
} else if (primaryImageAspectRatio > 0.71) {
|
||||
} else if (primaryImageAspectRatio > 0.8) {
|
||||
return CardShape.MixedSquare;
|
||||
} else {
|
||||
return CardShape.MixedPortrait;
|
||||
|
||||
@@ -21,7 +21,7 @@ export function buildCardImage(
|
||||
shape = CardShape.Banner;
|
||||
} else if (item.PrimaryImageAspectRatio >= 1.33) {
|
||||
shape = CardShape.Backdrop;
|
||||
} else if (item.PrimaryImageAspectRatio > 0.71) {
|
||||
} else if (item.PrimaryImageAspectRatio > 0.8) {
|
||||
shape = CardShape.Square;
|
||||
} else {
|
||||
shape = CardShape.Portrait;
|
||||
|
||||
@@ -9,7 +9,7 @@ import ButtonElement from '../../../elements/ButtonElement';
|
||||
import InputElement from '../../../elements/InputElement';
|
||||
|
||||
type IProps = {
|
||||
userId: string;
|
||||
userId: string | null;
|
||||
};
|
||||
|
||||
const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
||||
@@ -19,7 +19,12 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
console.error('[UserPasswordForm] Unexpected null page reference');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
console.error('[UserPasswordForm] missing user id');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,7 +63,7 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
console.error('[UserPasswordForm] Unexpected null page reference');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,6 +84,11 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
||||
};
|
||||
|
||||
const savePassword = () => {
|
||||
if (!userId) {
|
||||
console.error('[UserPasswordForm.savePassword] missing user id');
|
||||
return;
|
||||
}
|
||||
|
||||
let currentPassword = (page.querySelector('#txtCurrentPassword') as HTMLInputElement).value;
|
||||
const newPassword = (page.querySelector('#txtNewPassword') as HTMLInputElement).value;
|
||||
|
||||
@@ -105,6 +115,11 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
||||
};
|
||||
|
||||
const resetPassword = () => {
|
||||
if (!userId) {
|
||||
console.error('[UserPasswordForm.resetPassword] missing user id');
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = globalize.translate('PasswordResetConfirmation');
|
||||
confirm(msg, globalize.translate('ResetPassword')).then(function () {
|
||||
loading.show();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { history } from '../router/appRouter';
|
||||
import focusManager from '../focusManager';
|
||||
import browser from '../../scripts/browser';
|
||||
import layoutManager from '../layoutManager';
|
||||
@@ -6,6 +5,8 @@ import inputManager from '../../scripts/inputManager';
|
||||
import { toBoolean } from '../../utils/string.ts';
|
||||
import dom from '../../scripts/dom';
|
||||
|
||||
import { history } from 'RootAppRouter';
|
||||
|
||||
import './dialoghelper.scss';
|
||||
import '../../styles/scrollstyles.scss';
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../../elements/emby-collapse/emby-collapse';
|
||||
import './style.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import template from './filterdialog.template.html';
|
||||
import { stopMultiSelect } from '../../components/multiSelect/multiSelect';
|
||||
|
||||
function renderOptions(context, selector, cssClass, items, isCheckedFn) {
|
||||
const elem = context.querySelector(selector);
|
||||
@@ -104,6 +105,7 @@ function updateFilterControls(context, options) {
|
||||
* @param instance {FilterDialog} An instance of FilterDialog
|
||||
*/
|
||||
function triggerChange(instance) {
|
||||
stopMultiSelect();
|
||||
Events.trigger(instance, 'filterchange');
|
||||
}
|
||||
|
||||
|
||||
@@ -169,12 +169,30 @@ export function getCommands(options) {
|
||||
});
|
||||
}
|
||||
|
||||
if (item.Type === 'Season' || item.Type == 'Series') {
|
||||
commands.push({
|
||||
name: globalize.translate('DownloadAll'),
|
||||
id: 'downloadall',
|
||||
icon: 'file_download'
|
||||
});
|
||||
if (appHost.supports('filedownload')) {
|
||||
// CanDownload should probably be updated to return true for these items?
|
||||
if (user.Policy.EnableContentDownloading && (item.Type === 'Season' || item.Type == 'Series')) {
|
||||
commands.push({
|
||||
name: globalize.translate('DownloadAll'),
|
||||
id: 'downloadall',
|
||||
icon: 'file_download'
|
||||
});
|
||||
}
|
||||
|
||||
// Books are promoted to major download Button and therefor excluded in the context menu
|
||||
if (item.CanDownload && item.Type !== 'Book') {
|
||||
commands.push({
|
||||
name: globalize.translate('Download'),
|
||||
id: 'download',
|
||||
icon: 'file_download'
|
||||
});
|
||||
|
||||
commands.push({
|
||||
name: globalize.translate('CopyStreamURL'),
|
||||
id: 'copy-stream',
|
||||
icon: 'content_copy'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (item.CanDelete && options.deleteItem !== false) {
|
||||
@@ -193,21 +211,6 @@ export function getCommands(options) {
|
||||
}
|
||||
}
|
||||
|
||||
// Books are promoted to major download Button and therefor excluded in the context menu
|
||||
if ((item.CanDownload && appHost.supports('filedownload')) && item.Type !== 'Book') {
|
||||
commands.push({
|
||||
name: globalize.translate('Download'),
|
||||
id: 'download',
|
||||
icon: 'file_download'
|
||||
});
|
||||
|
||||
commands.push({
|
||||
name: globalize.translate('CopyStreamURL'),
|
||||
id: 'copy-stream',
|
||||
icon: 'content_copy'
|
||||
});
|
||||
}
|
||||
|
||||
if (commands.length) {
|
||||
commands.push({
|
||||
divider: true
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
* @module components/libraryoptionseditor/libraryoptionseditor
|
||||
*/
|
||||
|
||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||
import escapeHtml from 'escape-html';
|
||||
|
||||
import globalize from '../../scripts/globalize';
|
||||
import dom from '../../scripts/dom';
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
@@ -383,6 +385,13 @@ export async function embed(parent, contentType, libraryOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
const CHAPTER_CONTENT_TYPES = [
|
||||
CollectionType.Homevideos,
|
||||
CollectionType.Movies,
|
||||
CollectionType.Musicvideos,
|
||||
CollectionType.Tvshows
|
||||
];
|
||||
|
||||
export function setContentType(parent, contentType) {
|
||||
if (contentType === 'homevideos' || contentType === 'photos') {
|
||||
parent.querySelector('.chkEnablePhotosContainer').classList.remove('hide');
|
||||
@@ -390,13 +399,9 @@ export function setContentType(parent, contentType) {
|
||||
parent.querySelector('.chkEnablePhotosContainer').classList.add('hide');
|
||||
}
|
||||
|
||||
if (contentType !== 'tvshows' && contentType !== 'movies' && contentType !== 'homevideos' && contentType !== 'musicvideos' && contentType !== 'mixed') {
|
||||
parent.querySelector('.trickplaySettingsSection').classList.add('hide');
|
||||
parent.querySelector('.chapterSettingsSection').classList.add('hide');
|
||||
} else {
|
||||
parent.querySelector('.trickplaySettingsSection').classList.remove('hide');
|
||||
parent.querySelector('.chapterSettingsSection').classList.remove('hide');
|
||||
}
|
||||
const hasChapterOptions = !contentType /* Mixed */ || CHAPTER_CONTENT_TYPES.includes(contentType);
|
||||
parent.querySelector('.trickplaySettingsSection').classList.toggle('hide', !hasChapterOptions);
|
||||
parent.querySelector('.chapterSettingsSection').classList.toggle('hide', !hasChapterOptions);
|
||||
|
||||
if (contentType === 'tvshows') {
|
||||
parent.querySelector('.chkAutomaticallyGroupSeriesContainer').classList.remove('hide');
|
||||
@@ -623,8 +628,8 @@ let currentLibraryOptions;
|
||||
let currentAvailableOptions;
|
||||
|
||||
export default {
|
||||
embed: embed,
|
||||
setContentType: setContentType,
|
||||
getLibraryOptions: getLibraryOptions,
|
||||
setLibraryOptions: setLibraryOptions
|
||||
embed,
|
||||
setContentType,
|
||||
getLibraryOptions,
|
||||
setLibraryOptions
|
||||
};
|
||||
|
||||
@@ -17,6 +17,8 @@ import '../../elements/emby-ratingbutton/emby-ratingbutton';
|
||||
import '../../elements/emby-playstatebutton/emby-playstatebutton';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils';
|
||||
import markdownIt from 'markdown-it';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
function getIndex(item, options) {
|
||||
if (options.index === 'disc') {
|
||||
@@ -415,8 +417,9 @@ export function getListViewHtml(options) {
|
||||
}
|
||||
|
||||
if (enableOverview && item.Overview) {
|
||||
const overview = DOMPurify.sanitize(markdownIt({ html: true }).render(item.Overview || ''));
|
||||
html += '<div class="secondary listItem-overview listItemBodyText">';
|
||||
html += '<bdi>' + item.Overview + '</bdi>';
|
||||
html += '<bdi>' + overview + '</bdi>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ import alert from '../alert';
|
||||
import template from './mediaLibraryCreator.template.html';
|
||||
|
||||
function onAddLibrary(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (isCreating) {
|
||||
return false;
|
||||
}
|
||||
@@ -61,7 +63,6 @@ function onAddLibrary(e) {
|
||||
isCreating = false;
|
||||
loading.hide();
|
||||
});
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function getCollectionTypeOptionsHtml(collectionTypeOptions) {
|
||||
|
||||
@@ -99,9 +99,7 @@ function showSelection(item, isChecked) {
|
||||
parent.appendChild(itemSelectionPanel);
|
||||
|
||||
let cssClass = 'chkItemSelect';
|
||||
if (isChecked && !browser.firefox) {
|
||||
// In firefox, the initial tap hold doesnt' get treated as a click
|
||||
// In other browsers it does, so we need to make sure that initial click is ignored
|
||||
if (isChecked) {
|
||||
cssClass += ' checkedInitial';
|
||||
}
|
||||
const checkedAttribute = isChecked ? ' checked' : '';
|
||||
@@ -573,3 +571,7 @@ export default function (options) {
|
||||
export const startMultiSelect = (card) => {
|
||||
showSelections(card);
|
||||
};
|
||||
|
||||
export const stopMultiSelect = () => {
|
||||
hideSelections();
|
||||
};
|
||||
|
||||
@@ -33,6 +33,10 @@ function enableLocalPlaylistManagement(player) {
|
||||
return player.isLocalPlayer;
|
||||
}
|
||||
|
||||
function supportsPhysicalVolumeControl(player) {
|
||||
return player.isLocalPlayer && appHost.supports('physicalvolumecontrol');
|
||||
}
|
||||
|
||||
function bindToFullscreenChange(player) {
|
||||
if (Screenfull.isEnabled) {
|
||||
Screenfull.on('change', function () {
|
||||
@@ -1157,7 +1161,7 @@ class PlaybackManager {
|
||||
self.setVolume = function (val, player) {
|
||||
player = player || self._currentPlayer;
|
||||
|
||||
if (player) {
|
||||
if (player && !supportsPhysicalVolumeControl(player)) {
|
||||
player.setVolume(val);
|
||||
}
|
||||
};
|
||||
@@ -1165,15 +1169,17 @@ class PlaybackManager {
|
||||
self.getVolume = function (player) {
|
||||
player = player || self._currentPlayer;
|
||||
|
||||
if (player) {
|
||||
if (player && !supportsPhysicalVolumeControl(player)) {
|
||||
return player.getVolume();
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
||||
self.volumeUp = function (player) {
|
||||
player = player || self._currentPlayer;
|
||||
|
||||
if (player) {
|
||||
if (player && !supportsPhysicalVolumeControl(player)) {
|
||||
player.volumeUp();
|
||||
}
|
||||
};
|
||||
@@ -1181,7 +1187,7 @@ class PlaybackManager {
|
||||
self.volumeDown = function (player) {
|
||||
player = player || self._currentPlayer;
|
||||
|
||||
if (player) {
|
||||
if (player && !supportsPhysicalVolumeControl(player)) {
|
||||
player.volumeDown();
|
||||
}
|
||||
};
|
||||
@@ -1856,6 +1862,15 @@ class PlaybackManager {
|
||||
SortBy: options.shuffle ? 'Random' : 'SortName',
|
||||
MediaTypes: 'Audio'
|
||||
}, queryOptions));
|
||||
case 'Genre':
|
||||
return getItemsForPlayback(serverId, mergePlaybackQueries({
|
||||
GenreIds: firstItem.Id,
|
||||
ParentId: firstItem.ParentId,
|
||||
Filters: 'IsNotFolder',
|
||||
Recursive: true,
|
||||
SortBy: options.shuffle ? 'Random' : 'SortName',
|
||||
MediaTypes: 'Video'
|
||||
}, queryOptions));
|
||||
case 'Series':
|
||||
case 'Season':
|
||||
return getSeriesOrSeasonPlaybackPromise(firstItem, options, items);
|
||||
@@ -2284,33 +2299,18 @@ class PlaybackManager {
|
||||
// TODO: This should be the media type requested, not the original media type
|
||||
const mediaType = item.MediaType;
|
||||
|
||||
if (playOptions.fullscreen) {
|
||||
loading.show();
|
||||
}
|
||||
|
||||
return runInterceptors(item, playOptions)
|
||||
.then(() => {
|
||||
if (playOptions.fullscreen) {
|
||||
loading.show();
|
||||
}
|
||||
|
||||
if (!isServerItem(item) || itemHelper.isLocalItem(item)) {
|
||||
return Promise.reject('skip bitrate detection');
|
||||
}
|
||||
|
||||
return apiClient.getEndpointInfo().then((endpointInfo) => {
|
||||
if ((mediaType === 'Video' || mediaType === 'Audio') && appSettings.enableAutomaticBitrateDetection(endpointInfo.IsInNetwork, mediaType)) {
|
||||
return apiClient.detectBitrate().then((bitrate) => {
|
||||
appSettings.maxStreamingBitrate(endpointInfo.IsInNetwork, mediaType, bitrate);
|
||||
return bitrate;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject('skip bitrate detection');
|
||||
});
|
||||
})
|
||||
.catch(() => getSavedMaxStreamingBitrate(apiClient, mediaType))
|
||||
.then((bitrate) => {
|
||||
return playAfterBitrateDetect(bitrate, item, playOptions, onPlaybackStartedFn, prevSource);
|
||||
})
|
||||
.catch(onInterceptorRejection)
|
||||
.finally(() => {
|
||||
.then(() => detectBitrate(apiClient, item, mediaType))
|
||||
.then((bitrate) => {
|
||||
return playAfterBitrateDetect(bitrate, item, playOptions, onPlaybackStartedFn, prevSource)
|
||||
.catch(onPlaybackRejection);
|
||||
})
|
||||
.catch(() => {
|
||||
if (playOptions.fullscreen) {
|
||||
loading.hide();
|
||||
}
|
||||
@@ -2328,7 +2328,13 @@ class PlaybackManager {
|
||||
Events.trigger(self, 'playbackcancelled');
|
||||
}
|
||||
|
||||
function onInterceptorRejection(e) {
|
||||
function onInterceptorRejection() {
|
||||
cancelPlayback();
|
||||
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
function onPlaybackRejection(e) {
|
||||
cancelPlayback();
|
||||
|
||||
let displayErrorCode = 'ErrorDefault';
|
||||
@@ -2363,8 +2369,6 @@ class PlaybackManager {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.hide();
|
||||
|
||||
const options = Object.assign({}, playOptions);
|
||||
|
||||
options.mediaType = item.MediaType;
|
||||
@@ -2502,6 +2506,29 @@ class PlaybackManager {
|
||||
}
|
||||
}
|
||||
|
||||
function detectBitrate(apiClient, item, mediaType) {
|
||||
// FIXME: This is gnarly, but don't want to change too much here in a bugfix
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (!isServerItem(item) || itemHelper.isLocalItem(item)) {
|
||||
return Promise.reject('skip bitrate detection');
|
||||
}
|
||||
|
||||
return apiClient.getEndpointInfo()
|
||||
.then((endpointInfo) => {
|
||||
if ((mediaType === 'Video' || mediaType === 'Audio') && appSettings.enableAutomaticBitrateDetection(endpointInfo.IsInNetwork, mediaType)) {
|
||||
return apiClient.detectBitrate().then((bitrate) => {
|
||||
appSettings.maxStreamingBitrate(endpointInfo.IsInNetwork, mediaType, bitrate);
|
||||
return bitrate;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject('skip bitrate detection');
|
||||
});
|
||||
})
|
||||
.catch(() => getSavedMaxStreamingBitrate(apiClient, mediaType));
|
||||
}
|
||||
|
||||
function playAfterBitrateDetect(maxBitrate, item, playOptions, onPlaybackStartedFn, prevSource) {
|
||||
const startPosition = playOptions.startPositionTicks;
|
||||
|
||||
@@ -3282,18 +3309,21 @@ class PlaybackManager {
|
||||
const streamInfo = error.streamInfo || getPlayerData(player).streamInfo;
|
||||
|
||||
if (streamInfo?.url) {
|
||||
const isAlreadyFallbacking = streamInfo.url.toLowerCase().includes('transcodereasons');
|
||||
const currentlyPreventsVideoStreamCopy = streamInfo.url.toLowerCase().indexOf('allowvideostreamcopy=false') !== -1;
|
||||
const currentlyPreventsAudioStreamCopy = streamInfo.url.toLowerCase().indexOf('allowaudiostreamcopy=false') !== -1;
|
||||
|
||||
// Auto switch to transcoding
|
||||
if (enablePlaybackRetryWithTranscoding(streamInfo, errorType, currentlyPreventsVideoStreamCopy, currentlyPreventsAudioStreamCopy)) {
|
||||
const startTime = getCurrentTicks(player) || streamInfo.playerStartPositionTicks;
|
||||
const isRemoteSource = streamInfo.item.LocationType === 'Remote';
|
||||
// force transcoding and only allow remuxing for remote source like liveTV, but only for initial trial
|
||||
const tryVideoStreamCopy = isRemoteSource && !isAlreadyFallbacking;
|
||||
|
||||
changeStream(player, startTime, {
|
||||
// force transcoding
|
||||
EnableDirectPlay: false,
|
||||
EnableDirectStream: false,
|
||||
AllowVideoStreamCopy: false,
|
||||
EnableDirectStream: tryVideoStreamCopy,
|
||||
AllowVideoStreamCopy: tryVideoStreamCopy,
|
||||
AllowAudioStreamCopy: currentlyPreventsAudioStreamCopy || currentlyPreventsVideoStreamCopy ? false : null
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { pluginManager } from '../pluginManager';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import { appHost } from '../apphost';
|
||||
import { enable, isEnabled, supported } from '../../scripts/autocast';
|
||||
import { enable, isEnabled } from '../../scripts/autocast';
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import dialog from '../dialog/dialog';
|
||||
@@ -200,13 +200,11 @@ function showActivePlayerMenuInternal(playerInfo) {
|
||||
|
||||
html += '</div>';
|
||||
|
||||
if (supported()) {
|
||||
html += '<div><label class="checkboxContainer">';
|
||||
const checkedHtmlAC = isEnabled() ? ' checked' : '';
|
||||
html += '<input type="checkbox" is="emby-checkbox" class="chkAutoCast"' + checkedHtmlAC + '/>';
|
||||
html += '<span>' + globalize.translate('EnableAutoCast') + '</span>';
|
||||
html += '</label></div>';
|
||||
}
|
||||
html += '<div><label class="checkboxContainer">';
|
||||
const checkedHtmlAC = isEnabled() ? ' checked' : '';
|
||||
html += '<input type="checkbox" is="emby-checkbox" class="chkAutoCast"' + checkedHtmlAC + '/>';
|
||||
html += '<span>' + globalize.translate('EnableAutoCast') + '</span>';
|
||||
html += '</label></div>';
|
||||
|
||||
html += '<div style="margin-top:1em;display:flex;justify-content: flex-end;">';
|
||||
|
||||
|
||||
34
src/components/router/BangRedirect.tsx
Normal file
34
src/components/router/BangRedirect.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
|
||||
const BangRedirect = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const to = useMemo(() => {
|
||||
const _to = {
|
||||
search: location.search,
|
||||
hash: location.hash
|
||||
};
|
||||
|
||||
if (location.pathname.startsWith('/!/')) {
|
||||
return { ..._to, pathname: location.pathname.substring(2) };
|
||||
} else if (location.pathname.startsWith('/!')) {
|
||||
return { ..._to, pathname: location.pathname.replace(/^\/!/, '/') };
|
||||
} else if (location.pathname.startsWith('!')) {
|
||||
return { ..._to, pathname: location.pathname.substring(1) };
|
||||
}
|
||||
}, [ location ]);
|
||||
|
||||
if (!to) return null;
|
||||
|
||||
console.warn('[BangRedirect] You are using a deprecated URL format. This will stop working in a future Jellyfin update.');
|
||||
|
||||
return (
|
||||
<Navigate
|
||||
replace
|
||||
to={to}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BangRedirect;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||
import { Action, createHashHistory } from 'history';
|
||||
import { Action } from 'history';
|
||||
|
||||
import { appHost } from '../apphost';
|
||||
import { clearBackdrop, setBackdropTransparency } from '../backdrop/backdrop';
|
||||
@@ -15,8 +15,7 @@ import { queryClient } from 'utils/query/queryClient';
|
||||
import { getItemQuery } from 'hooks/useItem';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
import { ConnectionState } from 'utils/jellyfin-apiclient/ConnectionState.ts';
|
||||
|
||||
export const history = createHashHistory();
|
||||
import { history } from 'RootAppRouter';
|
||||
|
||||
/**
|
||||
* Page types of "no return" (when "Go back" should behave differently, probably quitting the application).
|
||||
@@ -388,7 +387,7 @@ class AppRouter {
|
||||
if (firstResult) {
|
||||
if (firstResult.State === ConnectionState.ServerSignIn) {
|
||||
const url = firstResult.ApiClient.serverAddress() + '/System/Info/Public';
|
||||
fetch(url).then(response => {
|
||||
fetch(url, { cache: 'no-cache' }).then(response => {
|
||||
if (!response.ok) return Promise.reject('fetch failed');
|
||||
return response.json();
|
||||
}).then(data => {
|
||||
|
||||
73
src/components/router/routerHistory.ts
Normal file
73
src/components/router/routerHistory.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type { Router, RouterState } from '@remix-run/router';
|
||||
import type { History, Listener, To } from 'history';
|
||||
|
||||
import Events, { type Event } from 'utils/events';
|
||||
|
||||
const HISTORY_UPDATE_EVENT = 'HISTORY_UPDATE';
|
||||
|
||||
export class RouterHistory implements History {
|
||||
_router: Router;
|
||||
createHref: (arg: any) => string;
|
||||
|
||||
constructor(router: Router) {
|
||||
this._router = router;
|
||||
|
||||
this._router.subscribe(state => {
|
||||
console.debug('[RouterHistory] history update', state);
|
||||
Events.trigger(document, HISTORY_UPDATE_EVENT, [ state ]);
|
||||
});
|
||||
|
||||
this.createHref = router.createHref;
|
||||
}
|
||||
|
||||
get action() {
|
||||
return this._router.state.historyAction;
|
||||
}
|
||||
|
||||
get location() {
|
||||
return this._router.state.location;
|
||||
}
|
||||
|
||||
back() {
|
||||
void this._router.navigate(-1);
|
||||
}
|
||||
|
||||
forward() {
|
||||
void this._router.navigate(1);
|
||||
}
|
||||
|
||||
go(delta: number) {
|
||||
void this._router.navigate(delta);
|
||||
}
|
||||
|
||||
push(to: To, state?: any) {
|
||||
void this._router.navigate(to, { state });
|
||||
}
|
||||
|
||||
replace(to: To, state?: any): void {
|
||||
void this._router.navigate(to, { state, replace: true });
|
||||
}
|
||||
|
||||
block() {
|
||||
// NOTE: We don't seem to use this functionality, so leaving it unimplemented.
|
||||
throw new Error('`history.block()` is not implemented');
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
listen(listener: Listener) {
|
||||
const compatListener = (_e: Event, state: RouterState) => {
|
||||
return listener({ action: state.historyAction, location: state.location });
|
||||
};
|
||||
|
||||
Events.on(document, HISTORY_UPDATE_EVENT, compatListener);
|
||||
|
||||
return () => Events.off(document, HISTORY_UPDATE_EVENT, compatListener);
|
||||
}
|
||||
}
|
||||
|
||||
export const createRouterHistory = (router: Router): History => {
|
||||
return new RouterHistory(router);
|
||||
};
|
||||
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { type ChangeEvent, type FC, useCallback } from 'react';
|
||||
import React, { type ChangeEvent, type FC, useCallback, useRef } from 'react';
|
||||
|
||||
import AlphaPicker from '../alphaPicker/AlphaPickerComponent';
|
||||
import Input from 'elements/emby-input/Input';
|
||||
@@ -20,15 +20,18 @@ const SearchFields: FC<SearchFieldsProps> = ({
|
||||
onSearch = () => { /* no-op */ },
|
||||
query
|
||||
}: SearchFieldsProps) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onAlphaPicked = useCallback((e: Event) => {
|
||||
const value = (e as CustomEvent).detail.value;
|
||||
const inputValue = inputRef.current?.value || '';
|
||||
|
||||
if (value === 'backspace') {
|
||||
onSearch(query.length ? query.substring(0, query.length - 1) : '');
|
||||
onSearch(inputValue.length ? inputValue.substring(0, inputValue.length - 1) : '');
|
||||
} else {
|
||||
onSearch(query + value);
|
||||
onSearch(inputValue + value);
|
||||
}
|
||||
}, [ onSearch, query ]);
|
||||
}, [onSearch]);
|
||||
|
||||
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
onSearch(e.target.value);
|
||||
@@ -43,6 +46,7 @@ const SearchFields: FC<SearchFieldsProps> = ({
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
id='searchTextInput'
|
||||
className='searchfields-txtSearch'
|
||||
type='text'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { BaseItemDto, BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import type { ApiClient } from 'jellyfin-apiclient';
|
||||
import classNames from 'classnames';
|
||||
import React, { type FC, useCallback, useEffect, useState } from 'react';
|
||||
@@ -77,16 +78,32 @@ const SearchResults: FC<SearchResultsProps> = ({ serverId = window.ApiClient.ser
|
||||
).then(ensureNonNullItems)
|
||||
), [getDefaultParameters]);
|
||||
|
||||
const fetchItems = useCallback((apiClient: ApiClient, params = {}) => (
|
||||
apiClient?.getItems(
|
||||
apiClient.getCurrentUserId(),
|
||||
{
|
||||
...getDefaultParameters(),
|
||||
IncludeMedia: true,
|
||||
...params
|
||||
const fetchItems = useCallback(async (apiClient?: ApiClient, params = {}) => {
|
||||
if (!apiClient) {
|
||||
console.error('[SearchResults] no apiClient; unable to fetch items');
|
||||
return {
|
||||
Items: []
|
||||
};
|
||||
}
|
||||
|
||||
const options = {
|
||||
...getDefaultParameters(),
|
||||
IncludeMedia: true,
|
||||
...params
|
||||
};
|
||||
|
||||
if (params.IncludeItemTypes === BaseItemKind.Episode) {
|
||||
const user = await apiClient.getCurrentUser();
|
||||
if (!user?.Configuration?.DisplayMissingEpisodes) {
|
||||
options.IsMissing = false;
|
||||
}
|
||||
).then(ensureNonNullItems)
|
||||
), [getDefaultParameters]);
|
||||
}
|
||||
|
||||
return apiClient.getItems(
|
||||
apiClient.getCurrentUserId(),
|
||||
options
|
||||
).then(ensureNonNullItems);
|
||||
}, [getDefaultParameters]);
|
||||
|
||||
const fetchPeople = useCallback((apiClient: ApiClient, params = {}) => (
|
||||
apiClient?.getPeople(
|
||||
|
||||
@@ -732,9 +732,8 @@ export default function (options) {
|
||||
|
||||
obj.x = eventX;
|
||||
obj.y = eventY;
|
||||
|
||||
showOsd();
|
||||
}
|
||||
showOsd();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,6 @@ import IconButton from '@mui/material/IconButton';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
@@ -17,7 +16,8 @@ interface AppToolbarProps {
|
||||
buttons?: ReactNode
|
||||
isDrawerAvailable: boolean
|
||||
isDrawerOpen: boolean
|
||||
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
||||
onDrawerButtonClick?: (event: React.MouseEvent<HTMLElement>) => void,
|
||||
isUserMenuAvailable?: boolean
|
||||
}
|
||||
|
||||
const onBackButtonClick = () => {
|
||||
@@ -32,17 +32,14 @@ const AppToolbar: FC<AppToolbarProps> = ({
|
||||
children,
|
||||
isDrawerAvailable,
|
||||
isDrawerOpen,
|
||||
onDrawerButtonClick
|
||||
onDrawerButtonClick = () => { /* no-op */ },
|
||||
isUserMenuAvailable = true
|
||||
}) => {
|
||||
const { user } = useApi();
|
||||
const isUserLoggedIn = Boolean(user);
|
||||
const currentLocation = useLocation();
|
||||
|
||||
const isBackButtonAvailable = appRouter.canGoBack();
|
||||
|
||||
// Handles a specific case to hide the user menu on the select server page while authenticated
|
||||
const isUserMenuAvailable = currentLocation.pathname !== '/selectserver.html';
|
||||
|
||||
return (
|
||||
<Toolbar
|
||||
variant='dense'
|
||||
@@ -84,16 +81,14 @@ const AppToolbar: FC<AppToolbarProps> = ({
|
||||
|
||||
{children}
|
||||
|
||||
{isUserLoggedIn && isUserMenuAvailable && (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'flex-end' }}>
|
||||
{buttons}
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'flex-end' }}>
|
||||
{buttons}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flexGrow: 0 }}>
|
||||
<UserMenuButton />
|
||||
</Box>
|
||||
</>
|
||||
{isUserLoggedIn && isUserMenuAvailable && (
|
||||
<Box sx={{ flexGrow: 0 }}>
|
||||
<UserMenuButton />
|
||||
</Box>
|
||||
)}
|
||||
</Toolbar>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Action } from 'history';
|
||||
import { FunctionComponent, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useLocation, useNavigationType } from 'react-router-dom';
|
||||
|
||||
import globalize from '../../scripts/globalize';
|
||||
import type { RestoreViewFailResponse } from '../../types/viewManager';
|
||||
@@ -15,6 +16,34 @@ export interface ViewManagerPageProps {
|
||||
transition?: string
|
||||
}
|
||||
|
||||
interface ViewOptions {
|
||||
url: string
|
||||
type?: string
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
state: any
|
||||
autoFocus: boolean
|
||||
fullscreen?: boolean
|
||||
transition?: string
|
||||
options: {
|
||||
supportsThemeMedia?: boolean
|
||||
enableMediaControl?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const loadView = async (controller: string, view: string, viewOptions: ViewOptions) => {
|
||||
const [ controllerFactory, viewHtml ] = await Promise.all([
|
||||
import(/* webpackChunkName: "[request]" */ `../../controllers/${controller}`),
|
||||
import(/* webpackChunkName: "[request]" */ `../../controllers/${view}`)
|
||||
.then(html => globalize.translateHtml(html))
|
||||
]);
|
||||
|
||||
viewManager.loadView({
|
||||
...viewOptions,
|
||||
controllerFactory,
|
||||
view: viewHtml
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Page component that renders legacy views via the ViewManager.
|
||||
* NOTE: Any new pages should use the generic Page component instead.
|
||||
@@ -29,6 +58,7 @@ const ViewManagerPage: FunctionComponent<ViewManagerPageProps> = ({
|
||||
transition
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const navigationType = useNavigationType();
|
||||
|
||||
useEffect(() => {
|
||||
const loadPage = () => {
|
||||
@@ -45,27 +75,24 @@ const ViewManagerPage: FunctionComponent<ViewManagerPageProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
viewManager.tryRestoreView(viewOptions)
|
||||
if (navigationType !== Action.Pop) {
|
||||
console.debug('[ViewManagerPage] loading view [%s]', view);
|
||||
return loadView(controller, view, viewOptions);
|
||||
}
|
||||
|
||||
console.debug('[ViewManagerPage] restoring view [%s]', view);
|
||||
return viewManager.tryRestoreView(viewOptions)
|
||||
.catch(async (result?: RestoreViewFailResponse) => {
|
||||
if (!result?.cancelled) {
|
||||
const [ controllerFactory, viewHtml ] = await Promise.all([
|
||||
import(/* webpackChunkName: "[request]" */ `../../controllers/${controller}`),
|
||||
import(/* webpackChunkName: "[request]" */ `../../controllers/${view}`)
|
||||
.then(html => globalize.translateHtml(html))
|
||||
]);
|
||||
|
||||
viewManager.loadView({
|
||||
...viewOptions,
|
||||
controllerFactory,
|
||||
view: viewHtml
|
||||
});
|
||||
console.debug('[ViewManagerPage] restore failed; loading view [%s]', view);
|
||||
return loadView(controller, view, viewOptions);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
loadPage();
|
||||
},
|
||||
// location.state is NOT included as a dependency here since dialogs will update state while the current view stays the same
|
||||
// location.state and navigationType are NOT included as dependencies here since dialogs will update state while the current view stays the same
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
controller,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="apiKeysPage" data-role="page" class="page type-interior advancedConfigurationPage fullWidthContent">
|
||||
<div id="apiKeysPage" data-role="page" class="page type-interior advancedConfigurationPage fullWidthContent" data-title="${HeaderApiKeys}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="detailSectionHeader">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="dashboardPage" data-role="page" class="page type-interior dashboardHomePage fullWidthContent">
|
||||
<div id="dashboardPage" data-role="page" class="page type-interior dashboardHomePage fullWidthContent" data-title="${TabDashboard}">
|
||||
<div class="content-primary">
|
||||
<div class="dashboardSections" style="padding-top:.5em;">
|
||||
<div class="dashboardColumn dashboardColumn-2-60 dashboardColumn-3-46">
|
||||
|
||||
@@ -319,7 +319,7 @@ function renderActiveConnections(view, sessions) {
|
||||
html += '<div class="sessionCardButtons flex align-items-center justify-content-center">';
|
||||
|
||||
let btnCssClass = session.ServerId && session.NowPlayingItem && session.SupportsRemoteControl ? '' : ' hide';
|
||||
const playIcon = session.PlayState.IsPaused ? 'pause' : 'play_arrow';
|
||||
const playIcon = session.PlayState.IsPaused ? 'play_arrow' : 'pause';
|
||||
|
||||
html += '<button is="paper-icon-button-light" class="sessionCardButton btnSessionPlayPause paper-icon-button-light ' + btnCssClass + '"><span class="material-icons ' + playIcon + '" aria-hidden="true"></span></button>';
|
||||
html += '<button is="paper-icon-button-light" class="sessionCardButton btnSessionStop paper-icon-button-light ' + btnCssClass + '"><span class="material-icons stop" aria-hidden="true"></span></button>';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="devicesPage" data-role="page" class="page type-interior devicesPage noSecondaryNavPage">
|
||||
<div id="devicesPage" data-role="page" class="page type-interior devicesPage noSecondaryNavPage" data-title="${HeaderDevices}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="verticalSection verticalSection">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="encodingSettingsPage" data-role="page" class="page type-interior playbackConfigurationPage withTabs">
|
||||
<div id="encodingSettingsPage" data-role="page" class="page type-interior playbackConfigurationPage" data-title="${TitlePlayback}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="encodingSettingsForm">
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'jquery';
|
||||
import loading from '../../components/loading/loading';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import dom from '../../scripts/dom';
|
||||
import libraryMenu from '../../scripts/libraryMenu';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import alert from '../../components/alert';
|
||||
|
||||
@@ -167,22 +166,6 @@ function setDecodingCodecsVisible(context, value) {
|
||||
}
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dashboard/playback/transcoding',
|
||||
name: globalize.translate('Transcoding')
|
||||
}, {
|
||||
href: '#/dashboard/playback/resume',
|
||||
name: globalize.translate('ButtonResume')
|
||||
}, {
|
||||
href: '#/dashboard/playback/streaming',
|
||||
name: globalize.translate('TabStreaming')
|
||||
}, {
|
||||
href: '#/dashboard/playback/trickplay',
|
||||
name: globalize.translate('Trickplay')
|
||||
}];
|
||||
}
|
||||
|
||||
let systemInfo;
|
||||
function getSystemInfo() {
|
||||
return systemInfo ? Promise.resolve(systemInfo) : ApiClient.getPublicSystemInfo().then(
|
||||
@@ -292,7 +275,6 @@ $(document).on('pageinit', '#encodingSettingsPage', function () {
|
||||
$('.encodingSettingsForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#encodingSettingsPage', function () {
|
||||
loading.show();
|
||||
libraryMenu.setTabs('playback', 0, getTabs);
|
||||
const page = this;
|
||||
ApiClient.getNamedConfiguration('encoding').then(function (config) {
|
||||
ApiClient.getSystemInfo().then(function (fetchedSystemInfo) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="dashboardGeneralPage" data-role="page" class="page type-interior dashboardHomePage">
|
||||
<div id="dashboardGeneralPage" data-role="page" class="page type-interior dashboardHomePage" data-title="${General}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="dashboardGeneralForm">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="mediaLibraryPage" data-role="page" class="page type-interior mediaLibraryPage librarySectionPage withTabs fullWidthContent">
|
||||
<div id="mediaLibraryPage" data-role="page" class="page type-interior mediaLibraryPage librarySectionPage fullWidthContent" data-title="${HeaderLibraries}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="padded-top padded-bottom">
|
||||
|
||||
@@ -2,7 +2,6 @@ import escapeHtml from 'escape-html';
|
||||
import 'jquery';
|
||||
import taskButton from '../../scripts/taskbutton';
|
||||
import loading from '../../components/loading/loading';
|
||||
import libraryMenu from '../../scripts/libraryMenu';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import dom from '../../scripts/dom';
|
||||
import imageHelper from '../../utils/image';
|
||||
@@ -358,22 +357,6 @@ function getVirtualFolderHtml(page, virtualFolder, index) {
|
||||
return html;
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dashboard/libraries',
|
||||
name: globalize.translate('HeaderLibraries')
|
||||
}, {
|
||||
href: '#/dashboard/libraries/display',
|
||||
name: globalize.translate('Display')
|
||||
}, {
|
||||
href: '#/dashboard/libraries/metadata',
|
||||
name: globalize.translate('Metadata')
|
||||
}, {
|
||||
href: '#/dashboard/libraries/nfo',
|
||||
name: globalize.translate('TabNfoSettings')
|
||||
}];
|
||||
}
|
||||
|
||||
window.WizardLibraryPage = {
|
||||
next: function () {
|
||||
Dashboard.navigate('wizardsettings.html');
|
||||
@@ -383,8 +366,6 @@ pageClassOn('pageshow', 'mediaLibraryPage', function () {
|
||||
reloadLibrary(this);
|
||||
});
|
||||
pageIdOn('pageshow', 'mediaLibraryPage', function () {
|
||||
libraryMenu.setTabs('librarysetup', 0, getTabs);
|
||||
|
||||
const page = this;
|
||||
taskButton({
|
||||
mode: 'on',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="libraryDisplayPage" data-role="page" class="page type-interior librarySectionPage withTabs">
|
||||
<div id="libraryDisplayPage" data-role="page" class="page type-interior librarySectionPage" data-title="${Display}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form>
|
||||
|
||||
@@ -1,26 +1,8 @@
|
||||
import globalize from '../../scripts/globalize';
|
||||
import loading from '../../components/loading/loading';
|
||||
import libraryMenu from '../../scripts/libraryMenu';
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dashboard/libraries',
|
||||
name: globalize.translate('HeaderLibraries')
|
||||
}, {
|
||||
href: '#/dashboard/libraries/display',
|
||||
name: globalize.translate('Display')
|
||||
}, {
|
||||
href: '#/dashboard/libraries/metadata',
|
||||
name: globalize.translate('Metadata')
|
||||
}, {
|
||||
href: '#/dashboard/libraries/nfo',
|
||||
name: globalize.translate('TabNfoSettings')
|
||||
}];
|
||||
}
|
||||
|
||||
export default function(view) {
|
||||
function loadData() {
|
||||
ApiClient.getServerConfiguration().then(function(config) {
|
||||
@@ -57,7 +39,6 @@ export default function(view) {
|
||||
});
|
||||
|
||||
view.addEventListener('viewshow', function() {
|
||||
libraryMenu.setTabs('librarysetup', 1, getTabs);
|
||||
loadData();
|
||||
ApiClient.getSystemInfo().then(function(info) {
|
||||
if (info.OperatingSystem === 'Windows') {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="logPage" data-role="page" class="page type-interior">
|
||||
<div id="logPage" data-role="page" class="page type-interior" data-title="${TabLogs}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="logsForm">
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ImageResolution } from '@jellyfin/sdk/lib/generated-client/models/image
|
||||
import 'jquery';
|
||||
|
||||
import loading from '../../components/loading/loading';
|
||||
import libraryMenu from '../../scripts/libraryMenu';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
|
||||
@@ -86,26 +85,9 @@ function onSubmit() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dashboard/libraries',
|
||||
name: globalize.translate('HeaderLibraries')
|
||||
}, {
|
||||
href: '#/dashboard/libraries/display',
|
||||
name: globalize.translate('Display')
|
||||
}, {
|
||||
href: '#/dashboard/libraries/metadata',
|
||||
name: globalize.translate('Metadata')
|
||||
}, {
|
||||
href: '#/dashboard/libraries/nfo',
|
||||
name: globalize.translate('TabNfoSettings')
|
||||
}];
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#metadataImagesConfigurationPage', function() {
|
||||
$('.metadataImagesConfigurationForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#metadataImagesConfigurationPage', function() {
|
||||
libraryMenu.setTabs('metadata', 2, getTabs);
|
||||
loading.show();
|
||||
loadPage(this);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="metadataImagesConfigurationPage" data-role="page" class="page type-interior metadataConfigurationPage withTabs">
|
||||
<div id="metadataImagesConfigurationPage" data-role="page" class="page type-interior metadataConfigurationPage" data-title="${Metadata}">
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="metadataNfoPage" data-role="page" class="page type-interior metadataConfigurationPage withTabs">
|
||||
<div id="metadataNfoPage" data-role="page" class="page type-interior metadataConfigurationPage" data-title="${TabNfoSettings}">
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import escapeHtml from 'escape-html';
|
||||
import 'jquery';
|
||||
import loading from '../../components/loading/loading';
|
||||
import libraryMenu from '../../scripts/libraryMenu';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import alert from '../../components/alert';
|
||||
@@ -44,27 +43,10 @@ function showConfirmMessage() {
|
||||
});
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dashboard/libraries',
|
||||
name: globalize.translate('HeaderLibraries')
|
||||
}, {
|
||||
href: '#/dashboard/libraries/display',
|
||||
name: globalize.translate('Display')
|
||||
}, {
|
||||
href: '#/dashboard/libraries/metadata',
|
||||
name: globalize.translate('Metadata')
|
||||
}, {
|
||||
href: '#/dashboard/libraries/nfo',
|
||||
name: globalize.translate('TabNfoSettings')
|
||||
}];
|
||||
}
|
||||
|
||||
const metadataKey = 'xbmcmetadata';
|
||||
$(document).on('pageinit', '#metadataNfoPage', function () {
|
||||
$('.metadataNfoForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#metadataNfoPage', function () {
|
||||
libraryMenu.setTabs('metadata', 3, getTabs);
|
||||
loading.show();
|
||||
const page = this;
|
||||
const promise1 = ApiClient.getUsers();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="networkingPage" data-role="page" class="page type-interior advancedConfigurationPage">
|
||||
<div id="networkingPage" data-role="page" class="page type-interior advancedConfigurationPage" data-title="${TabNetworking}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="dashboardHostingForm">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="playbackConfigurationPage" data-role="page" class="page type-interior playbackConfigurationPage withTabs">
|
||||
<div id="playbackConfigurationPage" data-role="page" class="page type-interior playbackConfigurationPage" data-title="${ButtonResume}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="playbackConfigurationForm">
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'jquery';
|
||||
import loading from '../../components/loading/loading';
|
||||
import libraryMenu from '../../scripts/libraryMenu';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
|
||||
function loadPage(page, config) {
|
||||
@@ -29,27 +27,10 @@ function onSubmit() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dashboard/playback/transcoding',
|
||||
name: globalize.translate('Transcoding')
|
||||
}, {
|
||||
href: '#/dashboard/playback/resume',
|
||||
name: globalize.translate('ButtonResume')
|
||||
}, {
|
||||
href: '#/dashboard/playback/streaming',
|
||||
name: globalize.translate('TabStreaming')
|
||||
}, {
|
||||
href: '#/dashboard/playback/trickplay',
|
||||
name: globalize.translate('Trickplay')
|
||||
}];
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#playbackConfigurationPage', function () {
|
||||
$('.playbackConfigurationForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#playbackConfigurationPage', function () {
|
||||
loading.show();
|
||||
libraryMenu.setTabs('playback', 1, getTabs);
|
||||
const page = this;
|
||||
ApiClient.getServerConfiguration().then(function (config) {
|
||||
loadPage(page, config);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="pluginCatalogPage" data-role="page" class="page type-interior pluginConfigurationPage withTabs fullWidthContent">
|
||||
<div id="pluginCatalogPage" data-role="page" class="page type-interior pluginConfigurationPage fullWidthContent" data-title="${TabCatalog}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="inputContainer">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import escapeHTML from 'escape-html';
|
||||
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import libraryMenu from '../../../../scripts/libraryMenu';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import '../../../../components/cardbuilder/card.scss';
|
||||
import '../../../../elements/emby-button/emby-button';
|
||||
@@ -159,22 +158,8 @@ function getPluginHtml(plugin, options, installedPlugins) {
|
||||
return html;
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dashboard/plugins',
|
||||
name: globalize.translate('TabMyPlugins')
|
||||
}, {
|
||||
href: '#/dashboard/plugins/catalog',
|
||||
name: globalize.translate('TabCatalog')
|
||||
}, {
|
||||
href: '#/dashboard/plugins/repositories',
|
||||
name: globalize.translate('TabRepositories')
|
||||
}];
|
||||
}
|
||||
|
||||
export default function (view) {
|
||||
view.addEventListener('viewshow', function () {
|
||||
libraryMenu.setTabs('plugins', 1, getTabs);
|
||||
reloadList(this);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="pluginsPage" data-role="page" class="page type-interior pluginConfigurationPage withTabs fullWidthContent">
|
||||
<div id="pluginsPage" data-role="page" class="page type-interior pluginConfigurationPage fullWidthContent" data-title="${TabPlugins}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="inputContainer">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import libraryMenu from '../../../../scripts/libraryMenu';
|
||||
import dom from '../../../../scripts/dom';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import '../../../../components/cardbuilder/card.scss';
|
||||
@@ -219,19 +218,6 @@ function reloadList(page) {
|
||||
});
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dashboard/plugins',
|
||||
name: globalize.translate('TabMyPlugins')
|
||||
}, {
|
||||
href: '#/dashboard/plugins/catalog',
|
||||
name: globalize.translate('TabCatalog')
|
||||
}, {
|
||||
href: '#/dashboard/plugins/repositories',
|
||||
name: globalize.translate('TabRepositories')
|
||||
}];
|
||||
}
|
||||
|
||||
function onInstalledPluginsClick(e) {
|
||||
if (dom.parentWithClass(e.target, 'noConfigPluginCard')) {
|
||||
showNoConfigurationMessage();
|
||||
@@ -257,7 +243,6 @@ function onFilterType(page, searchBar) {
|
||||
}
|
||||
|
||||
pageIdOn('pageshow', 'pluginsPage', function () {
|
||||
libraryMenu.setTabs('plugins', 0, getTabs);
|
||||
reloadList(this);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="repositories" data-role="page" class="page type-interior withTabs fullWidthContent">
|
||||
<div id="repositories" data-role="page" class="page type-interior fullWidthContent" data-title="${TabRepositories}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import libraryMenu from '../../../../scripts/libraryMenu';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import dialogHelper from '../../../../components/dialogHelper/dialogHelper';
|
||||
import confirm from '../../../../components/confirm/confirm';
|
||||
@@ -103,22 +102,8 @@ function getRepositoryElement(repository) {
|
||||
return listItem;
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dashboard/plugins',
|
||||
name: globalize.translate('TabMyPlugins')
|
||||
}, {
|
||||
href: '#/dashboard/plugins/catalog',
|
||||
name: globalize.translate('TabCatalog')
|
||||
}, {
|
||||
href: '#/dashboard/plugins/repositories',
|
||||
name: globalize.translate('TabRepositories')
|
||||
}];
|
||||
}
|
||||
|
||||
export default function(view) {
|
||||
view.addEventListener('viewshow', function () {
|
||||
libraryMenu.setTabs('plugins', 2, getTabs);
|
||||
reloadList(this);
|
||||
|
||||
const save = this;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-role="popup" id="popupAddTrigger" class="dialog dialog-fixedSize dialog-medium hide" style="position: fixed; top: 10%;">
|
||||
<div data-role="popup" id="popupAddTrigger" class="dialog dialog-fixedSize dialog-medium hide" style="position: fixed; top: 10%; z-index: 999999;">
|
||||
<form class="addTriggerForm" style="padding:1em;">
|
||||
<div class="ui-bar-a">
|
||||
<h3>${ButtonAddScheduledTaskTrigger}</h3>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="scheduledTasksPage" data-role="page" class="page type-interior scheduledTasksConfigurationPage">
|
||||
<div id="scheduledTasksPage" data-role="page" class="page type-interior scheduledTasksConfigurationPage" data-title="${TabScheduledTasks}">
|
||||
<style>
|
||||
.taskProgressOuter {
|
||||
height: 6px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="streamingSettingsPage" data-role="page" class="page type-interior playbackConfigurationPage withTabs">
|
||||
<div id="streamingSettingsPage" data-role="page" class="page type-interior playbackConfigurationPage" data-title="${TabStreaming}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="streamingSettingsForm">
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'jquery';
|
||||
import libraryMenu from '../../scripts/libraryMenu';
|
||||
import loading from '../../components/loading/loading';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
|
||||
function loadPage(page, config) {
|
||||
@@ -20,27 +18,10 @@ function onSubmit() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dashboard/playback/transcoding',
|
||||
name: globalize.translate('Transcoding')
|
||||
}, {
|
||||
href: '#/dashboard/playback/resume',
|
||||
name: globalize.translate('ButtonResume')
|
||||
}, {
|
||||
href: '#/dashboard/playback/streaming',
|
||||
name: globalize.translate('TabStreaming')
|
||||
}, {
|
||||
href: '#/dashboard/playback/trickplay',
|
||||
name: globalize.translate('Trickplay')
|
||||
}];
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#streamingSettingsPage', function () {
|
||||
$('.streamingSettingsForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#streamingSettingsPage', function () {
|
||||
loading.show();
|
||||
libraryMenu.setTabs('playback', 2, getTabs);
|
||||
const page = this;
|
||||
ApiClient.getServerConfiguration().then(function (config) {
|
||||
loadPage(page, config);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import { PersonKind } from '@jellyfin/sdk/lib/generated-client/models/person-kind';
|
||||
import { intervalToDuration } from 'date-fns';
|
||||
import DOMPurify from 'dompurify';
|
||||
@@ -988,6 +989,9 @@ function renderDirector(page, item, context) {
|
||||
}
|
||||
|
||||
function renderStudio(page, item, context) {
|
||||
// The list of studios can be massive for collections of items
|
||||
if ([BaseItemKind.BoxSet, BaseItemKind.Playlist].includes(item.Type)) return;
|
||||
|
||||
const studios = item.Studios || [];
|
||||
|
||||
const html = studios.map(function (studio) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import ServerConnections from '../components/ServerConnections';
|
||||
import LibraryMenu from '../scripts/libraryMenu';
|
||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by';
|
||||
import { stopMultiSelect } from 'components/multiSelect/multiSelect';
|
||||
|
||||
function getInitialLiveTvQuery(instance, params, startIndex = 0, limit = 300) {
|
||||
const query = {
|
||||
@@ -850,6 +851,10 @@ class ItemsView {
|
||||
setTitle(null);
|
||||
getItem(params).then(function (item) {
|
||||
setTitle(item);
|
||||
if (item && item.Type == 'Genre') {
|
||||
item.ParentId = params.parentId;
|
||||
}
|
||||
|
||||
self.currentItem = item;
|
||||
const refresh = !isRestored;
|
||||
self.itemsContainer.resume({
|
||||
@@ -1139,6 +1144,9 @@ class ItemsView {
|
||||
|
||||
setFilterStatus(hasFilters) {
|
||||
this.hasFilters = hasFilters;
|
||||
if (this.hasFilters) {
|
||||
stopMultiSelect();
|
||||
}
|
||||
const filterButtons = this.filterButtons;
|
||||
|
||||
if (filterButtons.length) {
|
||||
@@ -1301,4 +1309,3 @@ class ItemsView {
|
||||
}
|
||||
|
||||
export default ItemsView;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="liveTvSettingsPage" data-role="page" class="page type-interior liveTvPage">
|
||||
<div id="liveTvSettingsPage" data-role="page" class="page type-interior liveTvPage" data-title="${HeaderDVR}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="verticalSection">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="liveTvStatusPage" data-role="page" class="page type-interior liveTvSettingsPage">
|
||||
<div id="liveTvStatusPage" data-role="page" class="page type-interior liveTvSettingsPage" data-title="${LiveTV}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
|
||||
@@ -207,7 +207,7 @@ export default function (view, params, tabContent) {
|
||||
name: globalize.translate('Name'),
|
||||
id: 'SortName'
|
||||
}, {
|
||||
name: globalize.translate('OptionImdbRating'),
|
||||
name: globalize.translate('OptionCommunityRating'),
|
||||
id: 'CommunityRating,SortName'
|
||||
}, {
|
||||
name: globalize.translate('OptionDateAdded'),
|
||||
|
||||
@@ -212,7 +212,7 @@ export default function (view, params, tabContent, options) {
|
||||
name: globalize.translate('OptionRandom'),
|
||||
id: 'Random'
|
||||
}, {
|
||||
name: globalize.translate('OptionImdbRating'),
|
||||
name: globalize.translate('OptionCommunityRating'),
|
||||
id: 'CommunityRating,SortName,ProductionYear'
|
||||
}, {
|
||||
name: globalize.translate('OptionCriticRating'),
|
||||
@@ -321,4 +321,3 @@ export default function (view, params, tabContent, options) {
|
||||
itemsContainer = null;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ export default function (view, params, tabContent) {
|
||||
name: globalize.translate('Name'),
|
||||
id: 'SortName'
|
||||
}, {
|
||||
name: globalize.translate('OptionImdbRating'),
|
||||
name: globalize.translate('OptionCommunityRating'),
|
||||
id: 'CommunityRating,SortName'
|
||||
}, {
|
||||
name: globalize.translate('OptionDateAdded'),
|
||||
|
||||
@@ -33,15 +33,15 @@
|
||||
<span class="xlargePaperIconButton material-icons fiber_manual_record" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<button is="paper-icon-button-light" class="btnPreviousTrack autoSize hide" title="${PreviousTrack}">
|
||||
<button is="paper-icon-button-light" class="btnPreviousTrack autoSize hide" title="${PreviousTrack} (Shift+P)" aria-label="${PreviousTrack}">
|
||||
<span class="xlargePaperIconButton material-icons skip_previous" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<button is="paper-icon-button-light" class="btnPreviousChapter autoSize hide" title="${PreviousChapter}">
|
||||
<button is="paper-icon-button-light" class="btnPreviousChapter autoSize hide" title="${PreviousChapter} (PageDown)" aria-label="${PreviousChapter}">
|
||||
<span class="xlargePaperIconButton material-icons undo" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<button is="paper-icon-button-light" class="btnRewind" title="${Rewind} (j)" aria-label="${Rewind}">
|
||||
<button is="paper-icon-button-light" class="btnRewind" title="${Rewind} (J)" aria-label="${Rewind}">
|
||||
<span class="xlargePaperIconButton material-icons fast_rewind" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
@@ -49,15 +49,15 @@
|
||||
<span class="xlargePaperIconButton material-icons pause" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<button is="paper-icon-button-light" class="btnFastForward" title="${FastForward} (l)" aria-label="${FastForward}">
|
||||
<button is="paper-icon-button-light" class="btnFastForward" title="${FastForward} (L)" aria-label="${FastForward}">
|
||||
<span class="xlargePaperIconButton material-icons fast_forward" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<button is="paper-icon-button-light" class="btnNextChapter autoSize hide" title="${NextChapter}">
|
||||
<button is="paper-icon-button-light" class="btnNextChapter autoSize hide" title="${NextChapter} (PageUp)" aria-label="${NextChapter}">
|
||||
<span class="xlargePaperIconButton material-icons redo" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<button is="paper-icon-button-light" class="btnNextTrack autoSize hide" title="${NextTrack}">
|
||||
<button is="paper-icon-button-light" class="btnNextTrack autoSize hide" title="${NextTrack} (Shift+N)" aria-label="${NextTrack}">
|
||||
<span class="xlargePaperIconButton material-icons skip_next" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -65,7 +65,7 @@
|
||||
<div class="osdTimeText">
|
||||
<span class="endsAtText"></span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="osdRatingsText">
|
||||
</div>
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<span class="xlargePaperIconButton material-icons audiotrack" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="volumeButtons hide-mouse-idle-tv">
|
||||
<button is="paper-icon-button-light" class="buttonMute autoSize" title="${Mute} (m)" aria-label="${Mute}">
|
||||
<button is="paper-icon-button-light" class="buttonMute autoSize" title="${Mute} (M)" aria-label="${Mute}">
|
||||
<span class="xlargePaperIconButton material-icons volume_up" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="sliderContainer osdVolumeSliderContainer">
|
||||
@@ -96,7 +96,7 @@
|
||||
<button is="paper-icon-button-light" class="btnPip hide autoSize" title="${PictureInPicture}">
|
||||
<span class="xlargePaperIconButton material-icons picture_in_picture_alt" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button is="paper-icon-button-light" class="btnFullscreen hide autoSize" title="${Fullscreen} (f)" aria-label="${Fullscreen}">
|
||||
<button is="paper-icon-button-light" class="btnFullscreen hide autoSize" title="${Fullscreen} (F)" aria-label="${Fullscreen}">
|
||||
<span class="xlargePaperIconButton material-icons fullscreen" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,7 @@ import LibraryMenu from '../../../scripts/libraryMenu';
|
||||
import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components/backdrop/backdrop';
|
||||
import { pluginManager } from '../../../components/pluginManager';
|
||||
import { PluginType } from '../../../types/plugin.ts';
|
||||
import { EventType } from 'types/eventType';
|
||||
|
||||
const TICKS_PER_MINUTE = 600000000;
|
||||
const TICKS_PER_SECOND = 10000000;
|
||||
@@ -280,12 +281,14 @@ export default function (view) {
|
||||
let mouseIsDown = false;
|
||||
|
||||
function showOsd(focusElement) {
|
||||
Events.trigger(document, EventType.SHOW_VIDEO_OSD, [ true ]);
|
||||
slideDownToShow(headerElement);
|
||||
showMainOsdControls(focusElement);
|
||||
resetIdle();
|
||||
}
|
||||
|
||||
function hideOsd() {
|
||||
Events.trigger(document, EventType.SHOW_VIDEO_OSD, [ false ]);
|
||||
slideUpToHide(headerElement);
|
||||
hideMainOsdControls();
|
||||
mouseManager.hideCursor();
|
||||
@@ -320,18 +323,14 @@ export default function (view) {
|
||||
}
|
||||
|
||||
function clearHideAnimationEventListeners(elem) {
|
||||
dom.removeEventListener(elem, transitionEndEventName, onHideAnimationComplete, {
|
||||
once: true
|
||||
});
|
||||
elem.removeEventListener(transitionEndEventName, onHideAnimationComplete);
|
||||
}
|
||||
|
||||
function onHideAnimationComplete(e) {
|
||||
const elem = e.target;
|
||||
if (elem != osdBottomElement) return;
|
||||
elem.classList.add('hide');
|
||||
dom.removeEventListener(elem, transitionEndEventName, onHideAnimationComplete, {
|
||||
once: true
|
||||
});
|
||||
elem.removeEventListener(transitionEndEventName, onHideAnimationComplete);
|
||||
}
|
||||
|
||||
const _focus = debounce((focusElement) => focusManager.focus(focusElement), 50);
|
||||
@@ -361,9 +360,7 @@ export default function (view) {
|
||||
clearHideAnimationEventListeners(elem);
|
||||
elem.classList.add('videoOsdBottom-hidden');
|
||||
|
||||
dom.addEventListener(elem, transitionEndEventName, onHideAnimationComplete, {
|
||||
once: true
|
||||
});
|
||||
elem.addEventListener(transitionEndEventName, onHideAnimationComplete);
|
||||
currentVisibleMenu = null;
|
||||
toggleSubtitleSync('hide');
|
||||
|
||||
@@ -499,10 +496,10 @@ export default function (view) {
|
||||
icon.classList.remove('fullscreen_exit', 'fullscreen');
|
||||
|
||||
if (playbackManager.isFullscreen(currentPlayer)) {
|
||||
button.setAttribute('title', globalize.translate('ExitFullscreen') + ' (f)');
|
||||
button.setAttribute('title', globalize.translate('ExitFullscreen') + ' (F)');
|
||||
icon.classList.add('fullscreen_exit');
|
||||
} else {
|
||||
button.setAttribute('title', globalize.translate('Fullscreen') + ' (f)');
|
||||
button.setAttribute('title', globalize.translate('Fullscreen') + ' (F)');
|
||||
icon.classList.add('fullscreen');
|
||||
}
|
||||
}
|
||||
@@ -724,7 +721,7 @@ export default function (view) {
|
||||
}
|
||||
|
||||
btnPlayPauseIcon.classList.add(icon);
|
||||
dom.setElementTitle(btnPlayPause, title + ' (k)', title);
|
||||
dom.setElementTitle(btnPlayPause, title + ' (K)', title);
|
||||
}
|
||||
|
||||
function updatePlayerStateInternal(event, player, state) {
|
||||
@@ -873,10 +870,10 @@ export default function (view) {
|
||||
buttonMuteIcon.classList.remove('volume_off', 'volume_up');
|
||||
|
||||
if (isMuted) {
|
||||
buttonMute.setAttribute('title', globalize.translate('Unmute') + ' (m)');
|
||||
buttonMute.setAttribute('title', globalize.translate('Unmute') + ' (M)');
|
||||
buttonMuteIcon.classList.add('volume_off');
|
||||
} else {
|
||||
buttonMute.setAttribute('title', globalize.translate('Mute') + ' (m)');
|
||||
buttonMute.setAttribute('title', globalize.translate('Mute') + ' (M)');
|
||||
buttonMuteIcon.classList.add('volume_up');
|
||||
}
|
||||
|
||||
@@ -1248,6 +1245,7 @@ export default function (view) {
|
||||
}
|
||||
break;
|
||||
case 'k':
|
||||
case 'K':
|
||||
playbackManager.playPause(currentPlayer);
|
||||
showOsd(btnPlayPause);
|
||||
break;
|
||||
@@ -1260,23 +1258,27 @@ export default function (view) {
|
||||
playbackManager.volumeDown(currentPlayer);
|
||||
break;
|
||||
case 'l':
|
||||
case 'L':
|
||||
case 'ArrowRight':
|
||||
case 'Right':
|
||||
playbackManager.fastForward(currentPlayer);
|
||||
showOsd(btnFastForward);
|
||||
break;
|
||||
case 'j':
|
||||
case 'J':
|
||||
case 'ArrowLeft':
|
||||
case 'Left':
|
||||
playbackManager.rewind(currentPlayer);
|
||||
showOsd(btnRewind);
|
||||
break;
|
||||
case 'f':
|
||||
case 'F':
|
||||
if (!e.ctrlKey && !e.metaKey) {
|
||||
playbackManager.toggleFullscreen(currentPlayer);
|
||||
}
|
||||
break;
|
||||
case 'm':
|
||||
case 'M':
|
||||
playbackManager.toggleMute(currentPlayer);
|
||||
break;
|
||||
case 'p':
|
||||
@@ -1385,7 +1387,7 @@ export default function (view) {
|
||||
|
||||
// Create bubble elements if they don't already exist
|
||||
if (chapterThumbContainer) {
|
||||
chapterThumb = chapterThumbContainer.querySelector('.chapterThumb');
|
||||
chapterThumb = chapterThumbContainer.querySelector('.chapterThumbWrapper');
|
||||
chapterThumbText = chapterThumbContainer.querySelector('.chapterThumbText');
|
||||
} else {
|
||||
doFullUpdate = true;
|
||||
@@ -1394,22 +1396,12 @@ export default function (view) {
|
||||
chapterThumbContainer.classList.add('chapterThumbContainer');
|
||||
chapterThumbContainer.style.overflow = 'hidden';
|
||||
|
||||
const chapterThumbWrapper = document.createElement('div');
|
||||
chapterThumbWrapper.classList.add('chapterThumbWrapper');
|
||||
chapterThumbWrapper.style.overflow = 'hidden';
|
||||
chapterThumbWrapper.style.position = 'relative';
|
||||
chapterThumbWrapper.style.width = trickplayInfo.Width + 'px';
|
||||
chapterThumbWrapper.style.height = trickplayInfo.Height + 'px';
|
||||
chapterThumbContainer.appendChild(chapterThumbWrapper);
|
||||
|
||||
chapterThumb = document.createElement('img');
|
||||
chapterThumb.classList.add('chapterThumb');
|
||||
chapterThumb.style.position = 'absolute';
|
||||
chapterThumb.style.width = 'unset';
|
||||
chapterThumb.style.minWidth = 'unset';
|
||||
chapterThumb.style.height = 'unset';
|
||||
chapterThumb.style.minHeight = 'unset';
|
||||
chapterThumbWrapper.appendChild(chapterThumb);
|
||||
chapterThumb = document.createElement('div');
|
||||
chapterThumb.classList.add('chapterThumbWrapper');
|
||||
chapterThumb.style.overflow = 'hidden';
|
||||
chapterThumb.style.width = trickplayInfo.Width + 'px';
|
||||
chapterThumb.style.height = trickplayInfo.Height + 'px';
|
||||
chapterThumbContainer.appendChild(chapterThumb);
|
||||
|
||||
const chapterThumbTextContainer = document.createElement('div');
|
||||
chapterThumbTextContainer.classList.add('chapterThumbTextContainer');
|
||||
@@ -1438,9 +1430,9 @@ export default function (view) {
|
||||
MediaSourceId: mediaSourceId
|
||||
});
|
||||
|
||||
if (chapterThumb.src != imgSrc) chapterThumb.src = imgSrc;
|
||||
chapterThumb.style.left = offsetX + 'px';
|
||||
chapterThumb.style.top = offsetY + 'px';
|
||||
chapterThumb.style.backgroundImage = `url('${imgSrc}')`;
|
||||
chapterThumb.style.backgroundPositionX = offsetX + 'px';
|
||||
chapterThumb.style.backgroundPositionY = offsetY + 'px';
|
||||
|
||||
chapterThumbText.textContent = datetime.getDisplayRunningTime(positionTicks);
|
||||
|
||||
@@ -1832,22 +1824,11 @@ export default function (view) {
|
||||
};
|
||||
|
||||
nowPlayingPositionSlider.getMarkerInfo = function () {
|
||||
const markers = [];
|
||||
|
||||
const item = currentItem;
|
||||
|
||||
// use markers based on chapters
|
||||
if (item?.Chapters?.length) {
|
||||
item.Chapters.forEach(currentChapter => {
|
||||
markers.push({
|
||||
className: 'chapterMarker',
|
||||
name: currentChapter.Name,
|
||||
progress: currentChapter.StartPositionTicks / item.RunTimeTicks
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return markers;
|
||||
return currentItem?.Chapters?.map(currentChapter => ({
|
||||
name: currentChapter.Name,
|
||||
progress: currentChapter.StartPositionTicks / currentItem.RunTimeTicks
|
||||
})) || [];
|
||||
};
|
||||
|
||||
view.querySelector('.btnPreviousTrack').addEventListener('click', function () {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user