mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Integrate branch 'master' into feature/langugae_filters
This commit is contained in:
commit
89bcebc5e3
415 changed files with 23641 additions and 15013 deletions
|
@ -1,55 +0,0 @@
|
||||||
jobs:
|
|
||||||
- job: Build
|
|
||||||
displayName: 'Build'
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
Development:
|
|
||||||
BuildConfiguration: development
|
|
||||||
Production:
|
|
||||||
BuildConfiguration: production
|
|
||||||
|
|
||||||
pool:
|
|
||||||
vmImage: 'ubuntu-latest'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- task: NodeTool@0
|
|
||||||
displayName: 'Install Node'
|
|
||||||
inputs:
|
|
||||||
versionSpec: '20.x'
|
|
||||||
|
|
||||||
- task: Cache@2
|
|
||||||
displayName: 'Cache node_modules'
|
|
||||||
inputs:
|
|
||||||
key: 'npm | package-lock.json'
|
|
||||||
path: 'node_modules'
|
|
||||||
|
|
||||||
- script: 'npm ci --no-audit'
|
|
||||||
displayName: 'Install Dependencies'
|
|
||||||
|
|
||||||
- script: 'npm run build:development'
|
|
||||||
displayName: 'Build Development'
|
|
||||||
condition: eq(variables['BuildConfiguration'], 'development')
|
|
||||||
|
|
||||||
- script: 'npm run build:production'
|
|
||||||
displayName: 'Build Production'
|
|
||||||
condition: eq(variables['BuildConfiguration'], 'production')
|
|
||||||
|
|
||||||
- script: 'test -d dist'
|
|
||||||
displayName: 'Check Build'
|
|
||||||
|
|
||||||
- script: 'mv dist jellyfin-web'
|
|
||||||
displayName: 'Rename Directory'
|
|
||||||
|
|
||||||
- task: ArchiveFiles@2
|
|
||||||
displayName: 'Archive Directory'
|
|
||||||
inputs:
|
|
||||||
rootFolderOrFile: 'jellyfin-web'
|
|
||||||
includeRootFolder: true
|
|
||||||
archiveFile: 'jellyfin-web-$(BuildConfiguration)'
|
|
||||||
|
|
||||||
- task: PublishPipelineArtifact@1
|
|
||||||
displayName: 'Publish Release'
|
|
||||||
inputs:
|
|
||||||
targetPath: '$(Build.SourcesDirectory)/jellyfin-web-$(BuildConfiguration).zip'
|
|
||||||
artifactName: 'jellyfin-web-$(BuildConfiguration)'
|
|
|
@ -1,126 +0,0 @@
|
||||||
jobs:
|
|
||||||
- job: BuildPackage
|
|
||||||
displayName: 'Build Packages'
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
CentOS:
|
|
||||||
BuildConfiguration: centos
|
|
||||||
Debian:
|
|
||||||
BuildConfiguration: debian
|
|
||||||
Fedora:
|
|
||||||
BuildConfiguration: fedora
|
|
||||||
Portable:
|
|
||||||
BuildConfiguration: portable
|
|
||||||
|
|
||||||
pool:
|
|
||||||
vmImage: 'ubuntu-latest'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
|
|
||||||
displayName: Set release version (stable)
|
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
|
||||||
|
|
||||||
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-web-$(BuildConfiguration) deployment'
|
|
||||||
displayName: 'Build Dockerfile'
|
|
||||||
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
|
|
||||||
|
|
||||||
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-web-$(BuildConfiguration)'
|
|
||||||
displayName: 'Run Dockerfile (unstable)'
|
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
|
||||||
|
|
||||||
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-web-$(BuildConfiguration)'
|
|
||||||
displayName: 'Run Dockerfile (stable)'
|
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
|
||||||
|
|
||||||
- task: PublishPipelineArtifact@1
|
|
||||||
displayName: 'Publish Release'
|
|
||||||
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
|
|
||||||
inputs:
|
|
||||||
targetPath: '$(Build.SourcesDirectory)/deployment/dist'
|
|
||||||
artifactName: 'jellyfin-web-$(BuildConfiguration)'
|
|
||||||
|
|
||||||
- task: SSH@0
|
|
||||||
displayName: 'Create target directory on repository server'
|
|
||||||
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
|
|
||||||
inputs:
|
|
||||||
sshEndpoint: repository
|
|
||||||
runOptions: 'inline'
|
|
||||||
inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
|
|
||||||
|
|
||||||
- task: CopyFilesOverSSH@0
|
|
||||||
displayName: 'Upload artifacts to repository server'
|
|
||||||
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
|
|
||||||
inputs:
|
|
||||||
sshEndpoint: repository
|
|
||||||
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
|
|
||||||
contents: '**'
|
|
||||||
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
|
|
||||||
|
|
||||||
- job: BuildDocker
|
|
||||||
displayName: 'Build Docker'
|
|
||||||
|
|
||||||
pool:
|
|
||||||
vmImage: 'ubuntu-latest'
|
|
||||||
|
|
||||||
variables:
|
|
||||||
- name: JellyfinVersion
|
|
||||||
value: 0.0.0
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
|
|
||||||
displayName: Set release version (stable)
|
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
|
||||||
|
|
||||||
- task: Docker@2
|
|
||||||
displayName: 'Push Unstable Image'
|
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
|
||||||
inputs:
|
|
||||||
repository: 'jellyfin/jellyfin-web'
|
|
||||||
command: buildAndPush
|
|
||||||
buildContext: '.'
|
|
||||||
Dockerfile: 'deployment/Dockerfile.docker'
|
|
||||||
containerRegistry: Docker Hub
|
|
||||||
tags: |
|
|
||||||
unstable-$(Build.BuildNumber)
|
|
||||||
unstable
|
|
||||||
|
|
||||||
- task: Docker@2
|
|
||||||
displayName: 'Push Stable Image'
|
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
|
||||||
inputs:
|
|
||||||
repository: 'jellyfin/jellyfin-web'
|
|
||||||
command: buildAndPush
|
|
||||||
buildContext: '.'
|
|
||||||
Dockerfile: 'deployment/Dockerfile.docker'
|
|
||||||
containerRegistry: Docker Hub
|
|
||||||
tags: |
|
|
||||||
stable-$(Build.BuildNumber)
|
|
||||||
$(JellyfinVersion)
|
|
||||||
|
|
||||||
- job: CollectArtifacts
|
|
||||||
displayName: 'Collect Artifacts'
|
|
||||||
dependsOn:
|
|
||||||
- BuildPackage
|
|
||||||
- BuildDocker
|
|
||||||
condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
|
|
||||||
|
|
||||||
pool:
|
|
||||||
vmImage: 'ubuntu-latest'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- task: SSH@0
|
|
||||||
displayName: 'Update Unstable Repository'
|
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
|
||||||
inputs:
|
|
||||||
sshEndpoint: repository
|
|
||||||
runOptions: 'inline'
|
|
||||||
inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable'
|
|
||||||
|
|
||||||
- task: SSH@0
|
|
||||||
displayName: 'Update Stable Repository'
|
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
|
||||||
inputs:
|
|
||||||
sshEndpoint: repository
|
|
||||||
runOptions: 'inline'
|
|
||||||
inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) $(Build.SourceBranch)'
|
|
|
@ -1,16 +0,0 @@
|
||||||
trigger:
|
|
||||||
batch: true
|
|
||||||
branches:
|
|
||||||
include:
|
|
||||||
- '*'
|
|
||||||
tags:
|
|
||||||
include:
|
|
||||||
- '*'
|
|
||||||
pr:
|
|
||||||
branches:
|
|
||||||
include:
|
|
||||||
- '*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- template: azure-pipelines-build.yml
|
|
||||||
- template: azure-pipelines-package.yml
|
|
1
.copr
1
.copr
|
@ -1 +0,0 @@
|
||||||
fedora/
|
|
|
@ -8,5 +8,5 @@ trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
|
|
||||||
[*.json]
|
[*.{json,yaml,yml}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
|
@ -90,6 +90,7 @@ module.exports = {
|
||||||
|
|
||||||
'react/jsx-filename-extension': ['error', { 'extensions': ['.jsx', '.tsx'] }],
|
'react/jsx-filename-extension': ['error', { 'extensions': ['.jsx', '.tsx'] }],
|
||||||
'react/jsx-no-bind': ['error'],
|
'react/jsx-no-bind': ['error'],
|
||||||
|
'react/jsx-no-useless-fragment': ['error'],
|
||||||
'react/jsx-no-constructed-context-values': ['error'],
|
'react/jsx-no-constructed-context-values': ['error'],
|
||||||
'react/no-array-index-key': ['error'],
|
'react/no-array-index-key': ['error'],
|
||||||
|
|
||||||
|
@ -260,7 +261,13 @@ module.exports = {
|
||||||
'ServerNotifications': 'writable',
|
'ServerNotifications': 'writable',
|
||||||
'TaskButton': 'writable',
|
'TaskButton': 'writable',
|
||||||
'UserParentalControlPage': 'writable',
|
'UserParentalControlPage': 'writable',
|
||||||
'Windows': 'readonly'
|
'Windows': 'readonly',
|
||||||
|
// Build time definitions
|
||||||
|
__JF_BUILD_VERSION__: 'readonly',
|
||||||
|
__PACKAGE_JSON_NAME__: 'readonly',
|
||||||
|
__PACKAGE_JSON_VERSION__: 'readonly',
|
||||||
|
__USE_SYSTEM_FONTS__: 'readonly',
|
||||||
|
__WEBPACK_SERVE__: 'readonly'
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/prefer-string-starts-ends-with': ['error']
|
'@typescript-eslint/prefer-string-starts-ends-with': ['error']
|
||||||
|
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -1,2 +1 @@
|
||||||
* @jellyfin/web
|
* @jellyfin/web
|
||||||
.github @jellyfin/core
|
|
||||||
|
|
17
.github/workflows/build.yml
vendored
17
.github/workflows/build.yml
vendored
|
@ -18,10 +18,10 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Setup node environment
|
- name: Setup node environment
|
||||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
@ -31,10 +31,17 @@ jobs:
|
||||||
run: npm ci --no-audit
|
run: npm ci --no-audit
|
||||||
|
|
||||||
- name: Run a production build
|
- name: Run a production build
|
||||||
|
env:
|
||||||
|
JELLYFIN_VERSION: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
run: npm run build:production
|
run: npm run build:production
|
||||||
|
|
||||||
|
- name: Update config.json for testing
|
||||||
|
run: |
|
||||||
|
jq '.multiserver=true | .servers=["https://demo.jellyfin.org/unstable"]' dist/config.json > dist/config.tmp.json
|
||||||
|
mv dist/config.tmp.json dist/config.json
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v3.1.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: jellyfin-web__prod
|
name: jellyfin-web__prod
|
||||||
path: |
|
path: |
|
||||||
|
@ -51,13 +58,13 @@ jobs:
|
||||||
- name: Save PR context
|
- name: Save PR context
|
||||||
env:
|
env:
|
||||||
PR_NUMBER: ${{ github.event.number }}
|
PR_NUMBER: ${{ github.event.number }}
|
||||||
PR_SHA: ${{ github.sha }}
|
PR_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
run: |
|
run: |
|
||||||
echo $PR_NUMBER > PR_number
|
echo $PR_NUMBER > PR_number
|
||||||
echo $PR_SHA > PR_sha
|
echo $PR_SHA > PR_sha
|
||||||
|
|
||||||
- name: Upload PR number as artifact
|
- name: Upload PR number as artifact
|
||||||
uses: actions/upload-artifact@v3.1.3
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
name: PR_context
|
name: PR_context
|
||||||
path: |
|
path: |
|
||||||
|
|
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
|
@ -19,16 +19,16 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3
|
uses: github/codeql-action/init@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
|
||||||
with:
|
with:
|
||||||
languages: javascript
|
languages: javascript
|
||||||
queries: +security-extended
|
queries: +security-extended
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3
|
uses: github/codeql-action/autobuild@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3
|
uses: github/codeql-action/analyze@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
|
||||||
|
|
6
.github/workflows/commands.yml
vendored
6
.github/workflows/commands.yml
vendored
|
@ -12,13 +12,13 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Notify as seen
|
- name: Notify as seen
|
||||||
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
|
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
comment-id: ${{ github.event.comment.id }}
|
comment-id: ${{ github.event.comment.id }}
|
||||||
reactions: '+1'
|
reactions: '+1'
|
||||||
- name: Checkout the latest code
|
- name: Checkout the latest code
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
@ -28,7 +28,7 @@ jobs:
|
||||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
- name: Comment on failure
|
- name: Comment on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
|
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
issue-number: ${{ github.event.issue.number }}
|
issue-number: ${{ github.event.issue.number }}
|
||||||
|
|
38
.github/workflows/pr-suggestions.yml
vendored
Normal file
38
.github/workflows/pr-suggestions.yml
vendored
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
name: PR suggestions
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.id || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
branches: [ master, release* ]
|
||||||
|
types:
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-eslint:
|
||||||
|
name: Run eslint suggestions
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out Git repository
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
|
- name: Setup node environment
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
check-latest: true
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install Node.js dependencies
|
||||||
|
run: npm ci --no-audit
|
||||||
|
|
||||||
|
- name: Run eslint
|
||||||
|
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||||
|
uses: CatChen/eslint-suggestion-action@8fb7db4e235f7af9fc434349a124034b681d99a3 # v3.1.3
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
8
.github/workflows/publish.yml
vendored
8
.github/workflows/publish.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download workflow artifact
|
- name: Download workflow artifact
|
||||||
uses: dawidd6/action-download-artifact@v2.27.0
|
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
|
||||||
with:
|
with:
|
||||||
run_id: ${{ github.event.workflow_run.id }}
|
run_id: ${{ github.event.workflow_run.id }}
|
||||||
name: jellyfin-web__prod
|
name: jellyfin-web__prod
|
||||||
|
@ -28,7 +28,7 @@ jobs:
|
||||||
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
id: cf
|
id: cf
|
||||||
uses: cloudflare/pages-action@1
|
uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # 1
|
||||||
with:
|
with:
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
@ -47,7 +47,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Get PR context
|
- name: Get PR context
|
||||||
uses: dawidd6/action-download-artifact@v2.27.0
|
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
|
||||||
id: pr_context
|
id: pr_context
|
||||||
with:
|
with:
|
||||||
run_id: ${{ github.event.workflow_run.id }}
|
run_id: ${{ github.event.workflow_run.id }}
|
||||||
|
@ -88,7 +88,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Update job summary in PR comment
|
- name: Update job summary in PR comment
|
||||||
uses: thollander/actions-comment-pull-request@v2.4.2
|
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
message: ${{ needs.compose-comment.outputs.msg }}
|
message: ${{ needs.compose-comment.outputs.msg }}
|
||||||
|
|
52
.github/workflows/quality.yml
vendored
52
.github/workflows/quality.yml
vendored
|
@ -17,10 +17,10 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Setup node environment
|
- name: Setup node environment
|
||||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
@ -41,10 +41,10 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Setup node environment
|
- name: Setup node environment
|
||||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
@ -54,18 +54,18 @@ jobs:
|
||||||
run: npm ci --no-audit
|
run: npm ci --no-audit
|
||||||
|
|
||||||
- name: Run eslint
|
- name: Run eslint
|
||||||
run: npm run lint
|
run: npx eslint --quiet "."
|
||||||
|
|
||||||
run-stylelint-css:
|
run-stylelint:
|
||||||
name: Run stylelint (css)
|
name: Run stylelint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Setup node environment
|
- name: Setup node environment
|
||||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
@ -78,31 +78,7 @@ jobs:
|
||||||
run: npm ci --no-audit
|
run: npm ci --no-audit
|
||||||
|
|
||||||
- name: Run stylelint
|
- name: Run stylelint
|
||||||
run: npm run stylelint:css
|
run: npm run stylelint
|
||||||
|
|
||||||
run-stylelint-scss:
|
|
||||||
name: Run stylelint (scss)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out Git repository
|
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
|
||||||
|
|
||||||
- name: Setup node environment
|
|
||||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
check-latest: true
|
|
||||||
cache: npm
|
|
||||||
|
|
||||||
- name: Set up stylelint matcher
|
|
||||||
uses: xt0rted/stylelint-problem-matcher@34db1b874c0452909f0696aedef70b723870a583 # tag=v1
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: npm ci --no-audit
|
|
||||||
|
|
||||||
- name: Run stylelint
|
|
||||||
run: npm run stylelint:scss
|
|
||||||
|
|
||||||
run-tsc:
|
run-tsc:
|
||||||
name: Run TypeScript build check
|
name: Run TypeScript build check
|
||||||
|
@ -110,10 +86,10 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Setup node environment
|
- name: Setup node environment
|
||||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
@ -131,10 +107,10 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Setup node environment
|
- name: Setup node environment
|
||||||
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
|
@ -15,7 +15,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
|
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
operations-per-run: 75
|
operations-per-run: 75
|
||||||
|
@ -37,7 +37,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
|
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
operations-per-run: 75
|
operations-per-run: 75
|
||||||
|
|
52
.github/workflows/update-sdk.yml
vendored
Normal file
52
.github/workflows/update-sdk.yml
vendored
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
name: Update the Jellyfin SDK
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 7 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: unstable-sdk-pr
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out Git repository
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
check-latest: true
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install latest unstable SDK
|
||||||
|
run: |
|
||||||
|
npm i --save @jellyfin/sdk@unstable
|
||||||
|
VERSION=$(jq -r '.dependencies["@jellyfin/sdk"]' package.json)
|
||||||
|
echo "JF_SDK_VERSION=${VERSION}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Open a pull request
|
||||||
|
uses: peter-evans/create-pull-request@a4f52f8033a6168103c2538976c07b467e8163bc # v6.0.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
commit-message: Update @jellyfin/sdk to ${{env.JF_SDK_VERSION}}
|
||||||
|
committer: jellyfin-bot <team@jellyfin.org>
|
||||||
|
author: jellyfin-bot <team@jellyfin.org>
|
||||||
|
branch: update-jf-sdk
|
||||||
|
delete-branch: true
|
||||||
|
title: Update @jellyfin/sdk to ${{env.JF_SDK_VERSION}}
|
||||||
|
body: |
|
||||||
|
**Changes**
|
||||||
|
Updates to the latest unstable @jellyfin/sdk build
|
||||||
|
labels: |
|
||||||
|
dependencies
|
||||||
|
npm
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -8,6 +8,7 @@ config.json
|
||||||
|
|
||||||
# ide
|
# ide
|
||||||
.idea
|
.idea
|
||||||
|
.vs
|
||||||
|
|
||||||
# log
|
# log
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|
3
.sonarcloud.properties
Normal file
3
.sonarcloud.properties
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Exclude test files from Sonar sources
|
||||||
|
# See: https://docs.sonarcloud.io/advanced-setup/analysis-scope/#file-exclusion-and-inclusion
|
||||||
|
sonar.exclusions=src/**/*.test.js,src/**/*.test.ts
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"stylelint-no-browser-hacks/lib"
|
"stylelint-no-browser-hacks/lib"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"at-rule-empty-line-before": [ "always", {
|
"at-rule-empty-line-before": [ "always", {
|
||||||
"except": [
|
"except": [
|
||||||
|
@ -143,5 +143,20 @@
|
||||||
"value-list-comma-space-after": "always-single-line",
|
"value-list-comma-space-after": "always-single-line",
|
||||||
"value-list-comma-space-before": "never",
|
"value-list-comma-space-before": "never",
|
||||||
"value-list-max-empty-lines": 0
|
"value-list-max-empty-lines": 0
|
||||||
}
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.scss",
|
||||||
|
"**/*.scss"
|
||||||
|
],
|
||||||
|
"customSyntax": "postcss-scss",
|
||||||
|
"plugins": [ "stylelint-scss" ],
|
||||||
|
"rules": {
|
||||||
|
"at-rule-no-unknown": null,
|
||||||
|
"scss/at-rule-no-unknown": true,
|
||||||
|
"plugin/no-browser-hacks": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"extends": [ "./.stylelintrc.json" ],
|
|
||||||
"customSyntax": "postcss-scss",
|
|
||||||
"plugins": [ "stylelint-scss" ],
|
|
||||||
"rules": {
|
|
||||||
"at-rule-no-unknown": null,
|
|
||||||
"scss/at-rule-no-unknown": true,
|
|
||||||
"plugin/no-browser-hacks": null
|
|
||||||
}
|
|
||||||
}
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": "explicit"
|
||||||
},
|
},
|
||||||
"eslint.format.enable": true,
|
"eslint.format.enable": true,
|
||||||
"editor.formatOnSave": false
|
"editor.formatOnSave": false
|
||||||
|
|
282
CONTRIBUTORS.md
282
CONTRIBUTORS.md
|
@ -1,138 +1,152 @@
|
||||||
# Jellyfin Contributors
|
# Jellyfin Contributors
|
||||||
|
|
||||||
- [JoshuaBoniface](https://github.com/joshuaboniface)
|
- [JoshuaBoniface](https://github.com/joshuaboniface)
|
||||||
- [nvllsvm](https://github.com/nvllsvm)
|
- [nvllsvm](https://github.com/nvllsvm)
|
||||||
- [JustAMan](https://github.com/JustAMan)
|
- [JustAMan](https://github.com/JustAMan)
|
||||||
- [dcrdev](https://github.com/dcrdev)
|
- [dcrdev](https://github.com/dcrdev)
|
||||||
- [EraYaN](https://github.com/EraYaN)
|
- [EraYaN](https://github.com/EraYaN)
|
||||||
- [flemse](https://github.com/flemse)
|
- [flemse](https://github.com/flemse)
|
||||||
- [bfayers](https://github.com/bfayers)
|
- [bfayers](https://github.com/bfayers)
|
||||||
- [Bond_009](https://github.com/Bond-009)
|
- [Bond_009](https://github.com/Bond-009)
|
||||||
- [AnthonyLavado](https://github.com/anthonylavado)
|
- [AnthonyLavado](https://github.com/anthonylavado)
|
||||||
- [dkanada](https://github.com/dkanada)
|
- [dkanada](https://github.com/dkanada)
|
||||||
- [sparky8251](https://github.com/sparky8251)
|
- [sparky8251](https://github.com/sparky8251)
|
||||||
- [LeoVerto](https://github.com/LeoVerto)
|
- [LeoVerto](https://github.com/LeoVerto)
|
||||||
- [cvium](https://github.com/cvium)
|
- [cvium](https://github.com/cvium)
|
||||||
- [grafixeyehero](https://github.com/grafixeyehero)
|
- [grafixeyehero](https://github.com/grafixeyehero)
|
||||||
- [Drago96](https://github.com/drago-96)
|
- [Drago96](https://github.com/drago-96)
|
||||||
- [ViXXoR](https://github.com/ViXXoR)
|
- [ViXXoR](https://github.com/ViXXoR)
|
||||||
- [nkmerrill](https://github.com/nkmerrill)
|
- [nkmerrill](https://github.com/nkmerrill)
|
||||||
- [TtheCreator](https://github.com/Tthecreator)
|
- [TtheCreator](https://github.com/Tthecreator)
|
||||||
- [RazeLighter777](https://github.com/RazeLighter777)
|
- [RazeLighter777](https://github.com/RazeLighter777)
|
||||||
- [LogicalPhallacy](https://github.com/LogicalPhallacy)
|
- [LogicalPhallacy](https://github.com/LogicalPhallacy)
|
||||||
- [thornbill](https://github.com/thornbill)
|
- [thornbill](https://github.com/thornbill)
|
||||||
- [redSpoutnik](https://github.com/redSpoutnik)
|
- [redSpoutnik](https://github.com/redSpoutnik)
|
||||||
- [DrPandemic](https://github.com/drpandemic)
|
- [DrPandemic](https://github.com/drpandemic)
|
||||||
- [Oddstr13](https://github.com/oddstr13)
|
- [Oddstr13](https://github.com/oddstr13)
|
||||||
- [petermcneil](https://github.com/petermcneil)
|
- [petermcneil](https://github.com/petermcneil)
|
||||||
- [lewazo](https://github.com/lewazo)
|
- [lewazo](https://github.com/lewazo)
|
||||||
- [Raghu Saxena](https://github.com/ckcr4lyf)
|
- [Raghu Saxena](https://github.com/ckcr4lyf)
|
||||||
- [Nickbert7](https://github.com/Nickbert7)
|
- [Nickbert7](https://github.com/Nickbert7)
|
||||||
- [ferferga](https://github.com/ferferga)
|
- [ferferga](https://github.com/ferferga)
|
||||||
- [bilde2910](https://github.com/bilde2910)
|
- [bilde2910](https://github.com/bilde2910)
|
||||||
- [Daniel Hartung](https://github.com/dhartung)
|
- [Daniel Hartung](https://github.com/dhartung)
|
||||||
- [Ryan Hartzell](https://github.com/ryan-hartzell)
|
- [Ryan Hartzell](https://github.com/ryan-hartzell)
|
||||||
- [Thibault Nocchi](https://github.com/ThibaultNocchi)
|
- [Thibault Nocchi](https://github.com/ThibaultNocchi)
|
||||||
- [MrTimscampi](https://github.com/MrTimscampi)
|
- [MrTimscampi](https://github.com/MrTimscampi)
|
||||||
- [artiume](https://github.com/Artiume)
|
- [artiume](https://github.com/Artiume)
|
||||||
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
|
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
|
||||||
- [Sarab Singh](https://github.com/sarab97)
|
- [Sarab Singh](https://github.com/sarab97)
|
||||||
- [DesertCookie](https://github.com/desertcookie)
|
- [DesertCookie](https://github.com/desertcookie)
|
||||||
- [GuilhermeHideki](https://github.com/GuilhermeHideki)
|
- [GuilhermeHideki](https://github.com/GuilhermeHideki)
|
||||||
- [Andrei Oanca](https://github.com/OancaAndrei)
|
- [Andrei Oanca](https://github.com/OancaAndrei)
|
||||||
- [Cromefire_](https://github.com/cromefire)
|
- [Cromefire_](https://github.com/cromefire)
|
||||||
- [Orry Verducci](https://github.com/orryverducci)
|
- [Orry Verducci](https://github.com/orryverducci)
|
||||||
- [Camc314](https://github.com/camc314)
|
- [Camc314](https://github.com/camc314)
|
||||||
- [danieladov](https://github.com/danieladov)
|
- [danieladov](https://github.com/danieladov)
|
||||||
- [Stephane Senart](https://github.com/ssenart)
|
- [Stephane Senart](https://github.com/ssenart)
|
||||||
- [imchasingshadows](https://github.com/imchasingshadows)
|
- [imchasingshadows](https://github.com/imchasingshadows)
|
||||||
- [Ömer Erdinç Yağmurlu](https://github.com/omeryagmurlu)
|
- [Ömer Erdinç Yağmurlu](https://github.com/omeryagmurlu)
|
||||||
- [Keegan Dahm](https://github.com/keegandahm)
|
- [Keegan Dahm](https://github.com/keegandahm)
|
||||||
- [GodTamIt](https://github.com/GodTamIt)
|
- [GodTamIt](https://github.com/GodTamIt)
|
||||||
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
||||||
- [Matthew Jones](https://github.com/matthew-jones-uk)
|
- [Matthew Jones](https://github.com/matthew-jones-uk)
|
||||||
- [taku0](https://github.com/taku0)
|
- [taku0](https://github.com/taku0)
|
||||||
- [Viperinius](https://github.com/Viperinius)
|
- [Viperinius](https://github.com/Viperinius)
|
||||||
- [is343](https://github.com/is343)
|
- [is343](https://github.com/is343)
|
||||||
- [Meet Pandya](https://github.com/meet-k-pandya)
|
- [Meet Pandya](https://github.com/meet-k-pandya)
|
||||||
- [Peter Spenler](https://github.com/peterspenler)
|
- [Peter Spenler](https://github.com/peterspenler)
|
||||||
- [Vankerkom](https://github.com/vankerkom)
|
- [jomp16](https://github.com/jomp16)
|
||||||
- [edvwib](https://github.com/edvwib)
|
- [Leon de Klerk](https://github.com/leondeklerk)
|
||||||
- [Rob Farraher](https://github.com/farraherbg)
|
- [CrispyBaguette](https://github.com/CrispyBaguette)
|
||||||
- [TelepathicWalrus](https://github.com/TelepathicWalrus)
|
- [Vankerkom](https://github.com/vankerkom)
|
||||||
- [Pier-Luc Ducharme](https://github.com/pl-ducharme)
|
- [edvwib](https://github.com/edvwib)
|
||||||
- [Anantharaju S](https://github.com/Anantharajus)
|
- [Rob Farraher](https://github.com/farraherbg)
|
||||||
- [Merlin Sievers](https://github.com/dann-merlin)
|
- [TelepathicWalrus](https://github.com/TelepathicWalrus)
|
||||||
- [Fishbigger](https://github.com/fishbigger)
|
- [Pier-Luc Ducharme](https://github.com/pl-ducharme)
|
||||||
- [sleepycatcoding](https://github.com/sleepycatcoding)
|
- [Anantharaju S](https://github.com/Anantharajus)
|
||||||
- [TheMelmacian](https://github.com/TheMelmacian)
|
- [Merlin Sievers](https://github.com/dann-merlin)
|
||||||
- [v0idMrK](https://github.com/v0idMrK)
|
- [Fishbigger](https://github.com/fishbigger)
|
||||||
- [tehciolo](https://github.com/tehciolo)
|
- [sleepycatcoding](https://github.com/sleepycatcoding)
|
||||||
- [scampower3](https://github.com/scampower3)
|
- [TheMelmacian](https://github.com/TheMelmacian)
|
||||||
- [LittleBigOwI] (https://github.com/LittleBigOwI/)
|
- [v0idMrK](https://github.com/v0idMrK)
|
||||||
|
- [tehciolo](https://github.com/tehciolo)
|
||||||
|
- [scampower3](https://github.com/scampower3)
|
||||||
|
- [LittleBigOwI](https://github.com/LittleBigOwI/)
|
||||||
|
- [Nate G](https://github.com/GGProGaming)
|
||||||
|
- [Grady Hallenbeck](https://github.com/grhallenbeck)
|
||||||
|
- [DinuD](https://github.com/DinuD)
|
||||||
|
- [Kevin Tan (Valius)](https://github.com/valius)
|
||||||
|
- [Rasmus Krämer](https://github.com/rasmuslos)
|
||||||
|
- [ntarelix](https://github.com/ntarelix)
|
||||||
|
- [András Maróy](https://github.com/andrasmaroy)
|
||||||
|
- [Chris-Codes-It](https://github.com/Chris-Codes-It)
|
||||||
|
- [Vedant](https://github.com/viktory36)
|
||||||
|
|
||||||
# Emby Contributors
|
## Emby Contributors
|
||||||
|
|
||||||
- [LukePulverenti](https://github.com/LukePulverenti)
|
- [LukePulverenti](https://github.com/LukePulverenti)
|
||||||
- [ebr11](https://github.com/ebr11)
|
- [ebr11](https://github.com/ebr11)
|
||||||
- [lalmanzar](https://github.com/lalmanzar)
|
- [lalmanzar](https://github.com/lalmanzar)
|
||||||
- [schneifu](https://github.com/schneifu)
|
- [schneifu](https://github.com/schneifu)
|
||||||
- [Mark2xv](https://github.com/Mark2xv)
|
- [Mark2xv](https://github.com/Mark2xv)
|
||||||
- [ScottRapsey](https://github.com/ScottRapsey)
|
- [ScottRapsey](https://github.com/ScottRapsey)
|
||||||
- [skynet600](https://github.com/skynet600)
|
- [skynet600](https://github.com/skynet600)
|
||||||
- [Cheesegeezer](https://githum.com/Cheesegeezer)
|
- [Cheesegeezer](https://githum.com/Cheesegeezer)
|
||||||
- [Radeon](https://github.com/radeonorama)
|
- [Radeon](https://github.com/radeonorama)
|
||||||
- [gcw07](https://github.com/gcw07)
|
- [gcw07](https://github.com/gcw07)
|
||||||
- [SivaramAdhiappan](https://github.com/shivaram1190)
|
- [SivaramAdhiappan](https://github.com/shivaram1190)
|
||||||
- [CWatkinsNash](https://github.com/CWatkinsNash)
|
- [CWatkinsNash](https://github.com/CWatkinsNash)
|
||||||
- [sfnetwork](https://github.com/sfnetwork)
|
- [sfnetwork](https://github.com/sfnetwork)
|
||||||
- [Logos302](https://github.com/Logos302)
|
- [Logos302](https://github.com/Logos302)
|
||||||
- [TheWorkz](https://github.com/TheWorkz)
|
- [TheWorkz](https://github.com/TheWorkz)
|
||||||
- [mboehler](https://github.com/mboehler)
|
- [mboehler](https://github.com/mboehler)
|
||||||
- [KaHooli](https://github.com/KaHooli)
|
- [KaHooli](https://github.com/KaHooli)
|
||||||
- [xzener](https://github.com/xzener)
|
- [xzener](https://github.com/xzener)
|
||||||
- [CBers](https://github.com/CBers)
|
- [CBers](https://github.com/CBers)
|
||||||
- [Sagaia](https://github.com/Sagaia)
|
- [Sagaia](https://github.com/Sagaia)
|
||||||
- [JHawk111](https://github.com/JHawk111)
|
- [JHawk111](https://github.com/JHawk111)
|
||||||
- [David3663](https://github.com/david3663)
|
- [David3663](https://github.com/david3663)
|
||||||
- [Smyken](https://github.com/Smyken)
|
- [Smyken](https://github.com/Smyken)
|
||||||
- [doron1](https://github.com/doron1)
|
- [doron1](https://github.com/doron1)
|
||||||
- [brainfryd](https://github.com/brainfryd)
|
- [brainfryd](https://github.com/brainfryd)
|
||||||
- [DGMayor](http://github.com/DGMayor)
|
- [DGMayor](http://github.com/DGMayor)
|
||||||
- [Jon-theHTPC](https://github.com/Jon-theHTPC)
|
- [Jon-theHTPC](https://github.com/Jon-theHTPC)
|
||||||
- [aspdend](https://github.com/aspdend)
|
- [aspdend](https://github.com/aspdend)
|
||||||
- [RedshirtMB](https://github.com/RedshirtMB)
|
- [RedshirtMB](https://github.com/RedshirtMB)
|
||||||
- [thealienamongus](https://github.com/thealienamongus)
|
- [thealienamongus](https://github.com/thealienamongus)
|
||||||
- [brocass](https://github.com/brocass)
|
- [brocass](https://github.com/brocass)
|
||||||
- [pjrollo2000](https://github.com/pjrollo2000)
|
- [pjrollo2000](https://github.com/pjrollo2000)
|
||||||
- [abobader](https://github.com/abobader)
|
- [abobader](https://github.com/abobader)
|
||||||
- [milli260876](https://github.com/milli260876)
|
- [milli260876](https://github.com/milli260876)
|
||||||
- [vileboy](https://github.com/vileboy)
|
- [vileboy](https://github.com/vileboy)
|
||||||
- [starkadius](https://github.com/starkadius)
|
- [starkadius](https://github.com/starkadius)
|
||||||
- [wraslor](https://github.com/wraslor)
|
- [wraslor](https://github.com/wraslor)
|
||||||
- [mrwebsmith](https://github.com/mrwebsmith)
|
- [mrwebsmith](https://github.com/mrwebsmith)
|
||||||
- [rickster53](https://github.com/rickster53)
|
- [rickster53](https://github.com/rickster53)
|
||||||
- [Tharnax](https://github.com/Tharnax)
|
- [Tharnax](https://github.com/Tharnax)
|
||||||
- [0sm0](https://github.com/0sm0)
|
- [0sm0](https://github.com/0sm0)
|
||||||
- [swhitmore](https://github.com/swhitmore)
|
- [swhitmore](https://github.com/swhitmore)
|
||||||
- [DigiTM](https://github.com/DigiTM)
|
- [DigiTM](https://github.com/DigiTM)
|
||||||
- [crisliv / xliv](https://github.com/crisliv)
|
- [crisliv / xliv](https://github.com/crisliv)
|
||||||
- [Yogi](https://github.com/yogi12)
|
- [Yogi](https://github.com/yogi12)
|
||||||
- [madFloyd](https://github.com/madFloyd)
|
- [madFloyd](https://github.com/madFloyd)
|
||||||
- [yardameus](https://github.com/yardameus)
|
- [yardameus](https://github.com/yardameus)
|
||||||
- [rrb008](https://github.com/rrb008)
|
- [rrb008](https://github.com/rrb008)
|
||||||
- [Toonguy](https://github.com/Toonguy)
|
- [Toonguy](https://github.com/Toonguy)
|
||||||
- [Alwin Hummels](https://github.com/AlwinHummels)
|
- [Alwin Hummels](https://github.com/AlwinHummels)
|
||||||
- [trooper11](https://github.com/trooper11)
|
- [trooper11](https://github.com/trooper11)
|
||||||
- [danlotfy](https://github.com/danlotfy)
|
- [danlotfy](https://github.com/danlotfy)
|
||||||
- [jordy1955](https://github.com/jordy1955)
|
- [jordy1955](https://github.com/jordy1955)
|
||||||
- [JoshFink](https://github.com/JoshFink)
|
- [JoshFink](https://github.com/JoshFink)
|
||||||
- [Detector1](https://github.com/Detector1)
|
- [Detector1](https://github.com/Detector1)
|
||||||
- [BlackIce013](https://github.com/blackice013)
|
- [BlackIce013](https://github.com/blackice013)
|
||||||
- [mporcas](https://github.com/mporcas)
|
- [mporcas](https://github.com/mporcas)
|
||||||
- [tikuf](https://github.com/tikuf/)
|
- [tikuf](https://github.com/tikuf/)
|
||||||
- [Tim Hobbs](https://github.com/timhobbs)
|
- [Tim Hobbs](https://github.com/timhobbs)
|
||||||
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
|
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
|
||||||
- [jomp16](https://github.com/jomp16)
|
<!--
|
||||||
- [Leon de Klerk](https://github.com/leondeklerk)
|
NOTE: This is the end of the list of past Emby Contributors.
|
||||||
- [CrispyBaguette](https://github.com/CrispyBaguette)
|
New Jellyfin contributors should add their name to the end
|
||||||
|
of the list of Jellyfin Contributors above. NOT HERE ;)
|
||||||
|
-->
|
||||||
|
|
110
build.sh
110
build.sh
|
@ -1,110 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# build.sh - Build Jellyfin binary packages
|
|
||||||
# Part of the Jellyfin Project
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o pipefail
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo -e "build.sh - Build Jellyfin binary packages"
|
|
||||||
echo -e "Usage:"
|
|
||||||
echo -e " $0 -t/--type <BUILD_TYPE> -p/--platform <PLATFORM> [-k/--keep-artifacts] [-l/--list-platforms]"
|
|
||||||
echo -e "Notes:"
|
|
||||||
echo -e " * BUILD_TYPE can be one of: [native, docker] and must be specified"
|
|
||||||
echo -e " * native: Build using the build script in the host OS"
|
|
||||||
echo -e " * docker: Build using the build script in a standardized Docker container"
|
|
||||||
echo -e " * PLATFORM can be any platform shown by -l/--list-platforms and must be specified"
|
|
||||||
echo -e " * If -k/--keep-artifacts is specified, transient artifacts (e.g. Docker containers) will be"
|
|
||||||
echo -e " retained after the build is finished; the source directory will still be cleaned"
|
|
||||||
echo -e " * If -l/--list-platforms is specified, all other arguments are ignored; the script will print"
|
|
||||||
echo -e " the list of supported platforms and exit"
|
|
||||||
}
|
|
||||||
|
|
||||||
list_platforms() {
|
|
||||||
declare -a platforms
|
|
||||||
platforms=(
|
|
||||||
$( find deployment -maxdepth 1 -mindepth 1 -name "build.*" | awk -F'.' '{ $1=""; printf $2; if ($3 != ""){ printf "." $3; }; if ($4 != ""){ printf "." $4; }; print ""; }' | sort )
|
|
||||||
)
|
|
||||||
echo -e "Valid platforms:"
|
|
||||||
echo
|
|
||||||
for platform in ${platforms[@]}; do
|
|
||||||
echo -e "* ${platform} : $( grep '^#=' deployment/build.${platform} | sed 's/^#= //' )"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
do_build_native() {
|
|
||||||
export IS_DOCKER=NO
|
|
||||||
deployment/build.${PLATFORM}
|
|
||||||
}
|
|
||||||
|
|
||||||
do_build_docker() {
|
|
||||||
if ! [ $(uname -m) = "x86_64" ]; then
|
|
||||||
echo "Docker-based builds only support amd64-based cross-building; use a 'native' build instead."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -f deployment/Dockerfile.${PLATFORM} ]]; then
|
|
||||||
echo "Missing Dockerfile for platform ${PLATFORM}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ${KEEP_ARTIFACTS} == YES ]]; then
|
|
||||||
docker_args=""
|
|
||||||
else
|
|
||||||
docker_args="--rm"
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker build . -t "jellyfin-builder.${PLATFORM}" -f deployment/Dockerfile.${PLATFORM}
|
|
||||||
mkdir -p ${ARTIFACT_DIR}
|
|
||||||
docker run $docker_args -v "${SOURCE_DIR}:/jellyfin" -v "${ARTIFACT_DIR}:/dist" "jellyfin-builder.${PLATFORM}"
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
key="$1"
|
|
||||||
case $key in
|
|
||||||
-t|--type)
|
|
||||||
BUILD_TYPE="$2"
|
|
||||||
shift
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-p|--platform)
|
|
||||||
PLATFORM="$2"
|
|
||||||
shift
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-k|--keep-artifacts)
|
|
||||||
KEEP_ARTIFACTS=YES
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-l|--list-platforms)
|
|
||||||
list_platforms
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option $1"
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z ${BUILD_TYPE} || -z ${PLATFORM} ]]; then
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export SOURCE_DIR="$( pwd )"
|
|
||||||
export ARTIFACT_DIR="${SOURCE_DIR}/../bin/${PLATFORM}"
|
|
||||||
|
|
||||||
# Determine build type
|
|
||||||
case ${BUILD_TYPE} in
|
|
||||||
native)
|
|
||||||
do_build_native
|
|
||||||
;;
|
|
||||||
docker)
|
|
||||||
do_build_docker
|
|
||||||
;;
|
|
||||||
esac
|
|
|
@ -1,9 +0,0 @@
|
||||||
---
|
|
||||||
# We just wrap `build` so this is really it
|
|
||||||
name: "jellyfin-web"
|
|
||||||
version: "10.8.0"
|
|
||||||
packages:
|
|
||||||
- debian.all
|
|
||||||
- fedora.all
|
|
||||||
- centos.all
|
|
||||||
- portable
|
|
67
bump_version
67
bump_version
|
@ -7,7 +7,7 @@ set -o pipefail
|
||||||
set -o xtrace
|
set -o xtrace
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
echo -e "bump_version - increase the shared version and generate changelogs"
|
echo -e "bump_version - increase the shared version"
|
||||||
echo -e ""
|
echo -e ""
|
||||||
echo -e "Usage:"
|
echo -e "Usage:"
|
||||||
echo -e " $ bump_version <new_version>"
|
echo -e " $ bump_version <new_version>"
|
||||||
|
@ -18,75 +18,12 @@ if [[ -z $1 ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
build_file="./build.yaml"
|
|
||||||
package_file="./package*.json"
|
|
||||||
|
|
||||||
new_version="$1"
|
new_version="$1"
|
||||||
|
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
|
||||||
old_version="$(
|
|
||||||
grep "version:" ${build_file} \
|
|
||||||
| sed -E 's/version: "([0-9\.]+[-a-z0-9]*)"/\1/'
|
|
||||||
)"
|
|
||||||
echo "Old version: ${old_version}"
|
|
||||||
|
|
||||||
# Bump the NPM version
|
# Bump the NPM version
|
||||||
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
|
|
||||||
npm --no-git-tag-version --allow-same-version version v${new_version_sed}
|
npm --no-git-tag-version --allow-same-version version v${new_version_sed}
|
||||||
|
|
||||||
# Set the build.yaml version to the specified new_version
|
|
||||||
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
|
|
||||||
sed -i "s/${old_version_sed}/${new_version_sed}/g" ${build_file}
|
|
||||||
|
|
||||||
|
|
||||||
if [[ ${new_version} == *"-"* ]]; then
|
|
||||||
new_version_pkg="$( sed 's/-/~/g' <<<"${new_version}" )"
|
|
||||||
new_version_deb_sup=""
|
|
||||||
else
|
|
||||||
new_version_pkg="${new_version}"
|
|
||||||
new_version_deb_sup="-1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Write out a temporary Debian changelog with our new stuff appended and some templated formatting
|
|
||||||
debian_changelog_file="debian/changelog"
|
|
||||||
debian_changelog_temp="$( mktemp )"
|
|
||||||
# Create new temp file with our changelog
|
|
||||||
echo -e "jellyfin-web (${new_version_pkg}${new_version_deb_sup}) unstable; urgency=medium
|
|
||||||
|
|
||||||
* New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v${new_version}
|
|
||||||
|
|
||||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
|
|
||||||
" >> ${debian_changelog_temp}
|
|
||||||
cat ${debian_changelog_file} >> ${debian_changelog_temp}
|
|
||||||
# Move into place
|
|
||||||
mv ${debian_changelog_temp} ${debian_changelog_file}
|
|
||||||
|
|
||||||
# Write out a temporary Yum changelog with our new stuff prepended and some templated formatting
|
|
||||||
fedora_spec_file="fedora/jellyfin-web.spec"
|
|
||||||
fedora_changelog_temp="$( mktemp )"
|
|
||||||
fedora_spec_temp_dir="$( mktemp -d )"
|
|
||||||
fedora_spec_temp="${fedora_spec_temp_dir}/jellyfin-web.spec.tmp"
|
|
||||||
# Make a copy of our spec file for hacking
|
|
||||||
cp ${fedora_spec_file} ${fedora_spec_temp_dir}/
|
|
||||||
pushd ${fedora_spec_temp_dir}
|
|
||||||
# Split out the stuff before and after changelog
|
|
||||||
csplit jellyfin-web.spec "/^%changelog/" # produces xx00 xx01
|
|
||||||
# Update the version in xx00
|
|
||||||
sed -i "s/${old_version_sed}/${new_version_pkg}/g" xx00
|
|
||||||
# Remove the header from xx01
|
|
||||||
sed -i '/^%changelog/d' xx01
|
|
||||||
# Create new temp file with our changelog
|
|
||||||
echo -e "%changelog
|
|
||||||
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
|
|
||||||
- New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v${new_version}" >> ${fedora_changelog_temp}
|
|
||||||
cat xx01 >> ${fedora_changelog_temp}
|
|
||||||
# Reassembble
|
|
||||||
cat xx00 ${fedora_changelog_temp} > ${fedora_spec_temp}
|
|
||||||
popd
|
|
||||||
# Move into place
|
|
||||||
mv ${fedora_spec_temp} ${fedora_spec_file}
|
|
||||||
# Clean up
|
|
||||||
rm -rf ${fedora_spec_temp_dir}
|
|
||||||
|
|
||||||
# Stage the changed files for commit
|
# Stage the changed files for commit
|
||||||
git add .
|
git add .
|
||||||
git status -v
|
git status -v
|
||||||
|
|
17
debian/changelog
vendored
17
debian/changelog
vendored
|
@ -1,17 +0,0 @@
|
||||||
jellyfin-web (10.8.0-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Forthcoming stable release
|
|
||||||
|
|
||||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 04 Dec 2020 21:58:23 -0500
|
|
||||||
|
|
||||||
jellyfin-web (10.7.0-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Forthcoming stable release
|
|
||||||
|
|
||||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 27 Jul 2020 19:13:31 -0400
|
|
||||||
|
|
||||||
jellyfin-web (10.6.0-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* New upstream version 10.6.0; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.6.0
|
|
||||||
|
|
||||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 16 Mar 2020 11:15:00 -0400
|
|
1
debian/compat
vendored
1
debian/compat
vendored
|
@ -1 +0,0 @@
|
||||||
8
|
|
1
debian/conffiles
vendored
1
debian/conffiles
vendored
|
@ -1 +0,0 @@
|
||||||
/usr/share/jellyfin/web/config.json
|
|
16
debian/control
vendored
16
debian/control
vendored
|
@ -1,16 +0,0 @@
|
||||||
Source: jellyfin-web
|
|
||||||
Section: misc
|
|
||||||
Priority: optional
|
|
||||||
Maintainer: Jellyfin Team <team@jellyfin.org>
|
|
||||||
Build-Depends: debhelper (>= 9),
|
|
||||||
npm | nodejs
|
|
||||||
Standards-Version: 3.9.4
|
|
||||||
Homepage: https://jellyfin.org/
|
|
||||||
Vcs-Git: https://github.org/jellyfin/jellyfin-web.git
|
|
||||||
Vcs-Browser: https://github.org/jellyfin/jellyfin-web
|
|
||||||
|
|
||||||
Package: jellyfin-web
|
|
||||||
Recommends: jellyfin-server
|
|
||||||
Architecture: all
|
|
||||||
Description: Jellyfin is the Free Software Media System.
|
|
||||||
This package provides the Jellyfin web client.
|
|
28
debian/copyright
vendored
28
debian/copyright
vendored
|
@ -1,28 +0,0 @@
|
||||||
Format: http://dep.debian.net/deps/dep5
|
|
||||||
Upstream-Name: jellyfin-web
|
|
||||||
Source: https://github.com/jellyfin/jellyfin-web
|
|
||||||
|
|
||||||
Files: *
|
|
||||||
Copyright: 2018-2020 Jellyfin Team
|
|
||||||
License: GPL-3.0
|
|
||||||
|
|
||||||
Files: debian/*
|
|
||||||
Copyright: 2020 Joshua Boniface <joshua@boniface.me>
|
|
||||||
License: GPL-3.0
|
|
||||||
|
|
||||||
License: GPL-3.0
|
|
||||||
This package is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 2 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
.
|
|
||||||
This package is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
.
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
.
|
|
||||||
On Debian systems, the complete text of the GNU General
|
|
||||||
Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
|
|
6
debian/gbp.conf
vendored
6
debian/gbp.conf
vendored
|
@ -1,6 +0,0 @@
|
||||||
[DEFAULT]
|
|
||||||
pristine-tar = False
|
|
||||||
cleaner = fakeroot debian/rules clean
|
|
||||||
|
|
||||||
[import-orig]
|
|
||||||
filter = [ ".git*", ".hg*", ".vs*", ".vscode*" ]
|
|
1
debian/install
vendored
1
debian/install
vendored
|
@ -1 +0,0 @@
|
||||||
web usr/share/jellyfin/
|
|
1
debian/po/POTFILES.in
vendored
1
debian/po/POTFILES.in
vendored
|
@ -1 +0,0 @@
|
||||||
[type: gettext/rfc822deb] templates
|
|
57
debian/po/templates.pot
vendored
57
debian/po/templates.pot
vendored
|
@ -1,57 +0,0 @@
|
||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the PACKAGE package.
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: jellyfin-server\n"
|
|
||||||
"Report-Msgid-Bugs-To: jellyfin-server@packages.debian.org\n"
|
|
||||||
"POT-Creation-Date: 2015-06-12 20:51-0600\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"Language: \n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
|
|
||||||
#. Type: note
|
|
||||||
#. Description
|
|
||||||
#: ../templates:1001
|
|
||||||
msgid "Jellyfin permission info:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. Type: note
|
|
||||||
#. Description
|
|
||||||
#: ../templates:1001
|
|
||||||
msgid ""
|
|
||||||
"Jellyfin by default runs under a user named \"jellyfin\". Please ensure that the "
|
|
||||||
"user jellyfin has read and write access to any folders you wish to add to your "
|
|
||||||
"library. Otherwise please run jellyfin under a different user."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. Type: string
|
|
||||||
#. Description
|
|
||||||
#: ../templates:2001
|
|
||||||
msgid "Username to run Jellyfin as:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. Type: string
|
|
||||||
#. Description
|
|
||||||
#: ../templates:2001
|
|
||||||
msgid "The user that jellyfin will run as."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. Type: note
|
|
||||||
#. Description
|
|
||||||
#: ../templates:3001
|
|
||||||
msgid "Jellyfin still running"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. Type: note
|
|
||||||
#. Description
|
|
||||||
#: ../templates:3001
|
|
||||||
msgid "Jellyfin is currently running. Please close it and try again."
|
|
||||||
msgstr ""
|
|
21
debian/rules
vendored
21
debian/rules
vendored
|
@ -1,21 +0,0 @@
|
||||||
#! /usr/bin/make -f
|
|
||||||
export DH_VERBOSE=1
|
|
||||||
|
|
||||||
%:
|
|
||||||
dh $@
|
|
||||||
|
|
||||||
# disable "make check"
|
|
||||||
override_dh_auto_test:
|
|
||||||
|
|
||||||
# disable stripping debugging symbols
|
|
||||||
override_dh_clistrip:
|
|
||||||
|
|
||||||
override_dh_auto_build:
|
|
||||||
npm ci --no-audit --unsafe-perm
|
|
||||||
npm run build:production
|
|
||||||
mv $(CURDIR)/dist $(CURDIR)/web
|
|
||||||
|
|
||||||
override_dh_auto_clean:
|
|
||||||
test -d $(CURDIR)/dist && rm -rf '$(CURDIR)/dist' || true
|
|
||||||
test -d $(CURDIR)/web && rm -rf '$(CURDIR)/web' || true
|
|
||||||
test -d $(CURDIR)/node_modules && rm -rf '$(CURDIR)/node_modules' || true
|
|
1
debian/source/format
vendored
1
debian/source/format
vendored
|
@ -1 +0,0 @@
|
||||||
1.0
|
|
7
debian/source/options
vendored
7
debian/source/options
vendored
|
@ -1,7 +0,0 @@
|
||||||
tar-ignore='.git*'
|
|
||||||
tar-ignore='**/.git'
|
|
||||||
tar-ignore='**/.hg'
|
|
||||||
tar-ignore='**/.vs'
|
|
||||||
tar-ignore='**/.vscode'
|
|
||||||
tar-ignore='deployment'
|
|
||||||
tar-ignore='*.deb'
|
|
|
@ -1,28 +0,0 @@
|
||||||
FROM quay.io/centos/centos:stream8
|
|
||||||
|
|
||||||
# Docker build arguments
|
|
||||||
ARG SOURCE_DIR=/jellyfin
|
|
||||||
ARG ARTIFACT_DIR=/dist
|
|
||||||
|
|
||||||
# Docker run environment
|
|
||||||
ENV SOURCE_DIR=/jellyfin
|
|
||||||
ENV ARTIFACT_DIR=/dist
|
|
||||||
ENV IS_DOCKER=YES
|
|
||||||
|
|
||||||
# Prepare CentOS environment
|
|
||||||
RUN yum update -y \
|
|
||||||
&& yum install -y epel-release \
|
|
||||||
&& yum install -y rpmdevtools git autoconf automake glibc-devel gcc-c++ make \
|
|
||||||
&& yum install https://rpm.nodesource.com/pub_20.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm -y \
|
|
||||||
&& yum install nodejs -y --setopt=nodesource-nodejs.module_hotfixes=1 \
|
|
||||||
&& yum clean all \
|
|
||||||
&& rm -rf /var/cache/dnf
|
|
||||||
|
|
||||||
# Link to build script
|
|
||||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.centos /build.sh
|
|
||||||
|
|
||||||
VOLUME ${SOURCE_DIR}
|
|
||||||
|
|
||||||
VOLUME ${ARTIFACT_DIR}
|
|
||||||
|
|
||||||
ENTRYPOINT ["/build.sh"]
|
|
|
@ -1,30 +0,0 @@
|
||||||
FROM debian:11
|
|
||||||
|
|
||||||
# Docker build arguments
|
|
||||||
ARG SOURCE_DIR=/jellyfin
|
|
||||||
ARG ARTIFACT_DIR=/dist
|
|
||||||
|
|
||||||
# Docker run environment
|
|
||||||
ENV SOURCE_DIR=/jellyfin
|
|
||||||
ENV ARTIFACT_DIR=/dist
|
|
||||||
ENV DEB_BUILD_OPTIONS=noddebs
|
|
||||||
ENV IS_DOCKER=YES
|
|
||||||
|
|
||||||
# Prepare Debian build environment
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y debhelper mmv git curl gnupg ca-certificates \
|
|
||||||
&& mkdir -p /etc/apt/keyrings \
|
|
||||||
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
|
||||||
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y nodejs \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
|
||||||
|
|
||||||
# Link to build script
|
|
||||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.debian /build.sh
|
|
||||||
|
|
||||||
VOLUME ${SOURCE_DIR}
|
|
||||||
|
|
||||||
VOLUME ${ARTIFACT_DIR}
|
|
||||||
|
|
||||||
ENTRYPOINT ["/build.sh"]
|
|
|
@ -1,13 +0,0 @@
|
||||||
FROM node:20-alpine
|
|
||||||
|
|
||||||
ARG SOURCE_DIR=/src
|
|
||||||
ARG ARTIFACT_DIR=/jellyfin-web
|
|
||||||
|
|
||||||
RUN apk --no-cache add autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3
|
|
||||||
|
|
||||||
WORKDIR ${SOURCE_DIR}
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN npm ci --no-audit --unsafe-perm \
|
|
||||||
&& npm run build:production \
|
|
||||||
&& mv dist ${ARTIFACT_DIR}
|
|
|
@ -1,26 +0,0 @@
|
||||||
FROM fedora:39
|
|
||||||
|
|
||||||
# Docker build arguments
|
|
||||||
ARG SOURCE_DIR=/jellyfin
|
|
||||||
ARG ARTIFACT_DIR=/dist
|
|
||||||
|
|
||||||
# Docker run environment
|
|
||||||
ENV SOURCE_DIR=/jellyfin
|
|
||||||
ENV ARTIFACT_DIR=/dist
|
|
||||||
ENV IS_DOCKER=YES
|
|
||||||
|
|
||||||
# Prepare Fedora environment
|
|
||||||
RUN dnf update -y \
|
|
||||||
&& dnf install https://rpm.nodesource.com/pub_20.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm -y \
|
|
||||||
&& dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core nodejs autoconf automake glibc-devel make --setopt=nodesource-nodejs.module_hotfixes=1 \
|
|
||||||
&& dnf clean all \
|
|
||||||
&& rm -rf /var/cache/dnf
|
|
||||||
|
|
||||||
# Link to build script
|
|
||||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora /build.sh
|
|
||||||
|
|
||||||
VOLUME ${SOURCE_DIR}
|
|
||||||
|
|
||||||
VOLUME ${ARTIFACT_DIR}
|
|
||||||
|
|
||||||
ENTRYPOINT ["/build.sh"]
|
|
|
@ -1,29 +0,0 @@
|
||||||
FROM debian:11
|
|
||||||
|
|
||||||
# Docker build arguments
|
|
||||||
ARG SOURCE_DIR=/jellyfin
|
|
||||||
ARG ARTIFACT_DIR=/dist
|
|
||||||
|
|
||||||
# Docker run environment
|
|
||||||
ENV SOURCE_DIR=/jellyfin
|
|
||||||
ENV ARTIFACT_DIR=/dist
|
|
||||||
ENV IS_DOCKER=YES
|
|
||||||
|
|
||||||
# Prepare Debian build environment
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y mmv curl git gnupg ca-certificates \
|
|
||||||
&& mkdir -p /etc/apt/keyrings \
|
|
||||||
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
|
||||||
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y nodejs \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
|
||||||
|
|
||||||
# Link to build script
|
|
||||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.portable /build.sh
|
|
||||||
|
|
||||||
VOLUME ${SOURCE_DIR}
|
|
||||||
|
|
||||||
VOLUME ${ARTIFACT_DIR}
|
|
||||||
|
|
||||||
ENTRYPOINT ["/build.sh"]
|
|
|
@ -1,41 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o xtrace
|
|
||||||
|
|
||||||
# move to source directory
|
|
||||||
pushd ${SOURCE_DIR}
|
|
||||||
|
|
||||||
cp -a package-lock.json /tmp/package-lock.json
|
|
||||||
|
|
||||||
# modify changelog to unstable configuration if IS_UNSTABLE
|
|
||||||
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
|
|
||||||
pushd fedora
|
|
||||||
|
|
||||||
PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
|
|
||||||
|
|
||||||
sed -i "s/Version:.*/Version: ${BUILD_ID}/" jellyfin-web.spec
|
|
||||||
sed -i "/%changelog/q" jellyfin-web.spec
|
|
||||||
|
|
||||||
cat <<EOF >>jellyfin-web.spec
|
|
||||||
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
|
|
||||||
- Jellyfin Web unstable build ${BUILD_ID} for merged PR #${PR_ID}
|
|
||||||
EOF
|
|
||||||
popd
|
|
||||||
fi
|
|
||||||
|
|
||||||
# build rpm
|
|
||||||
make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS
|
|
||||||
rpmbuild --rebuild -bb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
|
|
||||||
|
|
||||||
# move the artifacts
|
|
||||||
mv /root/rpmbuild/RPMS/noarch/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/
|
|
||||||
|
|
||||||
if [[ ${IS_DOCKER} == YES ]]; then
|
|
||||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f fedora/jellyfin*.tar.gz
|
|
||||||
cp -a /tmp/package-lock.json package-lock.json
|
|
||||||
|
|
||||||
popd
|
|
|
@ -1,39 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o xtrace
|
|
||||||
|
|
||||||
# move to source directory
|
|
||||||
pushd ${SOURCE_DIR}
|
|
||||||
|
|
||||||
cp -a package-lock.json /tmp/package-lock.json
|
|
||||||
|
|
||||||
# modify changelog to unstable configuration if IS_UNSTABLE
|
|
||||||
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
|
|
||||||
pushd debian
|
|
||||||
|
|
||||||
PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
|
|
||||||
|
|
||||||
cat <<EOF >changelog
|
|
||||||
jellyfin-web (${BUILD_ID}-unstable) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Jellyfin Web unstable build ${BUILD_ID} for merged PR #${PR_ID}
|
|
||||||
|
|
||||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
|
|
||||||
EOF
|
|
||||||
popd
|
|
||||||
fi
|
|
||||||
|
|
||||||
# build deb
|
|
||||||
dpkg-buildpackage -us -uc --pre-clean --post-clean
|
|
||||||
|
|
||||||
mkdir -p ${ARTIFACT_DIR}
|
|
||||||
mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}
|
|
||||||
|
|
||||||
cp -a /tmp/package-lock.json package-lock.json
|
|
||||||
|
|
||||||
if [[ ${IS_DOCKER} == YES ]]; then
|
|
||||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
|
||||||
fi
|
|
||||||
|
|
||||||
popd
|
|
|
@ -1,41 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o xtrace
|
|
||||||
|
|
||||||
# move to source directory
|
|
||||||
pushd ${SOURCE_DIR}
|
|
||||||
|
|
||||||
cp -a package-lock.json /tmp/package-lock.json
|
|
||||||
|
|
||||||
# modify changelog to unstable configuration if IS_UNSTABLE
|
|
||||||
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
|
|
||||||
pushd fedora
|
|
||||||
|
|
||||||
PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
|
|
||||||
|
|
||||||
sed -i "s/Version:.*/Version: ${BUILD_ID}/" jellyfin-web.spec
|
|
||||||
sed -i "/%changelog/q" jellyfin-web.spec
|
|
||||||
|
|
||||||
cat <<EOF >>jellyfin-web.spec
|
|
||||||
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
|
|
||||||
- Jellyfin Web unstable build ${BUILD_ID} for merged PR #${PR_ID}
|
|
||||||
EOF
|
|
||||||
popd
|
|
||||||
fi
|
|
||||||
|
|
||||||
# build rpm
|
|
||||||
make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS
|
|
||||||
rpmbuild -rb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
|
|
||||||
|
|
||||||
# move the artifacts
|
|
||||||
mv /root/rpmbuild/RPMS/noarch/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}
|
|
||||||
|
|
||||||
if [[ ${IS_DOCKER} == YES ]]; then
|
|
||||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f fedora/jellyfin*.tar.gz
|
|
||||||
cp -a /tmp/package-lock.json package-lock.json
|
|
||||||
|
|
||||||
popd
|
|
|
@ -1,31 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o xtrace
|
|
||||||
|
|
||||||
# move to source directory
|
|
||||||
pushd ${SOURCE_DIR}
|
|
||||||
|
|
||||||
# get version
|
|
||||||
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
|
|
||||||
version="${BUILD_ID}"
|
|
||||||
else
|
|
||||||
version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# build archives
|
|
||||||
npm ci --no-audit --unsafe-perm
|
|
||||||
npm run build:production
|
|
||||||
mv dist jellyfin-web_${version}
|
|
||||||
tar -czf jellyfin-web_${version}_portable.tar.gz jellyfin-web_${version}
|
|
||||||
rm -rf dist
|
|
||||||
|
|
||||||
# move the artifacts
|
|
||||||
mkdir -p ${ARTIFACT_DIR}
|
|
||||||
mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}
|
|
||||||
|
|
||||||
if [[ ${IS_DOCKER} == YES ]]; then
|
|
||||||
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
|
|
||||||
fi
|
|
||||||
|
|
||||||
popd
|
|
|
@ -1,48 +0,0 @@
|
||||||
DIR := $(dir $(lastword $(MAKEFILE_LIST)))
|
|
||||||
# install git and npm
|
|
||||||
$(info $(shell set -x; if [ "$$(id -u)" = "0" ]; then echo "Installing git"; dnf -y install git npm; fi))
|
|
||||||
NAME := jellyfin-web
|
|
||||||
VERSION := $(shell set -x; sed -ne '/^Version:/s/.* *//p' $(DIR)/$(NAME).spec)
|
|
||||||
RELEASE := $(shell set -x; sed -ne '/^Release:/s/.* *\(.*\)%{.*}.*/\1/p' $(DIR)/$(NAME).spec)
|
|
||||||
SRPM := jellyfin-web-$(subst -,~,$(VERSION))-$(RELEASE)$(shell rpm --eval %dist).src.rpm
|
|
||||||
TARBALL :=$(NAME)-$(subst -,~,$(VERSION)).tar.gz
|
|
||||||
|
|
||||||
epel-7-x86_64_repos := https://rpm.nodesource.com/pub_20.x/nodistro/\$$basearch/
|
|
||||||
|
|
||||||
fed_ver := $(shell rpm -E %fedora)
|
|
||||||
# fallback when not running on Fedora
|
|
||||||
fed_ver ?= 36
|
|
||||||
TARGET ?= fedora-$(fed_ver)-x86_64
|
|
||||||
|
|
||||||
outdir ?= $(PWD)/$(DIR)/
|
|
||||||
|
|
||||||
srpm: $(DIR)/$(SRPM)
|
|
||||||
tarball: $(DIR)/$(TARBALL)
|
|
||||||
|
|
||||||
$(DIR)/$(TARBALL):
|
|
||||||
cd $(DIR)/; \
|
|
||||||
SOURCE_DIR=.. \
|
|
||||||
WORKDIR="$${PWD}"; \
|
|
||||||
version=$(VERSION); \
|
|
||||||
tar \
|
|
||||||
--transform "s,^\.,$(NAME)-$(subst -,~,$(VERSION))," \
|
|
||||||
--exclude='.git*' \
|
|
||||||
--exclude='**/.git' \
|
|
||||||
--exclude='**/.hg' \
|
|
||||||
--exclude=deployment \
|
|
||||||
--exclude='*.deb' \
|
|
||||||
--exclude='*.rpm' \
|
|
||||||
--exclude=$(notdir $@) \
|
|
||||||
-czf $(notdir $@) \
|
|
||||||
-C $${SOURCE_DIR} ./
|
|
||||||
|
|
||||||
$(DIR)/$(SRPM): $(DIR)/$(TARBALL) $(DIR)/jellyfin-web.spec
|
|
||||||
cd $(DIR)/; \
|
|
||||||
rpmbuild -bs $(NAME).spec \
|
|
||||||
--define "_sourcedir $$PWD/" \
|
|
||||||
--define "_srcrpmdir $(outdir)"
|
|
||||||
|
|
||||||
rpms: $(DIR)/$(SRPM)
|
|
||||||
mock $(addprefix --addrepo=, $($(TARGET)_repos)) \
|
|
||||||
--enable-network \
|
|
||||||
-r $(TARGET) $<
|
|
|
@ -1,58 +0,0 @@
|
||||||
%global debug_package %{nil}
|
|
||||||
|
|
||||||
Name: jellyfin-web
|
|
||||||
Version: 10.8.0
|
|
||||||
Release: 2%{?dist}
|
|
||||||
Summary: The Free Software Media System web client
|
|
||||||
License: GPLv2
|
|
||||||
URL: https://jellyfin.org
|
|
||||||
# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%%{version}.tar.gz`
|
|
||||||
Source0: jellyfin-web-%{version}.tar.gz
|
|
||||||
|
|
||||||
BuildArch: noarch
|
|
||||||
%if 0%{?rhel} > 0 && 0%{?rhel} < 8
|
|
||||||
BuildRequires: nodejs
|
|
||||||
%else
|
|
||||||
BuildRequires: git
|
|
||||||
# Nodejs 20 is required and npm >= 10 should bring in NodeJS 20
|
|
||||||
# This requires the build environment to use the nodejs:20 module stream:
|
|
||||||
# dnf module {install|switch-to}:web nodejs:20
|
|
||||||
BuildRequires: npm >= 10
|
|
||||||
%endif
|
|
||||||
|
|
||||||
%description
|
|
||||||
Jellyfin is a free software media system that puts you in control of managing and streaming your media.
|
|
||||||
|
|
||||||
|
|
||||||
%prep
|
|
||||||
%autosetup -n jellyfin-web-%{version} -b 0
|
|
||||||
|
|
||||||
%if 0%{?rhel} > 0 && 0%{?rhel} < 8
|
|
||||||
# Required for CentOS build
|
|
||||||
chown root:root -R .
|
|
||||||
%endif
|
|
||||||
|
|
||||||
|
|
||||||
%build
|
|
||||||
npm ci --no-audit --unsafe-perm
|
|
||||||
npm run build:production
|
|
||||||
|
|
||||||
|
|
||||||
%install
|
|
||||||
%{__mkdir} -p %{buildroot}%{_libdir}/jellyfin/jellyfin-web
|
|
||||||
%{__cp} -r dist/* %{buildroot}%{_libdir}/jellyfin/jellyfin-web
|
|
||||||
|
|
||||||
|
|
||||||
%files
|
|
||||||
%defattr(644,root,root,755)
|
|
||||||
%{_libdir}/jellyfin/jellyfin-web
|
|
||||||
%license LICENSE
|
|
||||||
|
|
||||||
|
|
||||||
%changelog
|
|
||||||
* Fri Dec 04 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
|
|
||||||
- Forthcoming stable release
|
|
||||||
* Mon Jul 27 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
|
|
||||||
- Forthcoming stable release
|
|
||||||
* Mon Mar 23 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
|
|
||||||
- Forthcoming stable release
|
|
14904
package-lock.json
generated
14904
package-lock.json
generated
File diff suppressed because it is too large
Load diff
157
package.json
157
package.json
|
@ -5,89 +5,93 @@
|
||||||
"repository": "https://github.com/jellyfin/jellyfin-web",
|
"repository": "https://github.com/jellyfin/jellyfin-web",
|
||||||
"license": "GPL-2.0-or-later",
|
"license": "GPL-2.0-or-later",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.21.8",
|
"@babel/core": "7.23.7",
|
||||||
"@babel/plugin-proposal-class-properties": "7.18.6",
|
"@babel/plugin-proposal-class-properties": "7.18.6",
|
||||||
"@babel/plugin-proposal-private-methods": "7.18.6",
|
"@babel/plugin-proposal-private-methods": "7.18.6",
|
||||||
"@babel/plugin-transform-modules-umd": "7.18.6",
|
"@babel/plugin-transform-modules-umd": "7.23.3",
|
||||||
"@babel/preset-env": "7.21.5",
|
"@babel/preset-env": "7.23.8",
|
||||||
"@babel/preset-react": "7.18.6",
|
"@babel/preset-react": "7.23.3",
|
||||||
"@types/escape-html": "1.0.2",
|
"@types/escape-html": "1.0.4",
|
||||||
"@types/loadable__component": "5.13.4",
|
"@types/loadable__component": "5.13.9",
|
||||||
"@types/lodash-es": "4.17.7",
|
"@types/lodash-es": "4.17.12",
|
||||||
"@types/react": "17.0.59",
|
"@types/markdown-it": "13.0.7",
|
||||||
"@types/react-dom": "17.0.20",
|
"@types/react": "17.0.75",
|
||||||
"@typescript-eslint/eslint-plugin": "5.59.7",
|
"@types/react-dom": "17.0.25",
|
||||||
"@typescript-eslint/parser": "5.59.7",
|
"@types/sortablejs": "1.15.8",
|
||||||
|
"@typescript-eslint/eslint-plugin": "5.62.0",
|
||||||
|
"@typescript-eslint/parser": "5.62.0",
|
||||||
"@uupaa/dynamic-import-polyfill": "1.0.2",
|
"@uupaa/dynamic-import-polyfill": "1.0.2",
|
||||||
"autoprefixer": "10.4.14",
|
"autoprefixer": "10.4.17",
|
||||||
"babel-loader": "9.1.2",
|
"babel-loader": "9.1.3",
|
||||||
"babel-plugin-dynamic-import-polyfill": "1.0.0",
|
"babel-plugin-dynamic-import-polyfill": "1.0.0",
|
||||||
"clean-webpack-plugin": "4.0.0",
|
"clean-webpack-plugin": "4.0.0",
|
||||||
"confusing-browser-globals": "1.0.11",
|
"confusing-browser-globals": "1.0.11",
|
||||||
"copy-webpack-plugin": "11.0.0",
|
"copy-webpack-plugin": "12.0.2",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "6.8.1",
|
"css-loader": "6.9.1",
|
||||||
"cssnano": "6.0.1",
|
"cssnano": "6.0.5",
|
||||||
"es-check": "7.1.1",
|
"es-check": "7.1.1",
|
||||||
"eslint": "8.41.0",
|
"eslint": "8.56.0",
|
||||||
"eslint-plugin-compat": "4.1.4",
|
"eslint-plugin-compat": "4.2.0",
|
||||||
"eslint-plugin-eslint-comments": "3.2.0",
|
"eslint-plugin-eslint-comments": "3.2.0",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.29.1",
|
||||||
"eslint-plugin-jsx-a11y": "6.7.1",
|
"eslint-plugin-jsx-a11y": "6.8.0",
|
||||||
"eslint-plugin-react": "7.32.2",
|
"eslint-plugin-react": "7.33.2",
|
||||||
"eslint-plugin-react-hooks": "4.6.0",
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
"eslint-plugin-sonarjs": "0.19.0",
|
"eslint-plugin-sonarjs": "0.23.0",
|
||||||
"expose-loader": "4.1.0",
|
"expose-loader": "4.1.0",
|
||||||
"fork-ts-checker-webpack-plugin": "8.0.0",
|
"fork-ts-checker-webpack-plugin": "9.0.2",
|
||||||
"html-loader": "4.2.0",
|
"html-loader": "4.2.0",
|
||||||
"html-webpack-plugin": "5.5.3",
|
"html-webpack-plugin": "5.6.0",
|
||||||
"mini-css-extract-plugin": "2.7.6",
|
"jsdom": "23.2.0",
|
||||||
"postcss": "8.4.24",
|
"mini-css-extract-plugin": "2.7.7",
|
||||||
"postcss-loader": "7.3.3",
|
"postcss": "8.4.33",
|
||||||
"postcss-preset-env": "8.4.1",
|
"postcss-loader": "7.3.4",
|
||||||
"postcss-scss": "4.0.6",
|
"postcss-preset-env": "9.3.0",
|
||||||
"sass": "1.62.1",
|
"postcss-scss": "4.0.9",
|
||||||
"sass-loader": "13.3.2",
|
"sass": "1.70.0",
|
||||||
"source-map-loader": "4.0.1",
|
"sass-loader": "13.3.3",
|
||||||
|
"source-map-loader": "4.0.2",
|
||||||
"speed-measure-webpack-plugin": "1.5.0",
|
"speed-measure-webpack-plugin": "1.5.0",
|
||||||
"style-loader": "3.3.3",
|
"style-loader": "3.3.4",
|
||||||
"stylelint": "15.6.2",
|
"stylelint": "15.11.0",
|
||||||
"stylelint-config-rational-order": "0.1.2",
|
"stylelint-config-rational-order": "0.1.2",
|
||||||
"stylelint-no-browser-hacks": "1.2.1",
|
"stylelint-no-browser-hacks": "1.2.1",
|
||||||
"stylelint-order": "6.0.3",
|
"stylelint-order": "6.0.4",
|
||||||
"stylelint-scss": "5.0.0",
|
"stylelint-scss": "5.3.2",
|
||||||
"ts-loader": "9.4.4",
|
"ts-loader": "9.5.1",
|
||||||
"typescript": "5.0.4",
|
"typescript": "5.3.3",
|
||||||
"vitest": "0.34.6",
|
"vitest": "1.3.0",
|
||||||
"webpack": "5.88.1",
|
"webpack": "5.89.0",
|
||||||
"webpack-bundle-analyzer": "4.9.1",
|
"webpack-bundle-analyzer": "4.10.1",
|
||||||
"webpack-cli": "5.1.4",
|
"webpack-cli": "5.1.4",
|
||||||
"webpack-dev-server": "4.15.1",
|
"webpack-dev-server": "4.15.1",
|
||||||
"webpack-merge": "5.9.0",
|
"webpack-merge": "5.10.0",
|
||||||
"workbox-webpack-plugin": "6.6.0",
|
|
||||||
"worker-loader": "3.0.8"
|
"worker-loader": "3.0.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "11.11.0",
|
"@emotion/react": "11.11.4",
|
||||||
"@emotion/styled": "11.11.0",
|
"@emotion/styled": "11.11.0",
|
||||||
"@fontsource/noto-sans": "5.0.4",
|
"@fontsource/noto-sans": "5.0.18",
|
||||||
"@fontsource/noto-sans-hk": "5.0.4",
|
"@fontsource/noto-sans-hk": "5.0.17",
|
||||||
"@fontsource/noto-sans-jp": "5.0.4",
|
"@fontsource/noto-sans-jp": "5.0.17",
|
||||||
"@fontsource/noto-sans-kr": "5.0.4",
|
"@fontsource/noto-sans-kr": "5.0.17",
|
||||||
"@fontsource/noto-sans-sc": "5.0.4",
|
"@fontsource/noto-sans-sc": "5.0.17",
|
||||||
"@fontsource/noto-sans-tc": "5.0.4",
|
"@fontsource/noto-sans-tc": "5.0.17",
|
||||||
"@jellyfin/sdk": "unstable",
|
"@jellyfin/sdk": "0.0.0-unstable.202403180216",
|
||||||
"@loadable/component": "5.15.3",
|
"@loadable/component": "5.16.3",
|
||||||
"@mui/icons-material": "5.11.16",
|
"@mui/icons-material": "5.15.11",
|
||||||
"@mui/material": "5.13.3",
|
"@mui/material": "5.15.11",
|
||||||
"@mui/x-data-grid": "6.6.0",
|
"@mui/x-data-grid": "6.19.5",
|
||||||
"@react-hook/resize-observer": "1.2.6",
|
"@react-hook/resize-observer": "1.2.6",
|
||||||
"@tanstack/react-query": "4.29.12",
|
"@tanstack/react-query": "4.36.1",
|
||||||
"@tanstack/react-query-devtools": "4.29.12",
|
"@tanstack/react-query-devtools": "4.36.1",
|
||||||
|
"@types/react-lazy-load-image-component": "1.6.3",
|
||||||
|
"abortcontroller-polyfill": "1.7.5",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||||
"classnames": "2.3.2",
|
"classnames": "2.5.1",
|
||||||
"core-js": "3.30.2",
|
"core-js": "3.35.1",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"dompurify": "3.0.1",
|
"dompurify": "3.0.1",
|
||||||
"epubjs": "0.3.93",
|
"epubjs": "0.3.93",
|
||||||
|
@ -97,29 +101,30 @@
|
||||||
"flv.js": "1.6.2",
|
"flv.js": "1.6.2",
|
||||||
"headroom.js": "0.12.0",
|
"headroom.js": "0.12.0",
|
||||||
"history": "5.3.0",
|
"history": "5.3.0",
|
||||||
"hls.js": "github:nyanmisaka/hls.js#v1.5.0-fix-firefox-av1",
|
"hls.js": "1.5.7",
|
||||||
"intersection-observer": "0.12.2",
|
"intersection-observer": "0.12.2",
|
||||||
"jassub": "1.7.1",
|
"jassub": "1.7.15",
|
||||||
"jellyfin-apiclient": "1.10.0",
|
"jellyfin-apiclient": "1.11.0",
|
||||||
"jquery": "3.7.0",
|
"jquery": "3.7.1",
|
||||||
"jstree": "3.3.15",
|
"jstree": "3.3.16",
|
||||||
"libarchive.js": "1.3.0",
|
"libarchive.js": "1.3.0",
|
||||||
"lodash-es": "4.17.21",
|
"lodash-es": "4.17.21",
|
||||||
"marked": "4.3.0",
|
"markdown-it": "14.0.0",
|
||||||
"material-design-icons-iconfont": "6.7.0",
|
"material-design-icons-iconfont": "6.7.0",
|
||||||
"native-promise-only": "0.8.1",
|
"native-promise-only": "0.8.1",
|
||||||
"pdfjs-dist": "3.6.172",
|
"pdfjs-dist": "3.11.174",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
|
"react-blurhash": "0.3.0",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-router-dom": "6.11.1",
|
"react-lazy-load-image-component": "1.6.0",
|
||||||
|
"react-router-dom": "6.21.3",
|
||||||
"resize-observer-polyfill": "1.5.1",
|
"resize-observer-polyfill": "1.5.1",
|
||||||
"screenfull": "6.0.2",
|
"screenfull": "6.0.2",
|
||||||
"sortablejs": "1.15.0",
|
"sortablejs": "1.15.2",
|
||||||
"swiper": "9.3.2",
|
"swiper": "11.0.5",
|
||||||
|
"usehooks-ts": "2.14.0",
|
||||||
"webcomponents.js": "0.7.24",
|
"webcomponents.js": "0.7.24",
|
||||||
"whatwg-fetch": "3.6.2",
|
"whatwg-fetch": "3.6.20"
|
||||||
"workbox-core": "6.6.0",
|
|
||||||
"workbox-precaching": "6.6.0"
|
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 2 Firefox versions",
|
"last 2 Firefox versions",
|
||||||
|
@ -146,11 +151,9 @@
|
||||||
"build:check": "tsc --noEmit",
|
"build:check": "tsc --noEmit",
|
||||||
"escheck": "es-check",
|
"escheck": "es-check",
|
||||||
"lint": "eslint \"./\"",
|
"lint": "eslint \"./\"",
|
||||||
"test": "vitest --watch=false",
|
"test": "vitest --watch=false --config vite.config.ts",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest --config vite.config.ts",
|
||||||
"stylelint": "npm run stylelint:css && npm run stylelint:scss",
|
"stylelint": "stylelint \"src/**/*.{css,scss}\""
|
||||||
"stylelint:css": "stylelint \"src/**/*.css\"",
|
|
||||||
"stylelint:scss": "stylelint --config=\".stylelintrc.scss.json\" \"src/**/*.scss\""
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0",
|
"node": ">=20.0.0",
|
||||||
|
|
|
@ -1,62 +1,37 @@
|
||||||
import loadable from '@loadable/component';
|
import loadable from '@loadable/component';
|
||||||
import { ThemeProvider } from '@mui/material/styles';
|
import { ThemeProvider } from '@mui/material/styles';
|
||||||
import { History } from '@remix-run/router';
|
import { History } from '@remix-run/router';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
|
||||||
import AppHeader from 'components/AppHeader';
|
|
||||||
import Backdrop from 'components/Backdrop';
|
|
||||||
import { HistoryRouter } from 'components/router/HistoryRouter';
|
|
||||||
import { ApiProvider } from 'hooks/useApi';
|
import { ApiProvider } from 'hooks/useApi';
|
||||||
import { WebConfigProvider } from 'hooks/useWebConfig';
|
import { WebConfigProvider } from 'hooks/useWebConfig';
|
||||||
import theme from 'themes/theme';
|
import theme from 'themes/theme';
|
||||||
|
import { queryClient } from 'utils/query/queryClient';
|
||||||
|
|
||||||
const DashboardApp = loadable(() => import('./apps/dashboard/App'));
|
const StableAppRouter = loadable(() => import('./apps/stable/AppRouter'));
|
||||||
const ExperimentalApp = loadable(() => import('./apps/experimental/App'));
|
const RootAppRouter = loadable(() => import('./RootAppRouter'));
|
||||||
const StableApp = loadable(() => import('./apps/stable/App'));
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const RootApp = ({ history }: Readonly<{ history: History }>) => {
|
||||||
|
|
||||||
const RootAppLayout = () => {
|
|
||||||
const layoutMode = localStorage.getItem('layout');
|
const layoutMode = localStorage.getItem('layout');
|
||||||
const isExperimentalLayout = layoutMode === 'experimental';
|
const isExperimentalLayout = layoutMode === 'experimental';
|
||||||
|
|
||||||
const location = useLocation();
|
|
||||||
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
|
|
||||||
.some(path => location.pathname.startsWith(`/${path}`));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Backdrop />
|
<ApiProvider>
|
||||||
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
|
<WebConfigProvider>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
{
|
{isExperimentalLayout ?
|
||||||
isExperimentalLayout ?
|
<RootAppRouter history={history} /> :
|
||||||
<ExperimentalApp /> :
|
<StableAppRouter history={history} />
|
||||||
<StableApp />
|
}
|
||||||
}
|
</ThemeProvider>
|
||||||
|
</WebConfigProvider>
|
||||||
<DashboardApp />
|
</ApiProvider>
|
||||||
</>
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RootApp = ({ history }: { history: History }) => (
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<ApiProvider>
|
|
||||||
<WebConfigProvider>
|
|
||||||
<ThemeProvider theme={theme}>
|
|
||||||
<HistoryRouter history={history}>
|
|
||||||
<RootAppLayout />
|
|
||||||
</HistoryRouter>
|
|
||||||
</ThemeProvider>
|
|
||||||
</WebConfigProvider>
|
|
||||||
</ApiProvider>
|
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
|
||||||
</QueryClientProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default RootApp;
|
export default RootApp;
|
||||||
|
|
45
src/RootAppRouter.tsx
Normal file
45
src/RootAppRouter.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
|
||||||
|
import { History } from '@remix-run/router';
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
RouterProvider,
|
||||||
|
createHashRouter,
|
||||||
|
Outlet
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
|
import { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/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';
|
||||||
|
|
||||||
|
const router = createHashRouter([
|
||||||
|
{
|
||||||
|
element: <RootAppLayout />,
|
||||||
|
children: [
|
||||||
|
...EXPERIMENTAL_APP_ROUTES,
|
||||||
|
...DASHBOARD_APP_ROUTES
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default function RootAppRouter({ 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 RootAppLayout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Backdrop />
|
||||||
|
<AppHeader isHidden />
|
||||||
|
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,66 +0,0 @@
|
||||||
import loadable from '@loadable/component';
|
|
||||||
import React from 'react';
|
|
||||||
import { Route, Routes } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ConnectionRequired from 'components/ConnectionRequired';
|
|
||||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
|
||||||
import { AsyncPageProps, AsyncRoute, toAsyncPageRoute } from 'components/router/AsyncRoute';
|
|
||||||
import { toRedirectRoute } from 'components/router/Redirect';
|
|
||||||
import ServerContentPage from 'components/ServerContentPage';
|
|
||||||
|
|
||||||
import AppLayout from './AppLayout';
|
|
||||||
import { REDIRECTS } from './routes/_redirects';
|
|
||||||
import { ASYNC_ADMIN_ROUTES } from './routes/_asyncRoutes';
|
|
||||||
import { LEGACY_ADMIN_ROUTES } from './routes/_legacyRoutes';
|
|
||||||
|
|
||||||
const DashboardAsyncPage = loadable(
|
|
||||||
(props: { page: string }) => import(/* webpackChunkName: "[request]" */ `./routes/${props.page}`),
|
|
||||||
{ cacheKey: (props: AsyncPageProps) => props.page }
|
|
||||||
);
|
|
||||||
|
|
||||||
const toDashboardAsyncPageRoute = (route: AsyncRoute) => (
|
|
||||||
toAsyncPageRoute({
|
|
||||||
...route,
|
|
||||||
element: DashboardAsyncPage
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const DASHBOARD_APP_PATHS = {
|
|
||||||
Dashboard: 'dashboard',
|
|
||||||
MetadataManager: 'metadata',
|
|
||||||
PluginConfig: 'configurationpage'
|
|
||||||
};
|
|
||||||
|
|
||||||
const DashboardApp = () => (
|
|
||||||
<Routes>
|
|
||||||
<Route element={<ConnectionRequired isAdminRequired />}>
|
|
||||||
<Route element={<AppLayout drawerlessPaths={[ DASHBOARD_APP_PATHS.MetadataManager ]} />}>
|
|
||||||
<Route path={DASHBOARD_APP_PATHS.Dashboard}>
|
|
||||||
{ASYNC_ADMIN_ROUTES.map(toDashboardAsyncPageRoute)}
|
|
||||||
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* NOTE: The metadata editor might deserve a dedicated app in the future */}
|
|
||||||
{toViewManagerPageRoute({
|
|
||||||
path: DASHBOARD_APP_PATHS.MetadataManager,
|
|
||||||
pageProps: {
|
|
||||||
controller: 'edititemmetadata',
|
|
||||||
view: 'edititemmetadata.html'
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
|
|
||||||
<Route path={DASHBOARD_APP_PATHS.PluginConfig} element={
|
|
||||||
<ServerContentPage view='/web/configurationpage' />
|
|
||||||
} />
|
|
||||||
</Route>
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* Suppress warnings for unhandled routes */}
|
|
||||||
<Route path='*' element={null} />
|
|
||||||
|
|
||||||
{/* Redirects for old paths */}
|
|
||||||
{REDIRECTS.map(toRedirectRoute)}
|
|
||||||
</Routes>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default DashboardApp;
|
|
|
@ -1,7 +1,8 @@
|
||||||
import AppBar from '@mui/material/AppBar';
|
import AppBar from '@mui/material/AppBar';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { type Theme } from '@mui/material/styles';
|
||||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
|
import React, { FC, useCallback, useState } from 'react';
|
||||||
import { Outlet, useLocation } from 'react-router-dom';
|
import { Outlet, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import AppBody from 'components/AppBody';
|
import AppBody from 'components/AppBody';
|
||||||
|
@ -9,7 +10,6 @@ import AppToolbar from 'components/toolbar/AppToolbar';
|
||||||
import ElevationScroll from 'components/ElevationScroll';
|
import ElevationScroll from 'components/ElevationScroll';
|
||||||
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
|
||||||
|
|
||||||
import AppDrawer from './components/drawer/AppDrawer';
|
import AppDrawer from './components/drawer/AppDrawer';
|
||||||
|
|
||||||
|
@ -19,34 +19,17 @@ interface AppLayoutProps {
|
||||||
drawerlessPaths: string[]
|
drawerlessPaths: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DashboardAppSettings {
|
|
||||||
isDrawerPinned: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_APP_SETTINGS: DashboardAppSettings = {
|
|
||||||
isDrawerPinned: false
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppLayout: FC<AppLayoutProps> = ({
|
const AppLayout: FC<AppLayoutProps> = ({
|
||||||
drawerlessPaths
|
drawerlessPaths
|
||||||
}) => {
|
}) => {
|
||||||
const [ appSettings, setAppSettings ] = useLocalStorage<DashboardAppSettings>('DashboardAppSettings', DEFAULT_APP_SETTINGS);
|
const [ isDrawerActive, setIsDrawerActive ] = useState(false);
|
||||||
const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned);
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const theme = useTheme();
|
|
||||||
const { user } = useApi();
|
const { user } = useApi();
|
||||||
|
|
||||||
const isDrawerAvailable = !drawerlessPaths.some(path => location.pathname.startsWith(`/${path}`));
|
const isMediumScreen = useMediaQuery((t: Theme) => t.breakpoints.up('md'));
|
||||||
const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user);
|
const isDrawerAvailable = Boolean(user)
|
||||||
|
&& !drawerlessPaths.some(path => location.pathname.startsWith(`/${path}`));
|
||||||
useEffect(() => {
|
const isDrawerOpen = isDrawerActive && isDrawerAvailable;
|
||||||
if (isDrawerActive !== appSettings.isDrawerPinned) {
|
|
||||||
setAppSettings({
|
|
||||||
...appSettings,
|
|
||||||
isDrawerPinned: isDrawerActive
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [ appSettings, isDrawerActive, setAppSettings ]);
|
|
||||||
|
|
||||||
const onToggleDrawer = useCallback(() => {
|
const onToggleDrawer = useCallback(() => {
|
||||||
setIsDrawerActive(!isDrawerActive);
|
setIsDrawerActive(!isDrawerActive);
|
||||||
|
@ -54,47 +37,43 @@ const AppLayout: FC<AppLayoutProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
<ElevationScroll elevate={isDrawerOpen}>
|
<ElevationScroll elevate={false}>
|
||||||
<AppBar
|
<AppBar
|
||||||
position='fixed'
|
position='fixed'
|
||||||
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.drawer + 1 }}
|
sx={{
|
||||||
|
width: {
|
||||||
|
xs: '100%',
|
||||||
|
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
|
||||||
|
},
|
||||||
|
ml: {
|
||||||
|
xs: 0,
|
||||||
|
md: isDrawerAvailable ? DRAWER_WIDTH : 0
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<AppToolbar
|
<AppToolbar
|
||||||
isDrawerAvailable={isDrawerAvailable}
|
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
|
||||||
isDrawerOpen={isDrawerOpen}
|
isDrawerOpen={isDrawerOpen}
|
||||||
onDrawerButtonClick={onToggleDrawer}
|
onDrawerButtonClick={onToggleDrawer}
|
||||||
/>
|
/>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
</ElevationScroll>
|
</ElevationScroll>
|
||||||
|
|
||||||
<AppDrawer
|
{
|
||||||
open={isDrawerOpen}
|
isDrawerAvailable && (
|
||||||
onClose={onToggleDrawer}
|
<AppDrawer
|
||||||
onOpen={onToggleDrawer}
|
open={isDrawerOpen}
|
||||||
/>
|
onClose={onToggleDrawer}
|
||||||
|
onOpen={onToggleDrawer}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
component='main'
|
component='main'
|
||||||
sx={{
|
sx={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
flexGrow: 1,
|
flexGrow: 1
|
||||||
transition: theme.transitions.create('margin', {
|
|
||||||
easing: theme.transitions.easing.sharp,
|
|
||||||
duration: theme.transitions.duration.leavingScreen
|
|
||||||
}),
|
|
||||||
marginLeft: 0,
|
|
||||||
...(isDrawerAvailable && {
|
|
||||||
marginLeft: {
|
|
||||||
sm: `-${DRAWER_WIDTH}px`
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
...(isDrawerActive && {
|
|
||||||
transition: theme.transitions.create('margin', {
|
|
||||||
easing: theme.transitions.easing.easeOut,
|
|
||||||
duration: theme.transitions.duration.enteringScreen
|
|
||||||
}),
|
|
||||||
marginLeft: 0
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AppBody>
|
<AppBody>
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import List from '@mui/material/List';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
import DrawerHeaderLink from 'apps/experimental/components/drawers/DrawerHeaderLink';
|
||||||
import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';
|
import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';
|
||||||
|
|
||||||
import ServerDrawerSection from './sections/ServerDrawerSection';
|
import ServerDrawerSection from './sections/ServerDrawerSection';
|
||||||
|
@ -18,6 +21,11 @@ const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onOpen={onOpen}
|
onOpen={onOpen}
|
||||||
>
|
>
|
||||||
|
<List disablePadding>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<DrawerHeaderLink />
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
<ServerDrawerSection />
|
<ServerDrawerSection />
|
||||||
<DevicesDrawerSection />
|
<DevicesDrawerSection />
|
||||||
<LiveTvDrawerSection />
|
<LiveTvDrawerSection />
|
||||||
|
|
|
@ -1,26 +1,15 @@
|
||||||
import { Devices, Analytics, Input, ExpandLess, ExpandMore } from '@mui/icons-material';
|
import { Devices, Analytics, Input } from '@mui/icons-material';
|
||||||
import Collapse from '@mui/material/Collapse';
|
|
||||||
import List from '@mui/material/List';
|
import List from '@mui/material/List';
|
||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import ListSubheader from '@mui/material/ListSubheader';
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ListItemLink from 'components/ListItemLink';
|
import ListItemLink from 'components/ListItemLink';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
const DLNA_PATHS = [
|
|
||||||
'/dashboard/dlna',
|
|
||||||
'/dashboard/dlna/profiles'
|
|
||||||
];
|
|
||||||
|
|
||||||
const DevicesDrawerSection = () => {
|
const DevicesDrawerSection = () => {
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const isDlnaSectionOpen = DLNA_PATHS.includes(location.pathname);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
aria-labelledby='devices-subheader'
|
aria-labelledby='devices-subheader'
|
||||||
|
@ -47,24 +36,13 @@ const DevicesDrawerSection = () => {
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/dashboard/dlna' selected={false}>
|
<ListItemLink to='/dashboard/dlna'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Input />
|
<Input />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={'DLNA'} />
|
<ListItemText primary={'DLNA'} />
|
||||||
{isDlnaSectionOpen ? <ExpandLess /> : <ExpandMore />}
|
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Collapse in={isDlnaSectionOpen} timeout='auto' unmountOnExit>
|
|
||||||
<List component='div' disablePadding>
|
|
||||||
<ListItemLink to='/dashboard/dlna' sx={{ pl: 4 }}>
|
|
||||||
<ListItemText inset primary={globalize.translate('Settings')} />
|
|
||||||
</ListItemLink>
|
|
||||||
<ListItemLink to='/dashboard/dlna/profiles' sx={{ pl: 4 }}>
|
|
||||||
<ListItemText inset primary={globalize.translate('TabProfiles')} />
|
|
||||||
</ListItemLink>
|
|
||||||
</List>
|
|
||||||
</Collapse>
|
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,7 +21,8 @@ const LIBRARY_PATHS = [
|
||||||
const PLAYBACK_PATHS = [
|
const PLAYBACK_PATHS = [
|
||||||
'/dashboard/playback/transcoding',
|
'/dashboard/playback/transcoding',
|
||||||
'/dashboard/playback/resume',
|
'/dashboard/playback/resume',
|
||||||
'/dashboard/playback/streaming'
|
'/dashboard/playback/streaming',
|
||||||
|
'/dashboard/playback/trickplay'
|
||||||
];
|
];
|
||||||
|
|
||||||
const ServerDrawerSection = () => {
|
const ServerDrawerSection = () => {
|
||||||
|
@ -108,6 +109,9 @@ const ServerDrawerSection = () => {
|
||||||
<ListItemLink to='/dashboard/playback/streaming' sx={{ pl: 4 }}>
|
<ListItemLink to='/dashboard/playback/streaming' sx={{ pl: 4 }}>
|
||||||
<ListItemText inset primary={globalize.translate('TabStreaming')} />
|
<ListItemText inset primary={globalize.translate('TabStreaming')} />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
|
<ListItemLink to='/dashboard/playback/trickplay' sx={{ pl: 4 }}>
|
||||||
|
<ListItemText inset primary={globalize.translate('Trickplay')} />
|
||||||
|
</ListItemLink>
|
||||||
</List>
|
</List>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</List>
|
</List>
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import type { AsyncRoute } from 'components/router/AsyncRoute';
|
import { AsyncRouteType, type AsyncRoute } from 'components/router/AsyncRoute';
|
||||||
|
|
||||||
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||||
{ path: 'activity' },
|
{ path: 'activity', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'notifications' },
|
{ path: 'dlna', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'users' },
|
{ path: 'notifications', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'users/access' },
|
{ path: 'users', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'users/add' },
|
{ path: 'users/access', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'users/parentalcontrol' },
|
{ path: 'users/add', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'users/password' },
|
{ path: 'users/parentalcontrol', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'users/profile' }
|
{ path: 'users/password', type: AsyncRouteType.Dashboard },
|
||||||
|
{ path: 'users/profile', type: AsyncRouteType.Dashboard },
|
||||||
|
{ path: 'playback/trickplay', type: AsyncRouteType.Dashboard }
|
||||||
];
|
];
|
||||||
|
|
|
@ -31,24 +31,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||||
controller: 'dashboard/devices/device',
|
controller: 'dashboard/devices/device',
|
||||||
view: 'dashboard/devices/device.html'
|
view: 'dashboard/devices/device.html'
|
||||||
}
|
}
|
||||||
}, {
|
|
||||||
path: 'dlna/profiles/edit',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/dlna/profile',
|
|
||||||
view: 'dashboard/dlna/profile.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'dlna/profiles',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/dlna/profiles',
|
|
||||||
view: 'dashboard/dlna/profiles.html'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
path: 'dlna',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/dlna/settings',
|
|
||||||
view: 'dashboard/dlna/settings.html'
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
path: 'plugins/add',
|
path: 'plugins/add',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
|
|
|
@ -8,8 +8,8 @@ export const REDIRECTS: Redirect[] = [
|
||||||
{ from: 'dashboardgeneral.html', to: '/dashboard/settings' },
|
{ from: 'dashboardgeneral.html', to: '/dashboard/settings' },
|
||||||
{ from: 'device.html', to: '/dashboard/devices/edit' },
|
{ from: 'device.html', to: '/dashboard/devices/edit' },
|
||||||
{ from: 'devices.html', to: '/dashboard/devices' },
|
{ from: 'devices.html', to: '/dashboard/devices' },
|
||||||
{ from: 'dlnaprofile.html', to: '/dashboard/dlna/profiles/edit' },
|
{ from: 'dlnaprofile.html', to: '/dashboard/dlna' },
|
||||||
{ from: 'dlnaprofiles.html', to: '/dashboard/dlna/profiles' },
|
{ from: 'dlnaprofiles.html', to: '/dashboard/dlna' },
|
||||||
{ from: 'dlnasettings.html', to: '/dashboard/dlna' },
|
{ from: 'dlnasettings.html', to: '/dashboard/dlna' },
|
||||||
{ from: 'edititemmetadata.html', to: '/metadata' },
|
{ from: 'edititemmetadata.html', to: '/metadata' },
|
||||||
{ from: 'encodingsettings.html', to: '/dashboard/playback/transcoding' },
|
{ from: 'encodingsettings.html', to: '/dashboard/playback/transcoding' },
|
||||||
|
|
33
src/apps/dashboard/routes/dlna.tsx
Normal file
33
src/apps/dashboard/routes/dlna.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import Alert from '@mui/material/Alert/Alert';
|
||||||
|
import Box from '@mui/material/Box/Box';
|
||||||
|
import Button from '@mui/material/Button/Button';
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import Page from 'components/Page';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
|
const DlnaPage = () => (
|
||||||
|
<Page
|
||||||
|
id='dlnaSettingsPage'
|
||||||
|
title='DLNA'
|
||||||
|
className='mainAnimatedPage type-interior'
|
||||||
|
>
|
||||||
|
<div className='content-primary'>
|
||||||
|
<h2>DLNA</h2>
|
||||||
|
<Alert severity='info'>
|
||||||
|
<Box sx={{ marginBottom: 2 }}>
|
||||||
|
{globalize.translate('DlnaMovedMessage')}
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to='/dashboard/plugins/add?name=DLNA&guid=33eba9cd7da14720967fdd7dae7b74a1'
|
||||||
|
>
|
||||||
|
{globalize.translate('GetThePlugin')}
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default DlnaPage;
|
|
@ -1,23 +1,13 @@
|
||||||
|
import Alert from '@mui/material/Alert/Alert';
|
||||||
|
import Box from '@mui/material/Box/Box';
|
||||||
|
import Button from '@mui/material/Button/Button';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import Page from 'components/Page';
|
import Page from 'components/Page';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
const PluginLink = () => (
|
const NotificationsPage = () => (
|
||||||
<div
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: `<a
|
|
||||||
is='emby-linkbutton'
|
|
||||||
class='button-link'
|
|
||||||
href='#/dashboard/plugins/add?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173'
|
|
||||||
>
|
|
||||||
${globalize.translate('GetThePlugin')}
|
|
||||||
</a>`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Notifications = () => (
|
|
||||||
<Page
|
<Page
|
||||||
id='notificationSettingPage'
|
id='notificationSettingPage'
|
||||||
title={globalize.translate('Notifications')}
|
title={globalize.translate('Notifications')}
|
||||||
|
@ -25,12 +15,20 @@ const Notifications = () => (
|
||||||
>
|
>
|
||||||
<div className='content-primary'>
|
<div className='content-primary'>
|
||||||
<h2>{globalize.translate('Notifications')}</h2>
|
<h2>{globalize.translate('Notifications')}</h2>
|
||||||
<p>
|
|
||||||
{globalize.translate('NotificationsMovedMessage')}
|
<Alert severity='info'>
|
||||||
</p>
|
<Box sx={{ marginBottom: 2 }}>
|
||||||
<PluginLink />
|
{globalize.translate('NotificationsMovedMessage')}
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to='/dashboard/plugins/add?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173'
|
||||||
|
>
|
||||||
|
{globalize.translate('GetThePlugin')}
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Notifications;
|
export default NotificationsPage;
|
||||||
|
|
305
src/apps/dashboard/routes/playback/trickplay.tsx
Normal file
305
src/apps/dashboard/routes/playback/trickplay.tsx
Normal file
|
@ -0,0 +1,305 @@
|
||||||
|
import type { ProcessPriorityClass, ServerConfiguration, TrickplayScanBehavior } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import React, { type FunctionComponent, useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import globalize from '../../../../scripts/globalize';
|
||||||
|
import Page from '../../../../components/Page';
|
||||||
|
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||||
|
import ButtonElement from '../../../../elements/ButtonElement';
|
||||||
|
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||||
|
import SelectElement from '../../../../elements/SelectElement';
|
||||||
|
import InputElement from '../../../../elements/InputElement';
|
||||||
|
import LinkTrickplayAcceleration from '../../../../components/dashboard/playback/trickplay/LinkTrickplayAcceleration';
|
||||||
|
import loading from '../../../../components/loading/loading';
|
||||||
|
import toast from '../../../../components/toast/toast';
|
||||||
|
import ServerConnections from '../../../../components/ServerConnections';
|
||||||
|
|
||||||
|
function onSaveComplete() {
|
||||||
|
loading.hide();
|
||||||
|
toast(globalize.translate('SettingsSaved'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlaybackTrickplay: FunctionComponent = () => {
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const loadConfig = useCallback((config) => {
|
||||||
|
const page = element.current;
|
||||||
|
const options = config.TrickplayOptions;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(page.querySelector('.chkEnableHwAcceleration') as HTMLInputElement).checked = options.EnableHwAcceleration;
|
||||||
|
(page.querySelector('#selectScanBehavior') as HTMLSelectElement).value = options.ScanBehavior;
|
||||||
|
(page.querySelector('#selectProcessPriority') as HTMLSelectElement).value = options.ProcessPriority;
|
||||||
|
(page.querySelector('#txtInterval') as HTMLInputElement).value = options.Interval;
|
||||||
|
(page.querySelector('#txtWidthResolutions') as HTMLInputElement).value = options.WidthResolutions.join(',');
|
||||||
|
(page.querySelector('#txtTileWidth') as HTMLInputElement).value = options.TileWidth;
|
||||||
|
(page.querySelector('#txtTileHeight') as HTMLInputElement).value = options.TileHeight;
|
||||||
|
(page.querySelector('#txtQscale') as HTMLInputElement).value = options.Qscale;
|
||||||
|
(page.querySelector('#txtJpegQuality') as HTMLInputElement).value = options.JpegQuality;
|
||||||
|
(page.querySelector('#txtProcessThreads') as HTMLInputElement).value = options.ProcessThreads;
|
||||||
|
|
||||||
|
loading.hide();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadData = useCallback(() => {
|
||||||
|
loading.show();
|
||||||
|
|
||||||
|
ServerConnections.currentApiClient()?.getServerConfiguration().then(function (config) {
|
||||||
|
loadConfig(config);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[PlaybackTrickplay] failed to fetch server config', err);
|
||||||
|
});
|
||||||
|
}, [loadConfig]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const page = element.current;
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveConfig = (config: ServerConfiguration) => {
|
||||||
|
const apiClient = ServerConnections.currentApiClient();
|
||||||
|
|
||||||
|
if (!apiClient) {
|
||||||
|
console.error('[PlaybackTrickplay] No current apiclient instance');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.TrickplayOptions) {
|
||||||
|
throw new Error('Unexpected null TrickplayOptions');
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = config.TrickplayOptions;
|
||||||
|
options.EnableHwAcceleration = (page.querySelector('.chkEnableHwAcceleration') as HTMLInputElement).checked;
|
||||||
|
options.ScanBehavior = (page.querySelector('#selectScanBehavior') as HTMLSelectElement).value as TrickplayScanBehavior;
|
||||||
|
options.ProcessPriority = (page.querySelector('#selectProcessPriority') as HTMLSelectElement).value as ProcessPriorityClass;
|
||||||
|
options.Interval = Math.max(1, parseInt((page.querySelector('#txtInterval') as HTMLInputElement).value || '10000', 10));
|
||||||
|
options.WidthResolutions = (page.querySelector('#txtWidthResolutions') as HTMLInputElement).value.replace(' ', '').split(',').map(Number);
|
||||||
|
options.TileWidth = Math.max(1, parseInt((page.querySelector('#txtTileWidth') as HTMLInputElement).value || '10', 10));
|
||||||
|
options.TileHeight = Math.max(1, parseInt((page.querySelector('#txtTileHeight') as HTMLInputElement).value || '10', 10));
|
||||||
|
options.Qscale = Math.min(31, parseInt((page.querySelector('#txtQscale') as HTMLInputElement).value || '4', 10));
|
||||||
|
options.JpegQuality = Math.min(100, parseInt((page.querySelector('#txtJpegQuality') as HTMLInputElement).value || '90', 10));
|
||||||
|
options.ProcessThreads = parseInt((page.querySelector('#txtProcessThreads') as HTMLInputElement).value || '1', 10);
|
||||||
|
|
||||||
|
apiClient.updateServerConfiguration(config).then(() => {
|
||||||
|
onSaveComplete();
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[PlaybackTrickplay] failed to update config', err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (e: Event) => {
|
||||||
|
const apiClient = ServerConnections.currentApiClient();
|
||||||
|
|
||||||
|
if (!apiClient) {
|
||||||
|
console.error('[PlaybackTrickplay] No current apiclient instance');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.show();
|
||||||
|
apiClient.getServerConfiguration().then(function (config) {
|
||||||
|
saveConfig(config);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[PlaybackTrickplay] failed to fetch server config', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
(page.querySelector('.trickplayConfigurationForm') as HTMLFormElement).addEventListener('submit', onSubmit);
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
const optionScanBehavior = () => {
|
||||||
|
let content = '';
|
||||||
|
content += `<option value='NonBlocking'>${globalize.translate('NonBlockingScan')}</option>`;
|
||||||
|
content += `<option value='Blocking'>${globalize.translate('BlockingScan')}</option>`;
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionProcessPriority = () => {
|
||||||
|
let content = '';
|
||||||
|
content += `<option value='High'>${globalize.translate('PriorityHigh')}</option>`;
|
||||||
|
content += `<option value='AboveNormal'>${globalize.translate('PriorityAboveNormal')}</option>`;
|
||||||
|
content += `<option value='Normal'>${globalize.translate('PriorityNormal')}</option>`;
|
||||||
|
content += `<option value='BelowNormal'>${globalize.translate('PriorityBelowNormal')}</option>`;
|
||||||
|
content += `<option value='Idle'>${globalize.translate('PriorityIdle')}</option>`;
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
id='trickplayConfigurationPage'
|
||||||
|
className='mainAnimatedPage type-interior playbackConfigurationPage'
|
||||||
|
>
|
||||||
|
<div ref={element} className='content-primary'>
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<SectionTitleContainer
|
||||||
|
title={globalize.translate('Trickplay')}
|
||||||
|
isLinkVisible={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className='trickplayConfigurationForm'>
|
||||||
|
<div className='checkboxContainer checkboxContainer-withDescription'>
|
||||||
|
<CheckBoxElement
|
||||||
|
className='chkEnableHwAcceleration'
|
||||||
|
title='LabelTrickplayAccel'
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription checkboxFieldDescription'>
|
||||||
|
<LinkTrickplayAcceleration
|
||||||
|
title='LabelTrickplayAccelHelp'
|
||||||
|
href='#/dashboard/playback/transcoding'
|
||||||
|
className='button-link'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='selectContainer fldSelectScanBehavior'>
|
||||||
|
<SelectElement
|
||||||
|
id='selectScanBehavior'
|
||||||
|
label='LabelScanBehavior'
|
||||||
|
>
|
||||||
|
{optionScanBehavior()}
|
||||||
|
</SelectElement>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelScanBehaviorHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='selectContainer fldSelectProcessPriority'>
|
||||||
|
<SelectElement
|
||||||
|
id='selectProcessPriority'
|
||||||
|
label='LabelProcessPriority'
|
||||||
|
>
|
||||||
|
{optionProcessPriority()}
|
||||||
|
</SelectElement>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelProcessPriorityHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='number'
|
||||||
|
id='txtInterval'
|
||||||
|
label='LabelImageInterval'
|
||||||
|
options={'required inputMode="numeric" pattern="[0-9]*" min="1"'}
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelImageIntervalHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='text'
|
||||||
|
id='txtWidthResolutions'
|
||||||
|
label='LabelWidthResolutions'
|
||||||
|
options={'required pattern="[0-9,]*"'}
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelWidthResolutionsHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='number'
|
||||||
|
id='txtTileWidth'
|
||||||
|
label='LabelTileWidth'
|
||||||
|
options={'required inputMode="numeric" pattern="[0-9]*" min="1"'}
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelTileWidthHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='number'
|
||||||
|
id='txtTileHeight'
|
||||||
|
label='LabelTileHeight'
|
||||||
|
options={'required inputMode="numeric" pattern="[0-9]*" min="1"'}
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelTileHeightHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='number'
|
||||||
|
id='txtJpegQuality'
|
||||||
|
label='LabelJpegQuality'
|
||||||
|
options={'required inputMode="numeric" pattern="[0-9]*" min="1" max="100"'}
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelJpegQualityHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='number'
|
||||||
|
id='txtQscale'
|
||||||
|
label='LabelQscale'
|
||||||
|
options={'required inputMode="numeric" pattern="[0-9]*" min="2" max="31"'}
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelQscaleHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='number'
|
||||||
|
id='txtProcessThreads'
|
||||||
|
label='LabelTrickplayThreads'
|
||||||
|
options={'required inputMode="numeric" pattern="[0-9]*" min="0"'}
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription'>
|
||||||
|
{globalize.translate('LabelTrickplayThreadsHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ButtonElement
|
||||||
|
type='submit'
|
||||||
|
className='raised button-submit block'
|
||||||
|
title='Save'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PlaybackTrickplay;
|
49
src/apps/dashboard/routes/routes.tsx
Normal file
49
src/apps/dashboard/routes/routes.tsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { RouteObject } from 'react-router-dom';
|
||||||
|
import AppLayout from '../AppLayout';
|
||||||
|
import ConnectionRequired from 'components/ConnectionRequired';
|
||||||
|
import { ASYNC_ADMIN_ROUTES } from './_asyncRoutes';
|
||||||
|
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||||
|
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||||
|
import { LEGACY_ADMIN_ROUTES } from './_legacyRoutes';
|
||||||
|
import ServerContentPage from 'components/ServerContentPage';
|
||||||
|
|
||||||
|
export const DASHBOARD_APP_PATHS = {
|
||||||
|
Dashboard: 'dashboard',
|
||||||
|
MetadataManager: 'metadata',
|
||||||
|
PluginConfig: 'configurationpage'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DASHBOARD_APP_ROUTES: RouteObject[] = [
|
||||||
|
{
|
||||||
|
element: <ConnectionRequired isAdminRequired />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
element: <AppLayout drawerlessPaths={[ DASHBOARD_APP_PATHS.MetadataManager ]} />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: DASHBOARD_APP_PATHS.Dashboard,
|
||||||
|
children: [
|
||||||
|
...ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute),
|
||||||
|
...LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
/* NOTE: The metadata editor might deserve a dedicated app in the future */
|
||||||
|
toViewManagerPageRoute({
|
||||||
|
path: DASHBOARD_APP_PATHS.MetadataManager,
|
||||||
|
pageProps: {
|
||||||
|
controller: 'edititemmetadata',
|
||||||
|
view: 'edititemmetadata.html'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
{
|
||||||
|
path: DASHBOARD_APP_PATHS.PluginConfig,
|
||||||
|
element: <ServerContentPage view='/web/configurationpage' />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
|
@ -29,8 +29,7 @@ const UserLibraryAccess: FunctionComponent = () => {
|
||||||
const element = useRef<HTMLDivElement>(null);
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const triggerChange = (select: HTMLInputElement) => {
|
const triggerChange = (select: HTMLInputElement) => {
|
||||||
const evt = document.createEvent('HTMLEvents');
|
const evt = new Event('change', { bubbles: false, cancelable: true });
|
||||||
evt.initEvent('change', false, true);
|
|
||||||
select.dispatchEvent(evt);
|
select.dispatchEvent(evt);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@ const UserProfiles: FunctionComponent = () => {
|
||||||
const showUserMenu = (elem: HTMLElement) => {
|
const showUserMenu = (elem: HTMLElement) => {
|
||||||
const card = dom.parentWithClass(elem, 'card');
|
const card = dom.parentWithClass(elem, 'card');
|
||||||
const userId = card?.getAttribute('data-userid');
|
const userId = card?.getAttribute('data-userid');
|
||||||
|
const username = card?.getAttribute('data-username');
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
console.error('Unexpected null user id');
|
console.error('Unexpected null user id');
|
||||||
|
@ -58,7 +59,7 @@ const UserProfiles: FunctionComponent = () => {
|
||||||
const menuItems: MenuEntry[] = [];
|
const menuItems: MenuEntry[] = [];
|
||||||
|
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
name: globalize.translate('ButtonOpen'),
|
name: globalize.translate('ButtonEditUser'),
|
||||||
id: 'open',
|
id: 'open',
|
||||||
icon: 'mode_edit'
|
icon: 'mode_edit'
|
||||||
});
|
});
|
||||||
|
@ -106,7 +107,7 @@ const UserProfiles: FunctionComponent = () => {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'delete':
|
case 'delete':
|
||||||
deleteUser(userId);
|
deleteUser(userId, username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
@ -117,12 +118,13 @@ const UserProfiles: FunctionComponent = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteUser = (id: string) => {
|
const deleteUser = (id: string, username?: string | null) => {
|
||||||
const msg = globalize.translate('DeleteUserConfirmation');
|
const title = username ? globalize.translate('DeleteName', username) : globalize.translate('DeleteUser');
|
||||||
|
const text = globalize.translate('DeleteUserConfirmation');
|
||||||
|
|
||||||
confirm({
|
confirm({
|
||||||
title: globalize.translate('DeleteUser'),
|
title,
|
||||||
text: msg,
|
text,
|
||||||
confirmText: globalize.translate('Delete'),
|
confirmText: globalize.translate('Delete'),
|
||||||
primary: 'delete'
|
primary: 'delete'
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
|
|
|
@ -414,7 +414,6 @@ const UserParentalControl: FunctionComponent = () => {
|
||||||
return <AccessScheduleList
|
return <AccessScheduleList
|
||||||
key={accessSchedule.Id}
|
key={accessSchedule.Id}
|
||||||
index={index}
|
index={index}
|
||||||
Id={accessSchedule.Id}
|
|
||||||
DayOfWeek={accessSchedule.DayOfWeek}
|
DayOfWeek={accessSchedule.DayOfWeek}
|
||||||
StartHour={accessSchedule.StartHour}
|
StartHour={accessSchedule.StartHour}
|
||||||
EndHour={accessSchedule.EndHour}
|
EndHour={accessSchedule.EndHour}
|
||||||
|
|
|
@ -52,8 +52,7 @@ const UserEdit: FunctionComponent = () => {
|
||||||
const element = useRef<HTMLDivElement>(null);
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const triggerChange = (select: HTMLInputElement) => {
|
const triggerChange = (select: HTMLInputElement) => {
|
||||||
const evt = document.createEvent('HTMLEvents');
|
const evt = new Event('change', { bubbles: false, cancelable: true });
|
||||||
evt.initEvent('change', false, true);
|
|
||||||
select.dispatchEvent(evt);
|
select.dispatchEvent(evt);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -183,6 +182,7 @@ const UserEdit: FunctionComponent = () => {
|
||||||
(page.querySelector('.chkDisabled') as HTMLInputElement).checked = user.Policy.IsDisabled;
|
(page.querySelector('.chkDisabled') as HTMLInputElement).checked = user.Policy.IsDisabled;
|
||||||
(page.querySelector('.chkIsHidden') as HTMLInputElement).checked = user.Policy.IsHidden;
|
(page.querySelector('.chkIsHidden') as HTMLInputElement).checked = user.Policy.IsHidden;
|
||||||
(page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked = user.Policy.EnableCollectionManagement;
|
(page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked = user.Policy.EnableCollectionManagement;
|
||||||
|
(page.querySelector('.chkEnableSubtitleManagement') as HTMLInputElement).checked = user.Policy.EnableSubtitleManagement;
|
||||||
(page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = user.Policy.EnableSharedDeviceControl;
|
(page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = user.Policy.EnableSharedDeviceControl;
|
||||||
(page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = user.Policy.EnableRemoteControlOfOtherUsers;
|
(page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = user.Policy.EnableRemoteControlOfOtherUsers;
|
||||||
(page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = user.Policy.EnableContentDownloading;
|
(page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = user.Policy.EnableContentDownloading;
|
||||||
|
@ -241,6 +241,7 @@ const UserEdit: FunctionComponent = () => {
|
||||||
user.Policy.EnableVideoPlaybackTranscoding = (page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked;
|
user.Policy.EnableVideoPlaybackTranscoding = (page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked;
|
||||||
user.Policy.EnablePlaybackRemuxing = (page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked;
|
user.Policy.EnablePlaybackRemuxing = (page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked;
|
||||||
user.Policy.EnableCollectionManagement = (page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked;
|
user.Policy.EnableCollectionManagement = (page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked;
|
||||||
|
user.Policy.EnableSubtitleManagement = (page.querySelector('.chkEnableSubtitleManagement') as HTMLInputElement).checked;
|
||||||
user.Policy.ForceRemoteSourceTranscoding = (page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked;
|
user.Policy.ForceRemoteSourceTranscoding = (page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked;
|
||||||
user.Policy.EnableContentDownloading = (page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked;
|
user.Policy.EnableContentDownloading = (page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked;
|
||||||
user.Policy.EnableRemoteAccess = (page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked;
|
user.Policy.EnableRemoteAccess = (page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked;
|
||||||
|
@ -254,7 +255,7 @@ const UserEdit: FunctionComponent = () => {
|
||||||
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType;
|
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType;
|
||||||
|
|
||||||
window.ApiClient.updateUser(user).then(() => (
|
window.ApiClient.updateUser(user).then(() => (
|
||||||
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {})
|
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || { PasswordResetProviderId: '', AuthenticationProviderId: '' })
|
||||||
)).then(() => {
|
)).then(() => {
|
||||||
onSaveComplete();
|
onSaveComplete();
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
@ -393,6 +394,11 @@ const UserEdit: FunctionComponent = () => {
|
||||||
className='chkEnableCollectionManagement'
|
className='chkEnableCollectionManagement'
|
||||||
title='AllowCollectionManagement'
|
title='AllowCollectionManagement'
|
||||||
/>
|
/>
|
||||||
|
<CheckBoxElement
|
||||||
|
labelClassName='checkboxContainer'
|
||||||
|
className='chkEnableSubtitleManagement'
|
||||||
|
title='AllowSubtitleManagement'
|
||||||
|
/>
|
||||||
<div id='featureAccessFields' className='verticalSection'>
|
<div id='featureAccessFields' className='verticalSection'>
|
||||||
<h2 className='paperListLabel'>
|
<h2 className='paperListLabel'>
|
||||||
{globalize.translate('HeaderFeatureAccess')}
|
{globalize.translate('HeaderFeatureAccess')}
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
|
||||||
import { REDIRECTS } from 'apps/stable/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 './routes/asyncRoutes';
|
|
||||||
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
|
||||||
|
|
||||||
const ExperimentalApp = () => {
|
|
||||||
return (
|
|
||||||
<Routes>
|
|
||||||
<Route path='/*' element={<AppLayout />}>
|
|
||||||
{/* User routes */}
|
|
||||||
<Route element={<ConnectionRequired />}>
|
|
||||||
{ASYNC_USER_ROUTES.map(toAsyncPageRoute)}
|
|
||||||
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* Public routes */}
|
|
||||||
<Route element={<ConnectionRequired isUserRequired={false} />}>
|
|
||||||
<Route index element={<Navigate replace to='/home.html' />} />
|
|
||||||
|
|
||||||
{LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)}
|
|
||||||
</Route>
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* Redirects for old paths */}
|
|
||||||
{REDIRECTS.map(toRedirectRoute)}
|
|
||||||
|
|
||||||
{/* Ignore dashboard routes */}
|
|
||||||
{Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => (
|
|
||||||
<Route
|
|
||||||
key={key}
|
|
||||||
path={`/${path}/*`}
|
|
||||||
element={null}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Routes>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ExperimentalApp;
|
|
|
@ -1,46 +1,28 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import AppBar from '@mui/material/AppBar';
|
import AppBar from '@mui/material/AppBar';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { type Theme } from '@mui/material/styles';
|
||||||
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import { Outlet, useLocation } from 'react-router-dom';
|
import { Outlet, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import AppBody from 'components/AppBody';
|
import AppBody from 'components/AppBody';
|
||||||
import ElevationScroll from 'components/ElevationScroll';
|
import ElevationScroll from 'components/ElevationScroll';
|
||||||
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
|
||||||
|
|
||||||
import AppToolbar from './components/AppToolbar';
|
import AppToolbar from './components/AppToolbar';
|
||||||
import AppDrawer, { isDrawerPath } from './components/drawers/AppDrawer';
|
import AppDrawer, { isDrawerPath } from './components/drawers/AppDrawer';
|
||||||
|
|
||||||
import './AppOverrides.scss';
|
import './AppOverrides.scss';
|
||||||
|
|
||||||
interface ExperimentalAppSettings {
|
|
||||||
isDrawerPinned: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_EXPERIMENTAL_APP_SETTINGS: ExperimentalAppSettings = {
|
|
||||||
isDrawerPinned: false
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppLayout = () => {
|
const AppLayout = () => {
|
||||||
const [ appSettings, setAppSettings ] = useLocalStorage<ExperimentalAppSettings>('ExperimentalAppSettings', DEFAULT_EXPERIMENTAL_APP_SETTINGS);
|
const [ isDrawerActive, setIsDrawerActive ] = useState(false);
|
||||||
const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned);
|
|
||||||
const { user } = useApi();
|
const { user } = useApi();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const isDrawerAvailable = isDrawerPath(location.pathname);
|
const isMediumScreen = useMediaQuery((t: Theme) => t.breakpoints.up('md'));
|
||||||
const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user);
|
const isDrawerAvailable = isDrawerPath(location.pathname) && Boolean(user);
|
||||||
|
const isDrawerOpen = isDrawerActive && isDrawerAvailable;
|
||||||
useEffect(() => {
|
|
||||||
if (isDrawerActive !== appSettings.isDrawerPinned) {
|
|
||||||
setAppSettings({
|
|
||||||
...appSettings,
|
|
||||||
isDrawerPinned: isDrawerActive
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [ appSettings, isDrawerActive, setAppSettings ]);
|
|
||||||
|
|
||||||
const onToggleDrawer = useCallback(() => {
|
const onToggleDrawer = useCallback(() => {
|
||||||
setIsDrawerActive(!isDrawerActive);
|
setIsDrawerActive(!isDrawerActive);
|
||||||
|
@ -48,46 +30,43 @@ const AppLayout = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
<ElevationScroll elevate={isDrawerOpen}>
|
<ElevationScroll elevate={false}>
|
||||||
<AppBar
|
<AppBar
|
||||||
position='fixed'
|
position='fixed'
|
||||||
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.drawer + 1 }}
|
sx={{
|
||||||
|
width: {
|
||||||
|
xs: '100%',
|
||||||
|
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
|
||||||
|
},
|
||||||
|
ml: {
|
||||||
|
xs: 0,
|
||||||
|
md: isDrawerAvailable ? DRAWER_WIDTH : 0
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<AppToolbar
|
<AppToolbar
|
||||||
|
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
|
||||||
isDrawerOpen={isDrawerOpen}
|
isDrawerOpen={isDrawerOpen}
|
||||||
onDrawerButtonClick={onToggleDrawer}
|
onDrawerButtonClick={onToggleDrawer}
|
||||||
/>
|
/>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
</ElevationScroll>
|
</ElevationScroll>
|
||||||
|
|
||||||
<AppDrawer
|
{
|
||||||
open={isDrawerOpen}
|
isDrawerAvailable && (
|
||||||
onClose={onToggleDrawer}
|
<AppDrawer
|
||||||
onOpen={onToggleDrawer}
|
open={isDrawerOpen}
|
||||||
/>
|
onClose={onToggleDrawer}
|
||||||
|
onOpen={onToggleDrawer}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
component='main'
|
component='main'
|
||||||
sx={{
|
sx={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
flexGrow: 1,
|
flexGrow: 1
|
||||||
transition: theme.transitions.create('margin', {
|
|
||||||
easing: theme.transitions.easing.sharp,
|
|
||||||
duration: theme.transitions.duration.leavingScreen
|
|
||||||
}),
|
|
||||||
marginLeft: 0,
|
|
||||||
...(isDrawerAvailable && {
|
|
||||||
marginLeft: {
|
|
||||||
sm: `-${DRAWER_WIDTH}px`
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
...(isDrawerActive && {
|
|
||||||
transition: theme.transitions.create('margin', {
|
|
||||||
easing: theme.transitions.easing.easeOut,
|
|
||||||
duration: theme.transitions.duration.enteringScreen
|
|
||||||
}),
|
|
||||||
marginLeft: 0
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AppBody>
|
<AppBody>
|
||||||
|
|
|
@ -20,11 +20,15 @@ $mui-bp-xl: 1536px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix the padding of some pages
|
// Fix the padding of some pages
|
||||||
.homePage.libraryPage, // Home page
|
.homePage.libraryPage.withTabs, // Home page
|
||||||
.libraryPage:not(.withTabs) { // Tabless library pages
|
// Library pages excluding the item details page and tabbed pages
|
||||||
|
.libraryPage:not(
|
||||||
|
.itemDetailPage,
|
||||||
|
.withTabs
|
||||||
|
) {
|
||||||
padding-top: 3.25rem !important;
|
padding-top: 3.25rem !important;
|
||||||
}
|
}
|
||||||
|
// Tabbed library pages
|
||||||
.libraryPage.withTabs {
|
.libraryPage.withTabs {
|
||||||
padding-top: 6.5rem !important;
|
padding-top: 6.5rem !important;
|
||||||
|
|
||||||
|
|
|
@ -8,21 +8,23 @@ import AppToolbar from 'components/toolbar/AppToolbar';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import AppTabs from '../tabs/AppTabs';
|
import AppTabs from '../tabs/AppTabs';
|
||||||
import { isDrawerPath } from '../drawers/AppDrawer';
|
|
||||||
import RemotePlayButton from './RemotePlayButton';
|
import RemotePlayButton from './RemotePlayButton';
|
||||||
import SyncPlayButton from './SyncPlayButton';
|
import SyncPlayButton from './SyncPlayButton';
|
||||||
|
import { isTabPath } from '../tabs/tabRoutes';
|
||||||
|
|
||||||
interface AppToolbarProps {
|
interface AppToolbarProps {
|
||||||
|
isDrawerAvailable: boolean
|
||||||
isDrawerOpen: boolean
|
isDrawerOpen: boolean
|
||||||
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
|
const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
|
||||||
|
isDrawerAvailable,
|
||||||
isDrawerOpen,
|
isDrawerOpen,
|
||||||
onDrawerButtonClick
|
onDrawerButtonClick
|
||||||
}) => {
|
}) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isDrawerAvailable = isDrawerPath(location.pathname);
|
const isTabsAvailable = isTabPath(location.pathname);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppToolbar
|
<AppToolbar
|
||||||
|
@ -48,7 +50,7 @@ const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
|
||||||
isDrawerOpen={isDrawerOpen}
|
isDrawerOpen={isDrawerOpen}
|
||||||
onDrawerButtonClick={onDrawerButtonClick}
|
onDrawerButtonClick={onDrawerButtonClick}
|
||||||
>
|
>
|
||||||
<AppTabs isDrawerOpen={isDrawerOpen} />
|
{isTabsAvailable && (<AppTabs isDrawerOpen={isDrawerOpen} />)}
|
||||||
</AppToolbar>
|
</AppToolbar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Collections from '@mui/icons-material/Collections';
|
||||||
import Queue from '@mui/icons-material/Queue';
|
import Queue from '@mui/icons-material/Queue';
|
||||||
import Folder from '@mui/icons-material/Folder';
|
import Folder from '@mui/icons-material/Folder';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { CollectionType } from 'types/collectionType';
|
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
|
|
||||||
interface LibraryIconProps {
|
interface LibraryIconProps {
|
||||||
item: BaseItemDto
|
item: BaseItemDto
|
||||||
|
@ -25,20 +25,20 @@ const LibraryIcon: FC<LibraryIconProps> = ({
|
||||||
return <Movie />;
|
return <Movie />;
|
||||||
case CollectionType.Music:
|
case CollectionType.Music:
|
||||||
return <MusicNote />;
|
return <MusicNote />;
|
||||||
case CollectionType.HomeVideos:
|
case CollectionType.Homevideos:
|
||||||
case CollectionType.Photos:
|
case CollectionType.Photos:
|
||||||
return <Photo />;
|
return <Photo />;
|
||||||
case CollectionType.LiveTv:
|
case CollectionType.Livetv:
|
||||||
return <LiveTv />;
|
return <LiveTv />;
|
||||||
case CollectionType.TvShows:
|
case CollectionType.Tvshows:
|
||||||
return <Tv />;
|
return <Tv />;
|
||||||
case CollectionType.Trailers:
|
case CollectionType.Trailers:
|
||||||
return <Theaters />;
|
return <Theaters />;
|
||||||
case CollectionType.MusicVideos:
|
case CollectionType.Musicvideos:
|
||||||
return <MusicVideo />;
|
return <MusicVideo />;
|
||||||
case CollectionType.Books:
|
case CollectionType.Books:
|
||||||
return <Book />;
|
return <Book />;
|
||||||
case CollectionType.BoxSets:
|
case CollectionType.Boxsets:
|
||||||
return <Collections />;
|
return <Collections />;
|
||||||
case CollectionType.Playlists:
|
case CollectionType.Playlists:
|
||||||
return <Queue />;
|
return <Queue />;
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';
|
import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';
|
||||||
|
|
||||||
import { ASYNC_USER_ROUTES } from '../../routes/asyncRoutes';
|
import { ASYNC_USER_ROUTES } from '../../routes/asyncRoutes';
|
||||||
import { LEGACY_USER_ROUTES } from '../../routes/legacyRoutes';
|
import { LEGACY_USER_ROUTES } from '../../routes/legacyRoutes';
|
||||||
import { isTabPath } from '../tabs/tabRoutes';
|
|
||||||
|
|
||||||
import MainDrawerContent from './MainDrawerContent';
|
import MainDrawerContent from './MainDrawerContent';
|
||||||
|
|
||||||
|
@ -27,20 +25,14 @@ const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
||||||
open = false,
|
open = false,
|
||||||
onClose,
|
onClose,
|
||||||
onOpen
|
onOpen
|
||||||
}) => {
|
}) => (
|
||||||
const location = useLocation();
|
<ResponsiveDrawer
|
||||||
const hasSecondaryToolBar = isTabPath(location.pathname);
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
return (
|
onOpen={onOpen}
|
||||||
<ResponsiveDrawer
|
>
|
||||||
hasSecondaryToolBar={hasSecondaryToolBar}
|
<MainDrawerContent />
|
||||||
open={open}
|
</ResponsiveDrawer>
|
||||||
onClose={onClose}
|
);
|
||||||
onOpen={onOpen}
|
|
||||||
>
|
|
||||||
<MainDrawerContent />
|
|
||||||
</ResponsiveDrawer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppDrawer;
|
export default AppDrawer;
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useSystemInfo } from 'hooks/useSystemInfo';
|
||||||
|
import ListItemLink from 'components/ListItemLink';
|
||||||
|
|
||||||
|
import appIcon from 'assets/img/icon-transparent.png';
|
||||||
|
|
||||||
|
const DrawerHeaderLink = () => {
|
||||||
|
const { data: systemInfo } = useSystemInfo();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItemLink to='/'>
|
||||||
|
<ListItemIcon sx={{ minWidth: 56 }}>
|
||||||
|
<Box
|
||||||
|
component='img'
|
||||||
|
src={appIcon}
|
||||||
|
sx={{ height: '2.5rem' }}
|
||||||
|
/>
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={systemInfo?.ServerName || 'Jellyfin'}
|
||||||
|
primaryTypographyProps={{ variant: 'h6' }}
|
||||||
|
secondary={systemInfo?.Version}
|
||||||
|
/>
|
||||||
|
</ListItemLink>);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DrawerHeaderLink;
|
|
@ -1,68 +1,44 @@
|
||||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
|
||||||
import type { SystemInfo } from '@jellyfin/sdk/lib/generated-client/models/system-info';
|
|
||||||
import { getUserViewsApi } from '@jellyfin/sdk/lib/utils/api/user-views-api';
|
|
||||||
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
|
|
||||||
import Dashboard from '@mui/icons-material/Dashboard';
|
import Dashboard from '@mui/icons-material/Dashboard';
|
||||||
import Edit from '@mui/icons-material/Edit';
|
import Edit from '@mui/icons-material/Edit';
|
||||||
import Favorite from '@mui/icons-material/Favorite';
|
import Favorite from '@mui/icons-material/Favorite';
|
||||||
import Home from '@mui/icons-material/Home';
|
import Home from '@mui/icons-material/Home';
|
||||||
import Link from '@mui/icons-material/Link';
|
|
||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
|
import Icon from '@mui/material/Icon';
|
||||||
import List from '@mui/material/List';
|
import List from '@mui/material/List';
|
||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import ListItemButton from '@mui/material/ListItemButton';
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import ListSubheader from '@mui/material/ListSubheader';
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import ListItemLink from 'components/ListItemLink';
|
import ListItemLink from 'components/ListItemLink';
|
||||||
import { appRouter } from 'components/router/appRouter';
|
import { appRouter } from 'components/router/appRouter';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
|
import { useUserViews } from 'hooks/useUserViews';
|
||||||
import { useWebConfig } from 'hooks/useWebConfig';
|
import { useWebConfig } from 'hooks/useWebConfig';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import LibraryIcon from '../LibraryIcon';
|
import LibraryIcon from '../LibraryIcon';
|
||||||
|
import DrawerHeaderLink from './DrawerHeaderLink';
|
||||||
|
|
||||||
const MainDrawerContent = () => {
|
const MainDrawerContent = () => {
|
||||||
const { api, user } = useApi();
|
const { user } = useApi();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [ systemInfo, setSystemInfo ] = useState<SystemInfo>();
|
const { data: userViewsData } = useUserViews(user?.Id);
|
||||||
const [ userViews, setUserViews ] = useState<BaseItemDto[]>([]);
|
const userViews = userViewsData?.Items || [];
|
||||||
const webConfig = useWebConfig();
|
const webConfig = useWebConfig();
|
||||||
|
|
||||||
const isHomeSelected = location.pathname === '/home.html' && (!location.search || location.search === '?tab=0');
|
const isHomeSelected = location.pathname === '/home.html' && (!location.search || location.search === '?tab=0');
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (api && user?.Id) {
|
|
||||||
getUserViewsApi(api)
|
|
||||||
.getUserViews({ userId: user.Id })
|
|
||||||
.then(({ data }) => {
|
|
||||||
setUserViews(data.Items || []);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.warn('[MainDrawer] failed to fetch user views', err);
|
|
||||||
setUserViews([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
getSystemApi(api)
|
|
||||||
.getSystemInfo()
|
|
||||||
.then(({ data }) => {
|
|
||||||
setSystemInfo(data);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.warn('[MainDrawer] failed to fetch system info', err);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setUserViews([]);
|
|
||||||
}
|
|
||||||
}, [ api, user?.Id ]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* MAIN LINKS */}
|
{/* MAIN LINKS */}
|
||||||
<List>
|
<List sx={{ paddingTop: 0 }}>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<DrawerHeaderLink />
|
||||||
|
</ListItem>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/home.html' selected={isHomeSelected}>
|
<ListItemLink to='/home.html' selected={isHomeSelected}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
|
@ -98,8 +74,7 @@ const MainDrawerContent = () => {
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
{/* TODO: Support custom icons */}
|
<Icon>{menuLink.icon ?? 'link'}</Icon>
|
||||||
<Link />
|
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={menuLink.name} />
|
<ListItemText primary={menuLink.name} />
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
|
@ -168,17 +143,6 @@ const MainDrawerContent = () => {
|
||||||
</List>
|
</List>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* FOOTER */}
|
|
||||||
<Divider style={{ marginTop: 'auto' }} />
|
|
||||||
<List>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText
|
|
||||||
primary={systemInfo?.ServerName ? systemInfo.ServerName : 'Jellyfin'}
|
|
||||||
secondary={systemInfo?.Version ? `v${systemInfo.Version}` : ''}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { useGetGenres } from 'hooks/useFetchItems';
|
import { useGetGenres } from 'hooks/useFetchItems';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
import Loading from 'components/loading/LoadingComponent';
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
import GenresSectionContainer from './GenresSectionContainer';
|
import GenresSectionContainer from './GenresSectionContainer';
|
||||||
import { CollectionType } from 'types/collectionType';
|
import type { ParentId } from 'types/library';
|
||||||
import { ParentId } from 'types/library';
|
|
||||||
|
|
||||||
interface GenresItemsContainerProps {
|
interface GenresItemsContainerProps {
|
||||||
parentId: ParentId;
|
parentId: ParentId;
|
||||||
collectionType: CollectionType;
|
collectionType: CollectionType | undefined;
|
||||||
itemType: BaseItemKind;
|
itemType: BaseItemKind[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const GenresItemsContainer: FC<GenresItemsContainerProps> = ({
|
const GenresItemsContainer: FC<GenresItemsContainerProps> = ({
|
||||||
|
@ -18,33 +18,32 @@ const GenresItemsContainer: FC<GenresItemsContainerProps> = ({
|
||||||
collectionType,
|
collectionType,
|
||||||
itemType
|
itemType
|
||||||
}) => {
|
}) => {
|
||||||
const { isLoading, data: genresResult } = useGetGenres(
|
const { isLoading, data: genresResult } = useGetGenres(itemType, parentId);
|
||||||
itemType,
|
|
||||||
parentId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!genresResult?.Items?.length) {
|
||||||
|
return (
|
||||||
|
<div className='noItemsMessage centerMessage'>
|
||||||
|
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||||
|
<p>{globalize.translate('MessageNoGenresAvailable')}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!genresResult?.Items?.length ? (
|
{genresResult.Items.map((genre) => (
|
||||||
<div className='noItemsMessage centerMessage'>
|
<GenresSectionContainer
|
||||||
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
key={genre.Id}
|
||||||
<p>{globalize.translate('MessageNoGenresAvailable')}</p>
|
collectionType={collectionType}
|
||||||
</div>
|
parentId={parentId}
|
||||||
) : (
|
itemType={itemType}
|
||||||
genresResult?.Items?.map((genre) => (
|
genre={genre}
|
||||||
<GenresSectionContainer
|
/>
|
||||||
key={genre.Id}
|
))}
|
||||||
collectionType={collectionType}
|
|
||||||
parentId={parentId}
|
|
||||||
itemType={itemType}
|
|
||||||
genre={genre}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,23 +1,22 @@
|
||||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
|
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
|
||||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||||
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
|
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
|
||||||
import escapeHTML from 'escape-html';
|
import React, { type FC } from 'react';
|
||||||
import React, { FC } from 'react';
|
|
||||||
|
|
||||||
import { useGetItems } from 'hooks/useFetchItems';
|
import { useGetItems } from 'hooks/useFetchItems';
|
||||||
import Loading from 'components/loading/LoadingComponent';
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
import { appRouter } from 'components/router/appRouter';
|
import { appRouter } from 'components/router/appRouter';
|
||||||
import SectionContainer from './SectionContainer';
|
import SectionContainer from './SectionContainer';
|
||||||
import { CollectionType } from 'types/collectionType';
|
import { CardShape } from 'utils/card';
|
||||||
import { ParentId } from 'types/library';
|
import type { ParentId } from 'types/library';
|
||||||
|
|
||||||
interface GenresSectionContainerProps {
|
interface GenresSectionContainerProps {
|
||||||
parentId: ParentId;
|
parentId: ParentId;
|
||||||
collectionType: CollectionType;
|
collectionType: CollectionType | undefined;
|
||||||
itemType: BaseItemKind;
|
itemType: BaseItemKind[];
|
||||||
genre: BaseItemDto;
|
genre: BaseItemDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,12 +30,11 @@ const GenresSectionContainer: FC<GenresSectionContainerProps> = ({
|
||||||
return {
|
return {
|
||||||
sortBy: [ItemSortBy.Random],
|
sortBy: [ItemSortBy.Random],
|
||||||
sortOrder: [SortOrder.Ascending],
|
sortOrder: [SortOrder.Ascending],
|
||||||
includeItemTypes: [itemType],
|
includeItemTypes: itemType,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
fields: [
|
fields: [
|
||||||
ItemFields.PrimaryImageAspectRatio,
|
ItemFields.PrimaryImageAspectRatio,
|
||||||
ItemFields.MediaSourceCount,
|
ItemFields.MediaSourceCount
|
||||||
ItemFields.BasicSyncInfo
|
|
||||||
],
|
],
|
||||||
imageTypeLimit: 1,
|
imageTypeLimit: 1,
|
||||||
enableImageTypes: [ImageType.Primary],
|
enableImageTypes: [ImageType.Primary],
|
||||||
|
@ -61,7 +59,7 @@ const GenresSectionContainer: FC<GenresSectionContainerProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return <SectionContainer
|
return <SectionContainer
|
||||||
sectionTitle={escapeHTML(genre.Name)}
|
sectionTitle={genre.Name || ''}
|
||||||
items={itemsResult?.Items || []}
|
items={itemsResult?.Items || []}
|
||||||
url={getRouteUrl(genre)}
|
url={getRouteUrl(genre)}
|
||||||
cardOptions={{
|
cardOptions={{
|
||||||
|
@ -70,9 +68,9 @@ const GenresSectionContainer: FC<GenresSectionContainerProps> = ({
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
centerText: true,
|
centerText: true,
|
||||||
cardLayout: false,
|
cardLayout: false,
|
||||||
shape: itemType === BaseItemKind.MusicAlbum ? 'overflowSquare' : 'overflowPortrait',
|
shape: collectionType === CollectionType.Music ? CardShape.SquareOverflow : CardShape.PortraitOverflow,
|
||||||
showParentTitle: itemType === BaseItemKind.MusicAlbum,
|
showParentTitle: collectionType === CollectionType.Music,
|
||||||
showYear: itemType !== BaseItemKind.MusicAlbum
|
showYear: collectionType !== CollectionType.Music
|
||||||
}}
|
}}
|
||||||
/>;
|
/>;
|
||||||
};
|
};
|
||||||
|
|
23
src/apps/experimental/components/library/GenresView.tsx
Normal file
23
src/apps/experimental/components/library/GenresView.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import GenresItemsContainer from './GenresItemsContainer';
|
||||||
|
import type { ParentId } from 'types/library';
|
||||||
|
|
||||||
|
interface GenresViewProps {
|
||||||
|
parentId: ParentId;
|
||||||
|
collectionType: CollectionType | undefined;
|
||||||
|
itemType: BaseItemKind[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const GenresView: FC<GenresViewProps> = ({ parentId, collectionType, itemType }) => {
|
||||||
|
return (
|
||||||
|
<GenresItemsContainer
|
||||||
|
parentId={parentId}
|
||||||
|
collectionType={collectionType}
|
||||||
|
itemType={itemType}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GenresView;
|
51
src/apps/experimental/components/library/GuideView.tsx
Normal file
51
src/apps/experimental/components/library/GuideView.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||||
|
import Guide from 'components/guide/guide';
|
||||||
|
import 'material-design-icons-iconfont';
|
||||||
|
import 'elements/emby-programcell/emby-programcell';
|
||||||
|
import 'elements/emby-button/emby-button';
|
||||||
|
import 'elements/emby-button/paper-icon-button-light';
|
||||||
|
import 'elements/emby-tabs/emby-tabs';
|
||||||
|
import 'elements/emby-scroller/emby-scroller';
|
||||||
|
import 'components/guide/guide.scss';
|
||||||
|
import 'components/guide/programs.scss';
|
||||||
|
import 'styles/scrollstyles.scss';
|
||||||
|
import 'styles/flexstyles.scss';
|
||||||
|
|
||||||
|
const GuideView: FC = () => {
|
||||||
|
const guideInstance = useRef<Guide | null>();
|
||||||
|
const tvGuideContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const initGuide = useCallback((element: HTMLDivElement) => {
|
||||||
|
guideInstance.current = new Guide({
|
||||||
|
element: element,
|
||||||
|
serverId: window.ApiClient.serverId()
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = tvGuideContainerRef.current;
|
||||||
|
if (!element) {
|
||||||
|
console.error('Unexpected null reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!guideInstance.current) {
|
||||||
|
initGuide(element);
|
||||||
|
}
|
||||||
|
}, [initGuide]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (guideInstance.current) {
|
||||||
|
guideInstance.current.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (guideInstance.current) {
|
||||||
|
guideInstance.current.pause();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [initGuide]);
|
||||||
|
|
||||||
|
return <div ref={tvGuideContainerRef} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GuideView;
|
|
@ -1,33 +0,0 @@
|
||||||
import React, { FC, useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
import ItemsContainerElement from 'elements/ItemsContainerElement';
|
|
||||||
import imageLoader from 'components/images/imageLoader';
|
|
||||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
|
||||||
import { LibraryViewSettings, ViewMode } from 'types/library';
|
|
||||||
|
|
||||||
interface ItemsContainerI {
|
|
||||||
libraryViewSettings: LibraryViewSettings;
|
|
||||||
getItemsHtml: () => string
|
|
||||||
}
|
|
||||||
|
|
||||||
const ItemsContainer: FC<ItemsContainerI> = ({ libraryViewSettings, getItemsHtml }) => {
|
|
||||||
const element = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const itemsContainer = element.current?.querySelector('.itemsContainer') as HTMLDivElement;
|
|
||||||
itemsContainer.innerHTML = getItemsHtml();
|
|
||||||
imageLoader.lazyChildren(itemsContainer);
|
|
||||||
}, [getItemsHtml]);
|
|
||||||
|
|
||||||
const cssClass = libraryViewSettings.ViewMode === ViewMode.ListView ? 'vertical-list' : 'vertical-wrap';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={element}>
|
|
||||||
<ItemsContainerElement
|
|
||||||
className={`itemsContainer ${cssClass} centered padded-left padded-right padded-right-withalphapicker`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ItemsContainer;
|
|
|
@ -1,19 +1,19 @@
|
||||||
import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client';
|
import { ImageType } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { type FC, useCallback } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||||
import { useGetItem, useGetItemsViewByType } from 'hooks/useFetchItems';
|
import { useGetItem, useGetItemsViewByType } from 'hooks/useFetchItems';
|
||||||
import { getDefaultLibraryViewSettings, getSettingsKey } from 'utils/items';
|
import { getDefaultLibraryViewSettings, getSettingsKey } from 'utils/items';
|
||||||
|
import { CardShape } from 'utils/card';
|
||||||
import Loading from 'components/loading/LoadingComponent';
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
import listview from 'components/listview/listview';
|
|
||||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
|
||||||
import { playbackManager } from 'components/playback/playbackmanager';
|
import { playbackManager } from 'components/playback/playbackmanager';
|
||||||
import globalize from 'scripts/globalize';
|
import ItemsContainer from 'elements/emby-itemscontainer/ItemsContainer';
|
||||||
import AlphabetPicker from './AlphabetPicker';
|
import AlphabetPicker from './AlphabetPicker';
|
||||||
import FilterButton from './filter/FilterButton';
|
import FilterButton from './filter/FilterButton';
|
||||||
import ItemsContainer from './ItemsContainer';
|
|
||||||
import NewCollectionButton from './NewCollectionButton';
|
import NewCollectionButton from './NewCollectionButton';
|
||||||
import Pagination from './Pagination';
|
import Pagination from './Pagination';
|
||||||
import PlayAllButton from './PlayAllButton';
|
import PlayAllButton from './PlayAllButton';
|
||||||
|
@ -21,17 +21,20 @@ import QueueButton from './QueueButton';
|
||||||
import ShuffleButton from './ShuffleButton';
|
import ShuffleButton from './ShuffleButton';
|
||||||
import SortButton from './SortButton';
|
import SortButton from './SortButton';
|
||||||
import GridListViewButton from './GridListViewButton';
|
import GridListViewButton from './GridListViewButton';
|
||||||
import { LibraryViewSettings, ParentId, ViewMode } from 'types/library';
|
import NoItemsMessage from 'components/common/NoItemsMessage';
|
||||||
import { CollectionType } from 'types/collectionType';
|
import Lists from 'components/listview/List/Lists';
|
||||||
|
import Cards from 'components/cardbuilder/Card/Cards';
|
||||||
import { LibraryTab } from 'types/libraryTab';
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
import { type LibraryViewSettings, type ParentId, ViewMode } from 'types/library';
|
||||||
import { CardOptions } from 'types/cardOptions';
|
import type { CardOptions } from 'types/cardOptions';
|
||||||
|
import type { ListOptions } from 'types/listOptions';
|
||||||
|
|
||||||
interface ItemsViewProps {
|
interface ItemsViewProps {
|
||||||
viewType: LibraryTab;
|
viewType: LibraryTab;
|
||||||
parentId: ParentId;
|
parentId: ParentId;
|
||||||
itemType: BaseItemKind[];
|
itemType: BaseItemKind[];
|
||||||
collectionType?: CollectionType;
|
collectionType?: CollectionType;
|
||||||
|
isPaginationEnabled?: boolean;
|
||||||
isBtnPlayAllEnabled?: boolean;
|
isBtnPlayAllEnabled?: boolean;
|
||||||
isBtnQueueEnabled?: boolean;
|
isBtnQueueEnabled?: boolean;
|
||||||
isBtnShuffleEnabled?: boolean;
|
isBtnShuffleEnabled?: boolean;
|
||||||
|
@ -47,6 +50,7 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
viewType,
|
viewType,
|
||||||
parentId,
|
parentId,
|
||||||
collectionType,
|
collectionType,
|
||||||
|
isPaginationEnabled = true,
|
||||||
isBtnPlayAllEnabled = false,
|
isBtnPlayAllEnabled = false,
|
||||||
isBtnQueueEnabled = false,
|
isBtnQueueEnabled = false,
|
||||||
isBtnShuffleEnabled = false,
|
isBtnShuffleEnabled = false,
|
||||||
|
@ -67,7 +71,8 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
const {
|
const {
|
||||||
isLoading,
|
isLoading,
|
||||||
data: itemsResult,
|
data: itemsResult,
|
||||||
isPreviousData
|
isPreviousData,
|
||||||
|
refetch
|
||||||
} = useGetItemsViewByType(
|
} = useGetItemsViewByType(
|
||||||
viewType,
|
viewType,
|
||||||
parentId,
|
parentId,
|
||||||
|
@ -76,26 +81,47 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
);
|
);
|
||||||
const { data: item } = useGetItem(parentId);
|
const { data: item } = useGetItem(parentId);
|
||||||
|
|
||||||
|
const getListOptions = useCallback(() => {
|
||||||
|
const listOptions: ListOptions = {
|
||||||
|
items: itemsResult?.Items ?? [],
|
||||||
|
context: collectionType
|
||||||
|
};
|
||||||
|
|
||||||
|
if (viewType === LibraryTab.Songs) {
|
||||||
|
listOptions.showParentTitle = true;
|
||||||
|
listOptions.action = 'playallfromhere';
|
||||||
|
listOptions.smallIcon = true;
|
||||||
|
listOptions.artist = true;
|
||||||
|
listOptions.addToListButton = true;
|
||||||
|
} else if (viewType === LibraryTab.Albums) {
|
||||||
|
listOptions.sortBy = libraryViewSettings.SortBy;
|
||||||
|
listOptions.addToListButton = true;
|
||||||
|
} else if (viewType === LibraryTab.Episodes) {
|
||||||
|
listOptions.showParentTitle = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return listOptions;
|
||||||
|
}, [itemsResult?.Items, collectionType, viewType, libraryViewSettings.SortBy]);
|
||||||
|
|
||||||
const getCardOptions = useCallback(() => {
|
const getCardOptions = useCallback(() => {
|
||||||
let shape;
|
let shape;
|
||||||
let preferThumb;
|
let preferThumb;
|
||||||
let preferDisc;
|
let preferDisc;
|
||||||
let preferLogo;
|
let preferLogo;
|
||||||
let lines = libraryViewSettings.ShowTitle ? 2 : 0;
|
|
||||||
|
|
||||||
if (libraryViewSettings.ImageType === ImageType.Banner) {
|
if (libraryViewSettings.ImageType === ImageType.Banner) {
|
||||||
shape = 'banner';
|
shape = CardShape.Banner;
|
||||||
} else if (libraryViewSettings.ImageType === ImageType.Disc) {
|
} else if (libraryViewSettings.ImageType === ImageType.Disc) {
|
||||||
shape = 'square';
|
shape = CardShape.Square;
|
||||||
preferDisc = true;
|
preferDisc = true;
|
||||||
} else if (libraryViewSettings.ImageType === ImageType.Logo) {
|
} else if (libraryViewSettings.ImageType === ImageType.Logo) {
|
||||||
shape = 'backdrop';
|
shape = CardShape.Backdrop;
|
||||||
preferLogo = true;
|
preferLogo = true;
|
||||||
} else if (libraryViewSettings.ImageType === ImageType.Thumb) {
|
} else if (libraryViewSettings.ImageType === ImageType.Thumb) {
|
||||||
shape = 'backdrop';
|
shape = CardShape.Backdrop;
|
||||||
preferThumb = true;
|
preferThumb = true;
|
||||||
} else {
|
} else {
|
||||||
shape = 'auto';
|
shape = CardShape.Auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardOptions: CardOptions = {
|
const cardOptions: CardOptions = {
|
||||||
|
@ -109,9 +135,9 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
preferThumb: preferThumb,
|
preferThumb: preferThumb,
|
||||||
preferDisc: preferDisc,
|
preferDisc: preferDisc,
|
||||||
preferLogo: preferLogo,
|
preferLogo: preferLogo,
|
||||||
overlayPlayButton: false,
|
overlayText: !libraryViewSettings.ShowTitle,
|
||||||
overlayMoreButton: true,
|
imageType: libraryViewSettings.ImageType,
|
||||||
overlayText: !libraryViewSettings.ShowTitle
|
queryKey: ['ItemsViewByType']
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -120,13 +146,28 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
|| viewType === LibraryTab.Episodes
|
|| viewType === LibraryTab.Episodes
|
||||||
) {
|
) {
|
||||||
cardOptions.showParentTitle = libraryViewSettings.ShowTitle;
|
cardOptions.showParentTitle = libraryViewSettings.ShowTitle;
|
||||||
|
cardOptions.overlayPlayButton = true;
|
||||||
} else if (viewType === LibraryTab.Artists) {
|
} else if (viewType === LibraryTab.Artists) {
|
||||||
|
cardOptions.lines = 1;
|
||||||
cardOptions.showYear = false;
|
cardOptions.showYear = false;
|
||||||
lines = 1;
|
cardOptions.overlayPlayButton = true;
|
||||||
|
} else if (viewType === LibraryTab.Channels) {
|
||||||
|
cardOptions.shape = CardShape.Square;
|
||||||
|
cardOptions.showDetailsMenu = true;
|
||||||
|
cardOptions.showCurrentProgram = true;
|
||||||
|
cardOptions.showCurrentProgramTime = true;
|
||||||
|
} else if (viewType === LibraryTab.SeriesTimers) {
|
||||||
|
cardOptions.shape = CardShape.Backdrop;
|
||||||
|
cardOptions.showSeriesTimerTime = true;
|
||||||
|
cardOptions.showSeriesTimerChannel = true;
|
||||||
|
cardOptions.overlayMoreButton = true;
|
||||||
|
cardOptions.lines = 3;
|
||||||
|
} else if (viewType === LibraryTab.Movies) {
|
||||||
|
cardOptions.overlayPlayButton = true;
|
||||||
|
} else if (viewType === LibraryTab.Series || viewType === LibraryTab.Networks) {
|
||||||
|
cardOptions.overlayMoreButton = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
cardOptions.lines = lines;
|
|
||||||
|
|
||||||
return cardOptions;
|
return cardOptions;
|
||||||
}, [
|
}, [
|
||||||
libraryViewSettings.ShowTitle,
|
libraryViewSettings.ShowTitle,
|
||||||
|
@ -137,33 +178,29 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
viewType
|
viewType
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const getItemsHtml = useCallback(() => {
|
const getItems = useCallback(() => {
|
||||||
let html = '';
|
if (!itemsResult?.Items?.length) {
|
||||||
|
return <NoItemsMessage noItemsMessage={noItemsMessage} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (libraryViewSettings.ViewMode === ViewMode.ListView) {
|
if (libraryViewSettings.ViewMode === ViewMode.ListView) {
|
||||||
html = listview.getListViewHtml({
|
return (
|
||||||
items: itemsResult?.Items ?? [],
|
<Lists
|
||||||
context: collectionType
|
items={itemsResult?.Items ?? []}
|
||||||
});
|
listOptions={getListOptions()}
|
||||||
} else {
|
/>
|
||||||
html = cardBuilder.getCardsHtml(
|
|
||||||
itemsResult?.Items ?? [],
|
|
||||||
getCardOptions()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
if (!itemsResult?.Items?.length) {
|
<Cards
|
||||||
html += '<div class="noItemsMessage centerMessage">';
|
items={itemsResult?.Items ?? []}
|
||||||
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
|
cardOptions={getCardOptions()}
|
||||||
html += '<p>' + globalize.translate(noItemsMessage) + '</p>';
|
/>
|
||||||
html += '</div>';
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}, [
|
}, [
|
||||||
libraryViewSettings.ViewMode,
|
libraryViewSettings.ViewMode,
|
||||||
itemsResult?.Items,
|
itemsResult?.Items,
|
||||||
collectionType,
|
getListOptions,
|
||||||
getCardOptions,
|
getCardOptions,
|
||||||
noItemsMessage
|
noItemsMessage
|
||||||
]);
|
]);
|
||||||
|
@ -177,15 +214,23 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
ItemSortBy.SortName
|
ItemSortBy.SortName
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const itemsContainerClass = classNames(
|
||||||
|
'centered padded-left padded-right padded-right-withalphapicker',
|
||||||
|
libraryViewSettings.ViewMode === ViewMode.ListView ?
|
||||||
|
'vertical-list' :
|
||||||
|
'vertical-wrap'
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
||||||
<Pagination
|
{isPaginationEnabled && (
|
||||||
totalRecordCount={totalRecordCount}
|
<Pagination
|
||||||
libraryViewSettings={libraryViewSettings}
|
totalRecordCount={totalRecordCount}
|
||||||
isPreviousData={isPreviousData}
|
libraryViewSettings={libraryViewSettings}
|
||||||
setLibraryViewSettings={setLibraryViewSettings}
|
isPreviousData={isPreviousData}
|
||||||
/>
|
setLibraryViewSettings={setLibraryViewSettings}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{isBtnPlayAllEnabled && (
|
{isBtnPlayAllEnabled && (
|
||||||
<PlayAllButton
|
<PlayAllButton
|
||||||
|
@ -252,19 +297,25 @@ const ItemsView: FC<ItemsViewProps> = ({
|
||||||
<Loading />
|
<Loading />
|
||||||
) : (
|
) : (
|
||||||
<ItemsContainer
|
<ItemsContainer
|
||||||
libraryViewSettings={libraryViewSettings}
|
className={itemsContainerClass}
|
||||||
getItemsHtml={getItemsHtml}
|
parentId={parentId}
|
||||||
/>
|
reloadItems={refetch}
|
||||||
|
queryKey={['ItemsViewByType']}
|
||||||
|
>
|
||||||
|
{getItems()}
|
||||||
|
</ItemsContainer>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
{isPaginationEnabled && (
|
||||||
<Pagination
|
<Box className='flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x'>
|
||||||
totalRecordCount={totalRecordCount}
|
<Pagination
|
||||||
libraryViewSettings={libraryViewSettings}
|
totalRecordCount={totalRecordCount}
|
||||||
isPreviousData={isPreviousData}
|
libraryViewSettings={libraryViewSettings}
|
||||||
setLibraryViewSettings={setLibraryViewSettings}
|
isPreviousData={isPreviousData}
|
||||||
/>
|
setLibraryViewSettings={setLibraryViewSettings}
|
||||||
</Box>
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
84
src/apps/experimental/components/library/PageTabContent.tsx
Normal file
84
src/apps/experimental/components/library/PageTabContent.tsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import React, { type FC } from 'react';
|
||||||
|
import SuggestionsSectionView from './SuggestionsSectionView';
|
||||||
|
import UpcomingView from './UpcomingView';
|
||||||
|
import GenresView from './GenresView';
|
||||||
|
import ItemsView from './ItemsView';
|
||||||
|
import GuideView from './GuideView';
|
||||||
|
import ProgramsSectionView from './ProgramsSectionView';
|
||||||
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
import type { ParentId } from 'types/library';
|
||||||
|
import type { LibraryTabContent } from 'types/libraryTabContent';
|
||||||
|
|
||||||
|
interface PageTabContentProps {
|
||||||
|
parentId: ParentId;
|
||||||
|
currentTab: LibraryTabContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageTabContent: FC<PageTabContentProps> = ({ parentId, currentTab }) => {
|
||||||
|
if (currentTab.viewType === LibraryTab.Suggestions) {
|
||||||
|
return (
|
||||||
|
<SuggestionsSectionView
|
||||||
|
parentId={parentId}
|
||||||
|
sectionType={
|
||||||
|
currentTab.sectionsView?.suggestionSections ?? []
|
||||||
|
}
|
||||||
|
isMovieRecommendationEnabled={
|
||||||
|
currentTab.sectionsView?.isMovieRecommendations
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTab.viewType === LibraryTab.Programs || currentTab.viewType === LibraryTab.Recordings || currentTab.viewType === LibraryTab.Schedule) {
|
||||||
|
return (
|
||||||
|
<ProgramsSectionView
|
||||||
|
parentId={parentId}
|
||||||
|
sectionType={
|
||||||
|
currentTab.sectionsView?.programSections ?? []
|
||||||
|
}
|
||||||
|
isUpcomingRecordingsEnabled={currentTab.sectionsView?.isLiveTvUpcomingRecordings}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTab.viewType === LibraryTab.Upcoming) {
|
||||||
|
return <UpcomingView parentId={parentId} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTab.viewType === LibraryTab.Genres) {
|
||||||
|
return (
|
||||||
|
<GenresView
|
||||||
|
parentId={parentId}
|
||||||
|
collectionType={currentTab.collectionType}
|
||||||
|
itemType={currentTab.itemType || []}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTab.viewType === LibraryTab.Guide) {
|
||||||
|
return <GuideView />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemsView
|
||||||
|
viewType={currentTab.viewType}
|
||||||
|
parentId={parentId}
|
||||||
|
collectionType={currentTab.collectionType}
|
||||||
|
isPaginationEnabled={currentTab.isPaginationEnabled}
|
||||||
|
isBtnPlayAllEnabled={currentTab.isBtnPlayAllEnabled}
|
||||||
|
isBtnQueueEnabled={currentTab.isBtnQueueEnabled}
|
||||||
|
isBtnShuffleEnabled={currentTab.isBtnShuffleEnabled}
|
||||||
|
isBtnNewCollectionEnabled={currentTab.isBtnNewCollectionEnabled}
|
||||||
|
isBtnFilterEnabled={currentTab.isBtnFilterEnabled}
|
||||||
|
isBtnGridListEnabled={currentTab.isBtnGridListEnabled}
|
||||||
|
isBtnSortEnabled={currentTab.isBtnSortEnabled}
|
||||||
|
isAlphabetPickerEnabled={currentTab.isAlphabetPickerEnabled}
|
||||||
|
itemType={currentTab.itemType || []}
|
||||||
|
noItemsMessage={
|
||||||
|
currentTab.noItemsMessage || 'MessageNoItemsAvailable'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageTabContent;
|
|
@ -1,4 +1,4 @@
|
||||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
import type { BaseItemDto, SeriesTimerInfoDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import { IconButton } from '@mui/material';
|
import { IconButton } from '@mui/material';
|
||||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||||
|
@ -10,8 +10,8 @@ import { LibraryViewSettings } from 'types/library';
|
||||||
import { LibraryTab } from 'types/libraryTab';
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
|
||||||
interface PlayAllButtonProps {
|
interface PlayAllButtonProps {
|
||||||
item: BaseItemDto | undefined;
|
item: BaseItemDto | null | undefined;
|
||||||
items: BaseItemDto[];
|
items: BaseItemDto[] | SeriesTimerInfoDto[];
|
||||||
viewType: LibraryTab;
|
viewType: LibraryTab;
|
||||||
hasFilters: boolean;
|
hasFilters: boolean;
|
||||||
libraryViewSettings: LibraryViewSettings
|
libraryViewSettings: LibraryViewSettings
|
||||||
|
|
102
src/apps/experimental/components/library/ProgramsSectionView.tsx
Normal file
102
src/apps/experimental/components/library/ProgramsSectionView.tsx
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import React, { type FC } from 'react';
|
||||||
|
import { useGetProgramsSectionsWithItems, useGetTimers } from 'hooks/useFetchItems';
|
||||||
|
import { appRouter } from 'components/router/appRouter';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
|
import SectionContainer from './SectionContainer';
|
||||||
|
import { CardShape } from 'utils/card';
|
||||||
|
import type { ParentId } from 'types/library';
|
||||||
|
import type { Section, SectionType } from 'types/sections';
|
||||||
|
|
||||||
|
interface ProgramsSectionViewProps {
|
||||||
|
parentId: ParentId;
|
||||||
|
sectionType: SectionType[];
|
||||||
|
isUpcomingRecordingsEnabled: boolean | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProgramsSectionView: FC<ProgramsSectionViewProps> = ({
|
||||||
|
parentId,
|
||||||
|
sectionType,
|
||||||
|
isUpcomingRecordingsEnabled = false
|
||||||
|
}) => {
|
||||||
|
const { isLoading, data: sectionsWithItems, refetch } = useGetProgramsSectionsWithItems(parentId, sectionType);
|
||||||
|
const {
|
||||||
|
isLoading: isUpcomingRecordingsLoading,
|
||||||
|
data: upcomingRecordings
|
||||||
|
} = useGetTimers(isUpcomingRecordingsEnabled);
|
||||||
|
|
||||||
|
if (isLoading || isUpcomingRecordingsLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sectionsWithItems?.length && !upcomingRecordings?.length) {
|
||||||
|
return (
|
||||||
|
<div className='noItemsMessage centerMessage'>
|
||||||
|
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||||
|
<p>
|
||||||
|
{globalize.translate('MessageNoItemsAvailable')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRouteUrl = (section: Section) => {
|
||||||
|
return appRouter.getRouteUrl('list', {
|
||||||
|
serverId: window.ApiClient.serverId(),
|
||||||
|
itemTypes: section.itemTypes,
|
||||||
|
isAiring: section.parametersOptions?.isAiring,
|
||||||
|
isMovie: section.parametersOptions?.isMovie,
|
||||||
|
isSports: section.parametersOptions?.isSports,
|
||||||
|
isKids: section.parametersOptions?.isKids,
|
||||||
|
isNews: section.parametersOptions?.isNews,
|
||||||
|
isSeries: section.parametersOptions?.isSeries
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{sectionsWithItems?.map(({ section, items }) => (
|
||||||
|
<SectionContainer
|
||||||
|
key={section.type}
|
||||||
|
sectionTitle={globalize.translate(section.name)}
|
||||||
|
items={items ?? []}
|
||||||
|
url={getRouteUrl(section)}
|
||||||
|
reloadItems={refetch}
|
||||||
|
cardOptions={{
|
||||||
|
...section.cardOptions,
|
||||||
|
queryKey: ['ProgramSectionWithItems']
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
))}
|
||||||
|
|
||||||
|
{upcomingRecordings?.map((group) => (
|
||||||
|
<SectionContainer
|
||||||
|
key={group.name}
|
||||||
|
sectionTitle={group.name}
|
||||||
|
items={group.timerInfo ?? []}
|
||||||
|
cardOptions={{
|
||||||
|
queryKey: ['Timers'],
|
||||||
|
shape: CardShape.BackdropOverflow,
|
||||||
|
showTitle: true,
|
||||||
|
showParentTitleOrTitle: true,
|
||||||
|
showAirTime: true,
|
||||||
|
showAirEndTime: true,
|
||||||
|
showChannelName: false,
|
||||||
|
cardLayout: true,
|
||||||
|
centerText: false,
|
||||||
|
action: 'edit',
|
||||||
|
cardFooterAside: 'none',
|
||||||
|
preferThumb: true,
|
||||||
|
coverImage: true,
|
||||||
|
allowBottomPadding: false,
|
||||||
|
overlayText: false,
|
||||||
|
showChannelLogo: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProgramsSectionView;
|
|
@ -1,4 +1,4 @@
|
||||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
import type { BaseItemDto, SeriesTimerInfoDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import { IconButton } from '@mui/material';
|
import { IconButton } from '@mui/material';
|
||||||
import QueueIcon from '@mui/icons-material/Queue';
|
import QueueIcon from '@mui/icons-material/Queue';
|
||||||
|
@ -8,7 +8,7 @@ import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
interface QueueButtonProps {
|
interface QueueButtonProps {
|
||||||
item: BaseItemDto | undefined
|
item: BaseItemDto | undefined
|
||||||
items: BaseItemDto[];
|
items: BaseItemDto[] | SeriesTimerInfoDto[];
|
||||||
hasFilters: boolean;
|
hasFilters: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
import { RecommendationDto, RecommendationType } from '@jellyfin/sdk/lib/generated-client';
|
|
||||||
import React, { FC } from 'react';
|
|
||||||
|
|
||||||
import globalize from 'scripts/globalize';
|
|
||||||
import escapeHTML from 'escape-html';
|
|
||||||
import SectionContainer from './SectionContainer';
|
|
||||||
|
|
||||||
interface RecommendationContainerProps {
|
|
||||||
recommendation?: RecommendationDto;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RecommendationContainer: FC<RecommendationContainerProps> = ({
|
|
||||||
recommendation = {}
|
|
||||||
}) => {
|
|
||||||
let title = '';
|
|
||||||
|
|
||||||
switch (recommendation.RecommendationType) {
|
|
||||||
case RecommendationType.SimilarToRecentlyPlayed:
|
|
||||||
title = globalize.translate(
|
|
||||||
'RecommendationBecauseYouWatched',
|
|
||||||
recommendation.BaselineItemName
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case RecommendationType.SimilarToLikedItem:
|
|
||||||
title = globalize.translate(
|
|
||||||
'RecommendationBecauseYouLike',
|
|
||||||
recommendation.BaselineItemName
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case RecommendationType.HasDirectorFromRecentlyPlayed:
|
|
||||||
case RecommendationType.HasLikedDirector:
|
|
||||||
title = globalize.translate(
|
|
||||||
'RecommendationDirectedBy',
|
|
||||||
recommendation.BaselineItemName
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case RecommendationType.HasActorFromRecentlyPlayed:
|
|
||||||
case RecommendationType.HasLikedActor:
|
|
||||||
title = globalize.translate(
|
|
||||||
'RecommendationStarring',
|
|
||||||
recommendation.BaselineItemName
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionContainer
|
|
||||||
sectionTitle={escapeHTML(title)}
|
|
||||||
items={recommendation.Items ?? []}
|
|
||||||
cardOptions={{
|
|
||||||
shape: 'overflowPortrait',
|
|
||||||
showYear: true,
|
|
||||||
scalable: true,
|
|
||||||
overlayPlayButton: true,
|
|
||||||
showTitle: true,
|
|
||||||
centerText: true,
|
|
||||||
cardLayout: false
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RecommendationContainer;
|
|
|
@ -1,43 +1,29 @@
|
||||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
import type { BaseItemDto, TimerInfoDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import React, { FC, useEffect, useRef } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
import ItemsContainer from 'elements/emby-itemscontainer/ItemsContainer';
|
||||||
import ItemsContainerElement from 'elements/ItemsContainerElement';
|
|
||||||
import Scroller from 'elements/emby-scroller/Scroller';
|
import Scroller from 'elements/emby-scroller/Scroller';
|
||||||
import LinkButton from 'elements/emby-button/LinkButton';
|
import LinkButton from 'elements/emby-button/LinkButton';
|
||||||
import imageLoader from 'components/images/imageLoader';
|
import Cards from 'components/cardbuilder/Card/Cards';
|
||||||
|
import type { CardOptions } from 'types/cardOptions';
|
||||||
import { CardOptions } from 'types/cardOptions';
|
|
||||||
|
|
||||||
interface SectionContainerProps {
|
interface SectionContainerProps {
|
||||||
url?: string;
|
url?: string;
|
||||||
sectionTitle: string;
|
sectionTitle: string;
|
||||||
items: BaseItemDto[];
|
items: BaseItemDto[] | TimerInfoDto[];
|
||||||
cardOptions: CardOptions;
|
cardOptions: CardOptions;
|
||||||
|
reloadItems?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SectionContainer: FC<SectionContainerProps> = ({
|
const SectionContainer: FC<SectionContainerProps> = ({
|
||||||
sectionTitle,
|
sectionTitle,
|
||||||
url,
|
url,
|
||||||
items,
|
items,
|
||||||
cardOptions
|
cardOptions,
|
||||||
|
reloadItems
|
||||||
}) => {
|
}) => {
|
||||||
const element = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const itemsContainer = element.current?.querySelector('.itemsContainer');
|
|
||||||
cardBuilder.buildCards(items, {
|
|
||||||
itemsContainer: itemsContainer,
|
|
||||||
parentContainer: element.current,
|
|
||||||
|
|
||||||
...cardOptions
|
|
||||||
});
|
|
||||||
|
|
||||||
imageLoader.lazyChildren(itemsContainer);
|
|
||||||
}, [cardOptions, items]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={element} className='verticalSection hide'>
|
<div className='verticalSection'>
|
||||||
<div className='sectionTitleContainer sectionTitleContainer-cards padded-left'>
|
<div className='sectionTitleContainer sectionTitleContainer-cards padded-left'>
|
||||||
{url && items.length > 5 ? (
|
{url && items.length > 5 ? (
|
||||||
<LinkButton
|
<LinkButton
|
||||||
|
@ -64,7 +50,13 @@ const SectionContainer: FC<SectionContainerProps> = ({
|
||||||
isMouseWheelEnabled={false}
|
isMouseWheelEnabled={false}
|
||||||
isCenterFocusEnabled={true}
|
isCenterFocusEnabled={true}
|
||||||
>
|
>
|
||||||
<ItemsContainerElement className='itemsContainer scrollSlider focuscontainer-x' />
|
<ItemsContainer
|
||||||
|
className='itemsContainer scrollSlider focuscontainer-x'
|
||||||
|
reloadItems={reloadItems}
|
||||||
|
queryKey={cardOptions.queryKey}
|
||||||
|
>
|
||||||
|
<Cards items={items} cardOptions={cardOptions} />
|
||||||
|
</ItemsContainer>
|
||||||
</Scroller>
|
</Scroller>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
import type { BaseItemDto, SeriesTimerInfoDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import { IconButton } from '@mui/material';
|
import { IconButton } from '@mui/material';
|
||||||
|
@ -11,8 +11,8 @@ import { LibraryViewSettings } from 'types/library';
|
||||||
import { LibraryTab } from 'types/libraryTab';
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
|
||||||
interface ShuffleButtonProps {
|
interface ShuffleButtonProps {
|
||||||
item: BaseItemDto | undefined;
|
item: BaseItemDto | null | undefined;
|
||||||
items: BaseItemDto[];
|
items: BaseItemDto[] | SeriesTimerInfoDto[];
|
||||||
viewType: LibraryTab
|
viewType: LibraryTab
|
||||||
hasFilters: boolean;
|
hasFilters: boolean;
|
||||||
libraryViewSettings: LibraryViewSettings
|
libraryViewSettings: LibraryViewSettings
|
||||||
|
|
|
@ -16,7 +16,14 @@ import { LibraryTab } from 'types/libraryTab';
|
||||||
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||||
import { SortOrder } from '@jellyfin/sdk/lib/generated-client';
|
import { SortOrder } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
|
||||||
const sortMenuOptions = [
|
type SortOption = {
|
||||||
|
label: string;
|
||||||
|
value: ItemSortBy;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SortOptionsMapping = Record<string, SortOption[]>;
|
||||||
|
|
||||||
|
const movieOrFavoriteOptions = [
|
||||||
{ label: 'Name', value: ItemSortBy.SortName },
|
{ label: 'Name', value: ItemSortBy.SortName },
|
||||||
{ label: 'OptionRandom', value: ItemSortBy.Random },
|
{ label: 'OptionRandom', value: ItemSortBy.Random },
|
||||||
{ label: 'OptionImdbRating', value: ItemSortBy.CommunityRating },
|
{ label: 'OptionImdbRating', value: ItemSortBy.CommunityRating },
|
||||||
|
@ -29,6 +36,66 @@ const sortMenuOptions = [
|
||||||
{ label: 'Runtime', value: ItemSortBy.Runtime }
|
{ label: 'Runtime', value: ItemSortBy.Runtime }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const sortOptionsMapping: SortOptionsMapping = {
|
||||||
|
[LibraryTab.Movies]: movieOrFavoriteOptions,
|
||||||
|
[LibraryTab.Trailers]: [
|
||||||
|
{ label: 'Name', value: ItemSortBy.SortName },
|
||||||
|
{ label: 'OptionImdbRating', value: ItemSortBy.CommunityRating },
|
||||||
|
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated },
|
||||||
|
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed },
|
||||||
|
{ label: 'OptionParentalRating', value: ItemSortBy.OfficialRating },
|
||||||
|
{ label: 'OptionPlayCount', value: ItemSortBy.PlayCount },
|
||||||
|
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate }
|
||||||
|
],
|
||||||
|
[LibraryTab.Favorites]: movieOrFavoriteOptions,
|
||||||
|
[LibraryTab.Series]: [
|
||||||
|
{ label: 'Name', value: ItemSortBy.SortName },
|
||||||
|
{ label: 'OptionRandom', value: ItemSortBy.Random },
|
||||||
|
{ label: 'OptionImdbRating', value: ItemSortBy.CommunityRating },
|
||||||
|
{ label: 'OptionDateShowAdded', value: ItemSortBy.DateCreated },
|
||||||
|
{ label: 'OptionDateEpisodeAdded', value: ItemSortBy.DateLastContentAdded },
|
||||||
|
{ label: 'OptionDatePlayed', value: ItemSortBy.SeriesDatePlayed },
|
||||||
|
{ label: 'OptionParentalRating', value: ItemSortBy.OfficialRating },
|
||||||
|
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate }
|
||||||
|
],
|
||||||
|
[LibraryTab.Episodes]: [
|
||||||
|
{ label: 'Name', value: ItemSortBy.SeriesSortName },
|
||||||
|
{ label: 'OptionImdbRating', value: ItemSortBy.CommunityRating },
|
||||||
|
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated },
|
||||||
|
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate },
|
||||||
|
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed },
|
||||||
|
{ label: 'OptionParentalRating', value: ItemSortBy.OfficialRating },
|
||||||
|
{ label: 'OptionPlayCount', value: ItemSortBy.PlayCount },
|
||||||
|
{ label: 'Runtime', value: ItemSortBy.Runtime },
|
||||||
|
{ label: 'OptionRandom', value: ItemSortBy.Random }
|
||||||
|
],
|
||||||
|
[LibraryTab.Albums]: [
|
||||||
|
{ label: 'Name', value: ItemSortBy.SortName },
|
||||||
|
{ label: 'OptionRandom', value: ItemSortBy.Random },
|
||||||
|
{ label: 'AlbumArtist', value: ItemSortBy.AlbumArtist },
|
||||||
|
{ label: 'OptionImdbRating', value: ItemSortBy.CommunityRating },
|
||||||
|
{ label: 'OptionCriticRating', value: ItemSortBy.CriticRating },
|
||||||
|
{ label: 'OptionReleaseDate', value: ItemSortBy.ProductionYear },
|
||||||
|
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated }
|
||||||
|
],
|
||||||
|
[LibraryTab.Songs]: [
|
||||||
|
{ label: 'Name', value: ItemSortBy.SortName },
|
||||||
|
{ label: 'Album', value: ItemSortBy.Album },
|
||||||
|
{ label: 'AlbumArtist', value: ItemSortBy.AlbumArtist },
|
||||||
|
{ label: 'Artist', value: ItemSortBy.Artist },
|
||||||
|
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated },
|
||||||
|
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed },
|
||||||
|
{ label: 'OptionPlayCount', value: ItemSortBy.PlayCount },
|
||||||
|
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate },
|
||||||
|
{ label: 'Runtime', value: ItemSortBy.Runtime },
|
||||||
|
{ label: 'OptionRandom', value: ItemSortBy.Random }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSortMenuOptions = (viewType: LibraryTab): SortOption[] => {
|
||||||
|
return sortOptionsMapping[viewType] || [];
|
||||||
|
};
|
||||||
|
|
||||||
const sortOrderMenuOptions = [
|
const sortOrderMenuOptions = [
|
||||||
{ label: 'Ascending', value: SortOrder.Ascending },
|
{ label: 'Ascending', value: SortOrder.Ascending },
|
||||||
{ label: 'Descending', value: SortOrder.Descending }
|
{ label: 'Descending', value: SortOrder.Descending }
|
||||||
|
@ -72,25 +139,7 @@ const SortButton: FC<SortButtonProps> = ({
|
||||||
[setLibraryViewSettings]
|
[setLibraryViewSettings]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getVisibleSortMenu = () => {
|
const sortMenuOptions = getSortMenuOptions(viewType);
|
||||||
const visibleSortMenu: ItemSortBy[] = [ItemSortBy.SortName, ItemSortBy.Random, ItemSortBy.DateCreated];
|
|
||||||
|
|
||||||
if (
|
|
||||||
viewType !== LibraryTab.Photos
|
|
||||||
&& viewType !== LibraryTab.Videos
|
|
||||||
&& viewType !== LibraryTab.Books
|
|
||||||
) {
|
|
||||||
visibleSortMenu.push(ItemSortBy.CommunityRating);
|
|
||||||
visibleSortMenu.push(ItemSortBy.CriticRating);
|
|
||||||
visibleSortMenu.push(ItemSortBy.DatePlayed);
|
|
||||||
visibleSortMenu.push(ItemSortBy.OfficialRating);
|
|
||||||
visibleSortMenu.push(ItemSortBy.PlayCount);
|
|
||||||
visibleSortMenu.push(ItemSortBy.PremiereDate);
|
|
||||||
visibleSortMenu.push(ItemSortBy.Runtime);
|
|
||||||
}
|
|
||||||
|
|
||||||
return visibleSortMenu;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
@ -120,7 +169,6 @@ const SortButton: FC<SortButtonProps> = ({
|
||||||
'& .MuiFormControl-root': { m: 1, width: 200 }
|
'& .MuiFormControl-root': { m: 1, width: 200 }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<InputLabel id='select-sort-label'>
|
<InputLabel id='select-sort-label'>
|
||||||
<Typography component='span'>
|
<Typography component='span'>
|
||||||
|
@ -136,7 +184,6 @@ const SortButton: FC<SortButtonProps> = ({
|
||||||
onChange={onSelectChange}
|
onChange={onSelectChange}
|
||||||
>
|
>
|
||||||
{sortMenuOptions
|
{sortMenuOptions
|
||||||
.filter((option) => getVisibleSortMenu().includes(option.value))
|
|
||||||
.map((option) => (
|
.map((option) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
|
@ -166,10 +213,7 @@ const SortButton: FC<SortButtonProps> = ({
|
||||||
onChange={onSelectChange}
|
onChange={onSelectChange}
|
||||||
>
|
>
|
||||||
{sortOrderMenuOptions.map((option) => (
|
{sortOrderMenuOptions.map((option) => (
|
||||||
<MenuItem
|
<MenuItem key={option.value} value={option.value}>
|
||||||
key={option.value}
|
|
||||||
value={option.value}
|
|
||||||
>
|
|
||||||
<Typography component='span'>
|
<Typography component='span'>
|
||||||
{option.label}
|
{option.label}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
|
@ -1,207 +0,0 @@
|
||||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
|
||||||
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
|
||||||
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
|
|
||||||
import React, { FC } from 'react';
|
|
||||||
import * as userSettings from 'scripts/settings/userSettings';
|
|
||||||
import SuggestionsSectionContainer from './SuggestionsSectionContainer';
|
|
||||||
import { Sections, SectionsView, SectionsViewType } from 'types/suggestionsSections';
|
|
||||||
import { ParentId } from 'types/library';
|
|
||||||
|
|
||||||
const getSuggestionsSections = (): Sections[] => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'HeaderContinueWatching',
|
|
||||||
viewType: SectionsViewType.ResumeItems,
|
|
||||||
type: 'Movie',
|
|
||||||
view: SectionsView.ContinueWatchingMovies,
|
|
||||||
parametersOptions: {
|
|
||||||
includeItemTypes: [BaseItemKind.Movie]
|
|
||||||
},
|
|
||||||
cardOptions: {
|
|
||||||
scalable: true,
|
|
||||||
overlayPlayButton: true,
|
|
||||||
showTitle: true,
|
|
||||||
centerText: true,
|
|
||||||
cardLayout: false,
|
|
||||||
preferThumb: true,
|
|
||||||
shape: 'overflowBackdrop',
|
|
||||||
showYear: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'HeaderLatestMovies',
|
|
||||||
viewType: SectionsViewType.LatestMedia,
|
|
||||||
type: 'Movie',
|
|
||||||
view: SectionsView.LatestMovies,
|
|
||||||
parametersOptions: {
|
|
||||||
includeItemTypes: [BaseItemKind.Movie]
|
|
||||||
},
|
|
||||||
cardOptions: {
|
|
||||||
scalable: true,
|
|
||||||
overlayPlayButton: true,
|
|
||||||
showTitle: true,
|
|
||||||
centerText: true,
|
|
||||||
cardLayout: false,
|
|
||||||
shape: 'overflowPortrait',
|
|
||||||
showYear: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'HeaderContinueWatching',
|
|
||||||
viewType: SectionsViewType.ResumeItems,
|
|
||||||
type: 'Episode',
|
|
||||||
view: SectionsView.ContinueWatchingEpisode,
|
|
||||||
parametersOptions: {
|
|
||||||
includeItemTypes: [BaseItemKind.Episode]
|
|
||||||
},
|
|
||||||
cardOptions: {
|
|
||||||
scalable: true,
|
|
||||||
overlayPlayButton: true,
|
|
||||||
showTitle: true,
|
|
||||||
centerText: true,
|
|
||||||
cardLayout: false,
|
|
||||||
shape: 'overflowBackdrop',
|
|
||||||
preferThumb: true,
|
|
||||||
inheritThumb:
|
|
||||||
!userSettings.useEpisodeImagesInNextUpAndResume(undefined),
|
|
||||||
showYear: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'HeaderLatestEpisodes',
|
|
||||||
viewType: SectionsViewType.LatestMedia,
|
|
||||||
type: 'Episode',
|
|
||||||
view: SectionsView.LatestEpisode,
|
|
||||||
parametersOptions: {
|
|
||||||
includeItemTypes: [BaseItemKind.Episode]
|
|
||||||
},
|
|
||||||
cardOptions: {
|
|
||||||
scalable: true,
|
|
||||||
overlayPlayButton: true,
|
|
||||||
showTitle: true,
|
|
||||||
centerText: true,
|
|
||||||
cardLayout: false,
|
|
||||||
shape: 'overflowBackdrop',
|
|
||||||
preferThumb: true,
|
|
||||||
showSeriesYear: true,
|
|
||||||
showParentTitle: true,
|
|
||||||
overlayText: false,
|
|
||||||
showUnplayedIndicator: false,
|
|
||||||
showChildCountIndicator: true,
|
|
||||||
lazy: true,
|
|
||||||
lines: 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'NextUp',
|
|
||||||
viewType: SectionsViewType.NextUp,
|
|
||||||
type: 'nextup',
|
|
||||||
view: SectionsView.NextUp,
|
|
||||||
cardOptions: {
|
|
||||||
scalable: true,
|
|
||||||
overlayPlayButton: true,
|
|
||||||
showTitle: true,
|
|
||||||
centerText: true,
|
|
||||||
cardLayout: false,
|
|
||||||
shape: 'overflowBackdrop',
|
|
||||||
preferThumb: true,
|
|
||||||
inheritThumb:
|
|
||||||
!userSettings.useEpisodeImagesInNextUpAndResume(undefined),
|
|
||||||
showParentTitle: true,
|
|
||||||
overlayText: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'HeaderLatestMusic',
|
|
||||||
viewType: SectionsViewType.LatestMedia,
|
|
||||||
type: 'Audio',
|
|
||||||
view: SectionsView.LatestMusic,
|
|
||||||
parametersOptions: {
|
|
||||||
includeItemTypes: [BaseItemKind.Audio]
|
|
||||||
},
|
|
||||||
cardOptions: {
|
|
||||||
showUnplayedIndicator: false,
|
|
||||||
shape: 'overflowSquare',
|
|
||||||
showTitle: true,
|
|
||||||
showParentTitle: true,
|
|
||||||
lazy: true,
|
|
||||||
centerText: true,
|
|
||||||
overlayPlayButton: true,
|
|
||||||
cardLayout: false,
|
|
||||||
coverImage: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'HeaderRecentlyPlayed',
|
|
||||||
type: 'Audio',
|
|
||||||
view: SectionsView.RecentlyPlayedMusic,
|
|
||||||
parametersOptions: {
|
|
||||||
sortBy: [ItemSortBy.DatePlayed],
|
|
||||||
sortOrder: [SortOrder.Descending],
|
|
||||||
includeItemTypes: [BaseItemKind.Audio]
|
|
||||||
},
|
|
||||||
cardOptions: {
|
|
||||||
showUnplayedIndicator: false,
|
|
||||||
shape: 'overflowSquare',
|
|
||||||
showTitle: true,
|
|
||||||
showParentTitle: true,
|
|
||||||
action: 'instantmix',
|
|
||||||
lazy: true,
|
|
||||||
centerText: true,
|
|
||||||
overlayMoreButton: true,
|
|
||||||
cardLayout: false,
|
|
||||||
coverImage: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'HeaderFrequentlyPlayed',
|
|
||||||
type: 'Audio',
|
|
||||||
view: SectionsView.FrequentlyPlayedMusic,
|
|
||||||
parametersOptions: {
|
|
||||||
sortBy: [ItemSortBy.PlayCount],
|
|
||||||
sortOrder: [SortOrder.Descending],
|
|
||||||
includeItemTypes: [BaseItemKind.Audio]
|
|
||||||
},
|
|
||||||
cardOptions: {
|
|
||||||
showUnplayedIndicator: false,
|
|
||||||
shape: 'overflowSquare',
|
|
||||||
showTitle: true,
|
|
||||||
showParentTitle: true,
|
|
||||||
action: 'instantmix',
|
|
||||||
lazy: true,
|
|
||||||
centerText: true,
|
|
||||||
overlayMoreButton: true,
|
|
||||||
cardLayout: false,
|
|
||||||
coverImage: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
interface SuggestionsItemsContainerProps {
|
|
||||||
parentId: ParentId;
|
|
||||||
sectionsView: SectionsView[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const SuggestionsItemsContainer: FC<SuggestionsItemsContainerProps> = ({
|
|
||||||
parentId,
|
|
||||||
sectionsView
|
|
||||||
}) => {
|
|
||||||
const suggestionsSections = getSuggestionsSections();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{suggestionsSections
|
|
||||||
.filter((section) => sectionsView.includes(section.view))
|
|
||||||
.map((section) => (
|
|
||||||
<SuggestionsSectionContainer
|
|
||||||
key={section.view}
|
|
||||||
parentId={parentId}
|
|
||||||
section={section}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SuggestionsItemsContainer;
|
|
|
@ -1,50 +0,0 @@
|
||||||
import React, { FC } from 'react';
|
|
||||||
import { useGetItemsBySectionType } from 'hooks/useFetchItems';
|
|
||||||
import globalize from 'scripts/globalize';
|
|
||||||
|
|
||||||
import Loading from 'components/loading/LoadingComponent';
|
|
||||||
import { appRouter } from 'components/router/appRouter';
|
|
||||||
import SectionContainer from './SectionContainer';
|
|
||||||
|
|
||||||
import { Sections } from 'types/suggestionsSections';
|
|
||||||
import { ParentId } from 'types/library';
|
|
||||||
|
|
||||||
interface SuggestionsSectionContainerProps {
|
|
||||||
parentId: ParentId;
|
|
||||||
section: Sections;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SuggestionsSectionContainer: FC<SuggestionsSectionContainerProps> = ({
|
|
||||||
parentId,
|
|
||||||
section
|
|
||||||
}) => {
|
|
||||||
const getRouteUrl = () => {
|
|
||||||
return appRouter.getRouteUrl('list', {
|
|
||||||
serverId: window.ApiClient.serverId(),
|
|
||||||
itemTypes: section.type,
|
|
||||||
parentId: parentId
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const { isLoading, data: items } = useGetItemsBySectionType(
|
|
||||||
section,
|
|
||||||
parentId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Loading />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionContainer
|
|
||||||
sectionTitle={globalize.translate(section.name)}
|
|
||||||
items={items ?? []}
|
|
||||||
url={getRouteUrl()}
|
|
||||||
cardOptions={{
|
|
||||||
...section.cardOptions
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SuggestionsSectionContainer;
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
import {
|
||||||
|
type RecommendationDto,
|
||||||
|
RecommendationType
|
||||||
|
} from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import React, { type FC } from 'react';
|
||||||
|
import {
|
||||||
|
useGetMovieRecommendations,
|
||||||
|
useGetSuggestionSectionsWithItems
|
||||||
|
} from 'hooks/useFetchItems';
|
||||||
|
import { appRouter } from 'components/router/appRouter';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
|
import SectionContainer from './SectionContainer';
|
||||||
|
import { CardShape } from 'utils/card';
|
||||||
|
import type { ParentId } from 'types/library';
|
||||||
|
import type { Section, SectionType } from 'types/sections';
|
||||||
|
|
||||||
|
interface SuggestionsSectionViewProps {
|
||||||
|
parentId: ParentId;
|
||||||
|
sectionType: SectionType[];
|
||||||
|
isMovieRecommendationEnabled: boolean | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SuggestionsSectionView: FC<SuggestionsSectionViewProps> = ({
|
||||||
|
parentId,
|
||||||
|
sectionType,
|
||||||
|
isMovieRecommendationEnabled = false
|
||||||
|
}) => {
|
||||||
|
const { isLoading, data: sectionsWithItems } =
|
||||||
|
useGetSuggestionSectionsWithItems(parentId, sectionType);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLoading: isRecommendationsLoading,
|
||||||
|
data: movieRecommendationsItems
|
||||||
|
} = useGetMovieRecommendations(isMovieRecommendationEnabled, parentId);
|
||||||
|
|
||||||
|
if (isLoading || isRecommendationsLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sectionsWithItems?.length && !movieRecommendationsItems?.length) {
|
||||||
|
return (
|
||||||
|
<div className='noItemsMessage centerMessage'>
|
||||||
|
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||||
|
<p>{globalize.translate('MessageNoItemsAvailable')}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRouteUrl = (section: Section) => {
|
||||||
|
return appRouter.getRouteUrl('list', {
|
||||||
|
serverId: window.ApiClient.serverId(),
|
||||||
|
itemTypes: section.itemTypes,
|
||||||
|
parentId: parentId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRecommendationTittle = (recommendation: RecommendationDto) => {
|
||||||
|
let title = '';
|
||||||
|
|
||||||
|
switch (recommendation.RecommendationType) {
|
||||||
|
case RecommendationType.SimilarToRecentlyPlayed:
|
||||||
|
title = globalize.translate(
|
||||||
|
'RecommendationBecauseYouWatched',
|
||||||
|
recommendation.BaselineItemName
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RecommendationType.SimilarToLikedItem:
|
||||||
|
title = globalize.translate(
|
||||||
|
'RecommendationBecauseYouLike',
|
||||||
|
recommendation.BaselineItemName
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RecommendationType.HasDirectorFromRecentlyPlayed:
|
||||||
|
case RecommendationType.HasLikedDirector:
|
||||||
|
title = globalize.translate(
|
||||||
|
'RecommendationDirectedBy',
|
||||||
|
recommendation.BaselineItemName
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RecommendationType.HasActorFromRecentlyPlayed:
|
||||||
|
case RecommendationType.HasLikedActor:
|
||||||
|
title = globalize.translate(
|
||||||
|
'RecommendationStarring',
|
||||||
|
recommendation.BaselineItemName
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return title;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{sectionsWithItems?.map(({ section, items }) => (
|
||||||
|
<SectionContainer
|
||||||
|
key={section.type}
|
||||||
|
sectionTitle={globalize.translate(section.name)}
|
||||||
|
items={items ?? []}
|
||||||
|
url={getRouteUrl(section)}
|
||||||
|
cardOptions={{
|
||||||
|
...section.cardOptions,
|
||||||
|
queryKey: ['SuggestionSectionWithItems'],
|
||||||
|
showTitle: true,
|
||||||
|
centerText: true,
|
||||||
|
cardLayout: false,
|
||||||
|
overlayText: false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{movieRecommendationsItems?.map((recommendation, index) => (
|
||||||
|
<SectionContainer
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
key={`${recommendation.CategoryId}-${index}`} // use a unique id return value may have duplicate id
|
||||||
|
sectionTitle={getRecommendationTittle(recommendation)}
|
||||||
|
items={recommendation.Items ?? []}
|
||||||
|
cardOptions={{
|
||||||
|
queryKey: ['MovieRecommendations'],
|
||||||
|
shape: CardShape.PortraitOverflow,
|
||||||
|
showYear: true,
|
||||||
|
scalable: true,
|
||||||
|
overlayPlayButton: true,
|
||||||
|
showTitle: true,
|
||||||
|
centerText: true,
|
||||||
|
cardLayout: false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SuggestionsSectionView;
|
49
src/apps/experimental/components/library/UpcomingView.tsx
Normal file
49
src/apps/experimental/components/library/UpcomingView.tsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import React, { type FC } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { useGetGroupsUpcomingEpisodes } from 'hooks/useFetchItems';
|
||||||
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import SectionContainer from './SectionContainer';
|
||||||
|
import { CardShape } from 'utils/card';
|
||||||
|
import type { LibraryViewProps } from 'types/library';
|
||||||
|
|
||||||
|
const UpcomingView: FC<LibraryViewProps> = ({ parentId }) => {
|
||||||
|
const { isLoading, data: groupsUpcomingEpisodes } = useGetGroupsUpcomingEpisodes(parentId);
|
||||||
|
|
||||||
|
if (isLoading) return <Loading />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{!groupsUpcomingEpisodes?.length ? (
|
||||||
|
<div className='noItemsMessage centerMessage'>
|
||||||
|
<h1>{globalize.translate('MessageNothingHere')}</h1>
|
||||||
|
<p>
|
||||||
|
{globalize.translate(
|
||||||
|
'MessagePleaseEnsureInternetMetadata'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
groupsUpcomingEpisodes?.map((group) => (
|
||||||
|
<SectionContainer
|
||||||
|
key={group.name}
|
||||||
|
sectionTitle={group.name}
|
||||||
|
items={group.items ?? []}
|
||||||
|
cardOptions={{
|
||||||
|
shape: CardShape.BackdropOverflow,
|
||||||
|
showLocationTypeIndicator: false,
|
||||||
|
showParentTitle: true,
|
||||||
|
preferThumb: true,
|
||||||
|
lazy: true,
|
||||||
|
showDetailsMenu: true,
|
||||||
|
missingIndicator: false,
|
||||||
|
cardLayout: false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpcomingView;
|
|
@ -16,9 +16,16 @@ import Popover from '@mui/material/Popover';
|
||||||
import ViewComfyIcon from '@mui/icons-material/ViewComfy';
|
import ViewComfyIcon from '@mui/icons-material/ViewComfy';
|
||||||
|
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
import { LibraryViewSettings, ViewMode } from 'types/library';
|
import { LibraryViewSettings } from 'types/library';
|
||||||
import { LibraryTab } from 'types/libraryTab';
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
|
||||||
|
const excludedViewType = [
|
||||||
|
LibraryTab.Episodes,
|
||||||
|
LibraryTab.Artists,
|
||||||
|
LibraryTab.AlbumArtists,
|
||||||
|
LibraryTab.Albums
|
||||||
|
];
|
||||||
|
|
||||||
const imageTypesOptions = [
|
const imageTypesOptions = [
|
||||||
{ label: 'Primary', value: ImageType.Primary },
|
{ label: 'Primary', value: ImageType.Primary },
|
||||||
{ label: 'Banner', value: ImageType.Banner },
|
{ label: 'Banner', value: ImageType.Banner },
|
||||||
|
@ -30,7 +37,9 @@ const imageTypesOptions = [
|
||||||
interface ViewSettingsButtonProps {
|
interface ViewSettingsButtonProps {
|
||||||
viewType: LibraryTab;
|
viewType: LibraryTab;
|
||||||
libraryViewSettings: LibraryViewSettings;
|
libraryViewSettings: LibraryViewSettings;
|
||||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
setLibraryViewSettings: React.Dispatch<
|
||||||
|
React.SetStateAction<LibraryViewSettings>
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ViewSettingsButton: FC<ViewSettingsButtonProps> = ({
|
const ViewSettingsButton: FC<ViewSettingsButtonProps> = ({
|
||||||
|
@ -72,27 +81,7 @@ const ViewSettingsButton: FC<ViewSettingsButtonProps> = ({
|
||||||
[setLibraryViewSettings]
|
[setLibraryViewSettings]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getVisibleImageType = () => {
|
const isVisible = !excludedViewType.includes(viewType);
|
||||||
const visibleImageType: ImageType[] = [ImageType.Primary];
|
|
||||||
|
|
||||||
if (
|
|
||||||
viewType !== LibraryTab.Episodes
|
|
||||||
&& viewType !== LibraryTab.Artists
|
|
||||||
&& viewType !== LibraryTab.AlbumArtists
|
|
||||||
&& viewType !== LibraryTab.Albums
|
|
||||||
) {
|
|
||||||
visibleImageType.push(ImageType.Banner);
|
|
||||||
visibleImageType.push(ImageType.Disc);
|
|
||||||
visibleImageType.push(ImageType.Logo);
|
|
||||||
visibleImageType.push(ImageType.Thumb);
|
|
||||||
}
|
|
||||||
|
|
||||||
return visibleImageType;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isViewSettingsEnabled = () => {
|
|
||||||
return libraryViewSettings.ViewMode !== ViewMode.ListView;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
@ -122,20 +111,19 @@ const ViewSettingsButton: FC<ViewSettingsButtonProps> = ({
|
||||||
'& .MuiFormControl-root': { m: 1, width: 220 }
|
'& .MuiFormControl-root': { m: 1, width: 220 }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormControl>
|
{isVisible && (
|
||||||
<InputLabel id='select-sort-label'>
|
<FormControl>
|
||||||
<Typography component='span'>
|
<InputLabel id='select-sort-label'>
|
||||||
{globalize.translate('LabelImageType')}
|
<Typography component='span'>
|
||||||
</Typography>
|
{globalize.translate('LabelImageType')}
|
||||||
</InputLabel>
|
</Typography>
|
||||||
<Select
|
</InputLabel>
|
||||||
value={libraryViewSettings.ImageType}
|
<Select
|
||||||
label={globalize.translate('LabelImageType')}
|
value={libraryViewSettings.ImageType}
|
||||||
onChange={onSelectChange}
|
label={globalize.translate('LabelImageType')}
|
||||||
>
|
onChange={onSelectChange}
|
||||||
{imageTypesOptions
|
>
|
||||||
.filter((imageType) => getVisibleImageType().includes(imageType.value))
|
{imageTypesOptions.map((imageType) => (
|
||||||
.map((imageType) => (
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={imageType.value}
|
key={imageType.value}
|
||||||
value={imageType.value}
|
value={imageType.value}
|
||||||
|
@ -145,51 +133,46 @@ const ViewSettingsButton: FC<ViewSettingsButtonProps> = ({
|
||||||
</Typography>
|
</Typography>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{isViewSettingsEnabled() && (
|
|
||||||
<>
|
|
||||||
<Divider />
|
|
||||||
<FormControl>
|
|
||||||
<FormGroup>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={libraryViewSettings.ShowTitle}
|
|
||||||
onChange={handleChange}
|
|
||||||
name='ShowTitle'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={globalize.translate('ShowTitle')}
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={libraryViewSettings.ShowYear}
|
|
||||||
onChange={handleChange}
|
|
||||||
name='ShowYear'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={globalize.translate('ShowYear')}
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={
|
|
||||||
libraryViewSettings.CardLayout
|
|
||||||
}
|
|
||||||
onChange={handleChange}
|
|
||||||
name='CardLayout'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={globalize.translate(
|
|
||||||
'EnableCardLayout'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</FormControl>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
<Divider />
|
||||||
|
<FormControl>
|
||||||
|
<FormGroup>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={libraryViewSettings.ShowTitle}
|
||||||
|
onChange={handleChange}
|
||||||
|
name='ShowTitle'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={globalize.translate('ShowTitle')}
|
||||||
|
/>
|
||||||
|
{isVisible && (
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={libraryViewSettings.ShowYear}
|
||||||
|
onChange={handleChange}
|
||||||
|
name='ShowYear'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={globalize.translate('ShowYear')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={libraryViewSettings.CardLayout}
|
||||||
|
onChange={handleChange}
|
||||||
|
name='CardLayout'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={globalize.translate('EnableCardLayout')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</FormControl>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
@ -209,107 +209,99 @@ const FilterButton: FC<FilterButtonProps> = ({
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
{isFiltersSeriesStatusEnabled() && (
|
{isFiltersSeriesStatusEnabled() && (
|
||||||
<>
|
<Accordion
|
||||||
<Accordion
|
expanded={expanded === 'filtersSeriesStatus'}
|
||||||
expanded={expanded === 'filtersSeriesStatus'}
|
onChange={handleChange('filtersSeriesStatus')}
|
||||||
onChange={handleChange('filtersSeriesStatus')}
|
>
|
||||||
|
<AccordionSummary
|
||||||
|
aria-controls='filtersSeriesStatus-content'
|
||||||
|
id='filtersSeriesStatus-header'
|
||||||
>
|
>
|
||||||
<AccordionSummary
|
<Typography>
|
||||||
aria-controls='filtersSeriesStatus-content'
|
{globalize.translate('HeaderSeriesStatus')}
|
||||||
id='filtersSeriesStatus-header'
|
</Typography>
|
||||||
>
|
</AccordionSummary>
|
||||||
<Typography>
|
<AccordionDetails>
|
||||||
{globalize.translate('HeaderSeriesStatus')}
|
<FiltersSeriesStatus
|
||||||
</Typography>
|
libraryViewSettings={libraryViewSettings}
|
||||||
</AccordionSummary>
|
setLibraryViewSettings={
|
||||||
<AccordionDetails>
|
setLibraryViewSettings
|
||||||
<FiltersSeriesStatus
|
}
|
||||||
libraryViewSettings={libraryViewSettings}
|
/>
|
||||||
setLibraryViewSettings={
|
</AccordionDetails>
|
||||||
setLibraryViewSettings
|
</Accordion>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{isFiltersEpisodesStatusEnabled() && (
|
{isFiltersEpisodesStatusEnabled() && (
|
||||||
<>
|
<Accordion
|
||||||
<Accordion
|
expanded={expanded === 'filtersEpisodesStatus'}
|
||||||
expanded={expanded === 'filtersEpisodesStatus'}
|
onChange={handleChange('filtersEpisodesStatus')}
|
||||||
onChange={handleChange('filtersEpisodesStatus')}
|
>
|
||||||
|
<AccordionSummary
|
||||||
|
aria-controls='filtersEpisodesStatus-content'
|
||||||
|
id='filtersEpisodesStatus-header'
|
||||||
>
|
>
|
||||||
<AccordionSummary
|
<Typography>
|
||||||
aria-controls='filtersEpisodesStatus-content'
|
{globalize.translate(
|
||||||
id='filtersEpisodesStatus-header'
|
'HeaderEpisodesStatus'
|
||||||
>
|
)}
|
||||||
<Typography>
|
</Typography>
|
||||||
{globalize.translate(
|
</AccordionSummary>
|
||||||
'HeaderEpisodesStatus'
|
<AccordionDetails>
|
||||||
)}
|
<FiltersEpisodesStatus
|
||||||
</Typography>
|
libraryViewSettings={libraryViewSettings}
|
||||||
</AccordionSummary>
|
setLibraryViewSettings={
|
||||||
<AccordionDetails>
|
setLibraryViewSettings
|
||||||
<FiltersEpisodesStatus
|
}
|
||||||
libraryViewSettings={libraryViewSettings}
|
/>
|
||||||
setLibraryViewSettings={
|
</AccordionDetails>
|
||||||
setLibraryViewSettings
|
</Accordion>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{isFiltersFeaturesEnabled() && (
|
{isFiltersFeaturesEnabled() && (
|
||||||
<>
|
<Accordion
|
||||||
<Accordion
|
expanded={expanded === 'filtersFeatures'}
|
||||||
expanded={expanded === 'filtersFeatures'}
|
onChange={handleChange('filtersFeatures')}
|
||||||
onChange={handleChange('filtersFeatures')}
|
>
|
||||||
|
<AccordionSummary
|
||||||
|
aria-controls='filtersFeatures-content'
|
||||||
|
id='filtersFeatures-header'
|
||||||
>
|
>
|
||||||
<AccordionSummary
|
<Typography>
|
||||||
aria-controls='filtersFeatures-content'
|
{globalize.translate('Features')}
|
||||||
id='filtersFeatures-header'
|
</Typography>
|
||||||
>
|
</AccordionSummary>
|
||||||
<Typography>
|
<AccordionDetails>
|
||||||
{globalize.translate('Features')}
|
<FiltersFeatures
|
||||||
</Typography>
|
libraryViewSettings={libraryViewSettings}
|
||||||
</AccordionSummary>
|
setLibraryViewSettings={
|
||||||
<AccordionDetails>
|
setLibraryViewSettings
|
||||||
<FiltersFeatures
|
}
|
||||||
libraryViewSettings={libraryViewSettings}
|
/>
|
||||||
setLibraryViewSettings={
|
</AccordionDetails>
|
||||||
setLibraryViewSettings
|
</Accordion>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isFiltersVideoTypesEnabled() && (
|
{isFiltersVideoTypesEnabled() && (
|
||||||
<>
|
<Accordion
|
||||||
<Accordion
|
expanded={expanded === 'filtersVideoTypes'}
|
||||||
expanded={expanded === 'filtersVideoTypes'}
|
onChange={handleChange('filtersVideoTypes')}
|
||||||
onChange={handleChange('filtersVideoTypes')}
|
>
|
||||||
|
<AccordionSummary
|
||||||
|
aria-controls='filtersVideoTypes-content'
|
||||||
|
id='filtersVideoTypes-header'
|
||||||
>
|
>
|
||||||
<AccordionSummary
|
<Typography>
|
||||||
aria-controls='filtersVideoTypes-content'
|
{globalize.translate('HeaderVideoType')}
|
||||||
id='filtersVideoTypes-header'
|
</Typography>
|
||||||
>
|
</AccordionSummary>
|
||||||
<Typography>
|
<AccordionDetails>
|
||||||
{globalize.translate('HeaderVideoType')}
|
<FiltersVideoTypes
|
||||||
</Typography>
|
libraryViewSettings={libraryViewSettings}
|
||||||
</AccordionSummary>
|
setLibraryViewSettings={
|
||||||
<AccordionDetails>
|
setLibraryViewSettings
|
||||||
<FiltersVideoTypes
|
}
|
||||||
libraryViewSettings={libraryViewSettings}
|
/>
|
||||||
setLibraryViewSettings={
|
</AccordionDetails>
|
||||||
setLibraryViewSettings
|
</Accordion>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isFiltersLegacyEnabled() && (
|
{isFiltersLegacyEnabled() && (
|
||||||
|
@ -329,7 +321,7 @@ const FilterButton: FC<FilterButtonProps> = ({
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<FiltersGenres
|
<FiltersGenres
|
||||||
filters={data}
|
genresOptions={data.Genres}
|
||||||
libraryViewSettings={
|
libraryViewSettings={
|
||||||
libraryViewSettings
|
libraryViewSettings
|
||||||
}
|
}
|
||||||
|
@ -363,7 +355,7 @@ const FilterButton: FC<FilterButtonProps> = ({
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<FiltersOfficialRatings
|
<FiltersOfficialRatings
|
||||||
filters={data}
|
OfficialRatingsOptions={data.OfficialRatings}
|
||||||
libraryViewSettings={
|
libraryViewSettings={
|
||||||
libraryViewSettings
|
libraryViewSettings
|
||||||
}
|
}
|
||||||
|
@ -390,7 +382,7 @@ const FilterButton: FC<FilterButtonProps> = ({
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<FiltersTags
|
<FiltersTags
|
||||||
filters={data}
|
tagsOptions={data.Tags}
|
||||||
libraryViewSettings={
|
libraryViewSettings={
|
||||||
libraryViewSettings
|
libraryViewSettings
|
||||||
}
|
}
|
||||||
|
@ -417,7 +409,7 @@ const FilterButton: FC<FilterButtonProps> = ({
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<FiltersYears
|
<FiltersYears
|
||||||
filters={data}
|
yearsOptions={data.Years}
|
||||||
libraryViewSettings={
|
libraryViewSettings={
|
||||||
libraryViewSettings
|
libraryViewSettings
|
||||||
}
|
}
|
||||||
|
@ -430,31 +422,29 @@ const FilterButton: FC<FilterButtonProps> = ({
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isFiltersStudiosEnabled() && (
|
{isFiltersStudiosEnabled() && studios && (
|
||||||
<>
|
<Accordion
|
||||||
<Accordion
|
expanded={expanded === 'filtersStudios'}
|
||||||
expanded={expanded === 'filtersStudios'}
|
onChange={handleChange('filtersStudios')}
|
||||||
onChange={handleChange('filtersStudios')}
|
>
|
||||||
|
<AccordionSummary
|
||||||
|
aria-controls='filtersStudios-content'
|
||||||
|
id='filtersStudios-header'
|
||||||
>
|
>
|
||||||
<AccordionSummary
|
<Typography>
|
||||||
aria-controls='filtersStudios-content'
|
{globalize.translate('Studios')}
|
||||||
id='filtersStudios-header'
|
</Typography>
|
||||||
>
|
</AccordionSummary>
|
||||||
<Typography>
|
<AccordionDetails>
|
||||||
{globalize.translate('Studios')}
|
<FiltersStudios
|
||||||
</Typography>
|
studiosOptions={studios}
|
||||||
</AccordionSummary>
|
libraryViewSettings={libraryViewSettings}
|
||||||
<AccordionDetails>
|
setLibraryViewSettings={
|
||||||
<FiltersStudios
|
setLibraryViewSettings
|
||||||
filters={studios}
|
}
|
||||||
libraryViewSettings={libraryViewSettings}
|
/>
|
||||||
setLibraryViewSettings={
|
</AccordionDetails>
|
||||||
setLibraryViewSettings
|
</Accordion>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Popover>
|
</Popover>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import type { QueryFiltersLegacy } from '@jellyfin/sdk/lib/generated-client';
|
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import FormGroup from '@mui/material/FormGroup';
|
import FormGroup from '@mui/material/FormGroup';
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
@ -6,13 +5,13 @@ import Checkbox from '@mui/material/Checkbox';
|
||||||
import { LibraryViewSettings } from 'types/library';
|
import { LibraryViewSettings } from 'types/library';
|
||||||
|
|
||||||
interface FiltersGenresProps {
|
interface FiltersGenresProps {
|
||||||
filters?: QueryFiltersLegacy;
|
genresOptions: string[];
|
||||||
libraryViewSettings: LibraryViewSettings;
|
libraryViewSettings: LibraryViewSettings;
|
||||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FiltersGenres: FC<FiltersGenresProps> = ({
|
const FiltersGenres: FC<FiltersGenresProps> = ({
|
||||||
filters,
|
genresOptions,
|
||||||
libraryViewSettings,
|
libraryViewSettings,
|
||||||
setLibraryViewSettings
|
setLibraryViewSettings
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -40,7 +39,7 @@ const FiltersGenres: FC<FiltersGenresProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
{filters?.Genres?.map((filter) => (
|
{genresOptions.map((filter) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={filter}
|
key={filter}
|
||||||
control={
|
control={
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import type { QueryFiltersLegacy } from '@jellyfin/sdk/lib/generated-client';
|
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import FormGroup from '@mui/material/FormGroup';
|
import FormGroup from '@mui/material/FormGroup';
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
@ -6,13 +5,13 @@ import Checkbox from '@mui/material/Checkbox';
|
||||||
import { LibraryViewSettings } from 'types/library';
|
import { LibraryViewSettings } from 'types/library';
|
||||||
|
|
||||||
interface FiltersOfficialRatingsProps {
|
interface FiltersOfficialRatingsProps {
|
||||||
filters?: QueryFiltersLegacy;
|
OfficialRatingsOptions: string[];
|
||||||
libraryViewSettings: LibraryViewSettings;
|
libraryViewSettings: LibraryViewSettings;
|
||||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FiltersOfficialRatings: FC<FiltersOfficialRatingsProps> = ({
|
const FiltersOfficialRatings: FC<FiltersOfficialRatingsProps> = ({
|
||||||
filters,
|
OfficialRatingsOptions,
|
||||||
libraryViewSettings,
|
libraryViewSettings,
|
||||||
setLibraryViewSettings
|
setLibraryViewSettings
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -40,7 +39,7 @@ const FiltersOfficialRatings: FC<FiltersOfficialRatingsProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
{filters?.OfficialRatings?.map((filter) => (
|
{OfficialRatingsOptions.map((filter) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={filter}
|
key={filter}
|
||||||
control={
|
control={
|
||||||
|
|
|
@ -55,6 +55,7 @@ const FiltersStatus: FC<FiltersStatusProps> = ({
|
||||||
&& viewType !== LibraryTab.Artists
|
&& viewType !== LibraryTab.Artists
|
||||||
&& viewType !== LibraryTab.AlbumArtists
|
&& viewType !== LibraryTab.AlbumArtists
|
||||||
&& viewType !== LibraryTab.Songs
|
&& viewType !== LibraryTab.Songs
|
||||||
|
&& viewType !== LibraryTab.Channels
|
||||||
) {
|
) {
|
||||||
visibleFiltersStatus.push(ItemFilter.IsUnplayed);
|
visibleFiltersStatus.push(ItemFilter.IsUnplayed);
|
||||||
visibleFiltersStatus.push(ItemFilter.IsPlayed);
|
visibleFiltersStatus.push(ItemFilter.IsPlayed);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import FormGroup from '@mui/material/FormGroup';
|
import FormGroup from '@mui/material/FormGroup';
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
@ -6,13 +6,13 @@ import Checkbox from '@mui/material/Checkbox';
|
||||||
import { LibraryViewSettings } from 'types/library';
|
import { LibraryViewSettings } from 'types/library';
|
||||||
|
|
||||||
interface FiltersStudiosProps {
|
interface FiltersStudiosProps {
|
||||||
filters?: BaseItemDtoQueryResult;
|
studiosOptions: BaseItemDto[];
|
||||||
libraryViewSettings: LibraryViewSettings;
|
libraryViewSettings: LibraryViewSettings;
|
||||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FiltersStudios: FC<FiltersStudiosProps> = ({
|
const FiltersStudios: FC<FiltersStudiosProps> = ({
|
||||||
filters,
|
studiosOptions,
|
||||||
libraryViewSettings,
|
libraryViewSettings,
|
||||||
setLibraryViewSettings
|
setLibraryViewSettings
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -40,7 +40,7 @@ const FiltersStudios: FC<FiltersStudiosProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
{filters?.Items?.map((filter) => (
|
{studiosOptions?.map((filter) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={filter.Id}
|
key={filter.Id}
|
||||||
control={
|
control={
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import type { QueryFiltersLegacy } from '@jellyfin/sdk/lib/generated-client';
|
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import FormGroup from '@mui/material/FormGroup';
|
import FormGroup from '@mui/material/FormGroup';
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
@ -6,13 +5,13 @@ import Checkbox from '@mui/material/Checkbox';
|
||||||
import { LibraryViewSettings } from 'types/library';
|
import { LibraryViewSettings } from 'types/library';
|
||||||
|
|
||||||
interface FiltersTagsProps {
|
interface FiltersTagsProps {
|
||||||
filters?: QueryFiltersLegacy;
|
tagsOptions: string[];
|
||||||
libraryViewSettings: LibraryViewSettings;
|
libraryViewSettings: LibraryViewSettings;
|
||||||
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FiltersTags: FC<FiltersTagsProps> = ({
|
const FiltersTags: FC<FiltersTagsProps> = ({
|
||||||
filters,
|
tagsOptions,
|
||||||
libraryViewSettings,
|
libraryViewSettings,
|
||||||
setLibraryViewSettings
|
setLibraryViewSettings
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -40,7 +39,7 @@ const FiltersTags: FC<FiltersTagsProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
{filters?.Tags?.map((filter) => (
|
{tagsOptions.map((filter) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={filter}
|
key={filter}
|
||||||
control={
|
control={
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue