Compare commits
21 Commits
renovate/m
...
release-10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc66fadb0e | ||
|
|
298d8d7929 | ||
|
|
bdf8553728 | ||
|
|
8ac155fb7a | ||
|
|
54dee197fd | ||
|
|
7a3eb3ffad | ||
|
|
15b987e0de | ||
|
|
54b05ab27d | ||
|
|
e1308f01b6 | ||
|
|
c596c19bfd | ||
|
|
087d06debf | ||
|
|
0b4402b642 | ||
|
|
b36369b562 | ||
|
|
78a2ba73e7 | ||
|
|
1be5e20363 | ||
|
|
9a130f5869 | ||
|
|
d6cec24d9e | ||
|
|
2044efc0c3 | ||
|
|
e12a8bc39d | ||
|
|
a885964e46 | ||
|
|
efdc07e032 |
64
.ci/azure-pipelines.yml
Normal file
64
.ci/azure-pipelines.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- master
|
||||
- release-*
|
||||
tags:
|
||||
include:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
- job: main_build
|
||||
displayName: 'Main Build'
|
||||
|
||||
dependsOn: lint
|
||||
condition: succeeded()
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: 'Install Node.js'
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
|
||||
- script: |
|
||||
yarn install
|
||||
displayName: 'Install dependencies'
|
||||
|
||||
- script: |
|
||||
test -d dist
|
||||
displayName: 'Check dist directory'
|
||||
|
||||
- script: |
|
||||
yarn pack --filename jellyfin-web.tgz
|
||||
displayName: 'Build package'
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: 'Publish package'
|
||||
condition: succeeded()
|
||||
inputs:
|
||||
targetPath: '$(Build.SourcesDirectory)/jellyfin-web.tgz'
|
||||
artifactName: 'jellyfin-web'
|
||||
|
||||
- job: lint
|
||||
displayName: 'Lint'
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: 'Install Node.js'
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
|
||||
- script: |
|
||||
yarn install
|
||||
displayName: 'Install dependencies'
|
||||
|
||||
- script: |
|
||||
yarn run lint
|
||||
displayName: 'Run ESLint'
|
||||
@@ -1,23 +0,0 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
|
||||
{
|
||||
"name": "Node.js",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-20-bullseye",
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
//https://github.com/microsoft/vscode-dev-containers/issues/559
|
||||
"postCreateCommand": "source $NVM_DIR/nvm.sh && nvm install 20"
|
||||
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
@@ -7,6 +7,3 @@ charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
end_of_line = lf
|
||||
|
||||
[*.{json,yaml,yml}]
|
||||
indent_size = 2
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ecmaVersion": "es5",
|
||||
"files": "./dist/**/*.js",
|
||||
"not": [
|
||||
"./dist/libraries/pdf.worker.js",
|
||||
"./dist/libraries/worker-bundle.js",
|
||||
"./dist/serviceworker.js"
|
||||
]
|
||||
}
|
||||
4
.eslintrc.yml
Normal file
4
.eslintrc.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
env:
|
||||
es6: true
|
||||
browser: true
|
||||
amd: true
|
||||
36
.gitattributes
vendored
36
.gitattributes
vendored
@@ -1,35 +1 @@
|
||||
* text=auto
|
||||
|
||||
CONTRIBUTORS.md merge=union
|
||||
README.md text
|
||||
LICENSE text
|
||||
|
||||
*.css text
|
||||
*.eot binary
|
||||
*.gif binary
|
||||
*.html text diff=html
|
||||
*.ico binary
|
||||
*.*ignore text
|
||||
*.jpg binary
|
||||
*.js text
|
||||
*.json text
|
||||
*.lock text -diff
|
||||
*.map text -diff
|
||||
*.md text
|
||||
*.otf binary
|
||||
*.png binary
|
||||
*.py text diff=python
|
||||
*.svg binary
|
||||
*.ts text
|
||||
*.ttf binary
|
||||
*.sass text
|
||||
*.vue text
|
||||
*.webp binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
|
||||
.editorconfig text
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
|
||||
*.gitattributes linguist-language=gitattributes
|
||||
/CONTRIBUTORS.md merge=union
|
||||
|
||||
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
@@ -1,5 +0,0 @@
|
||||
* @jellyfin/web
|
||||
# Joshua must review all changes to bump_version
|
||||
bump_version @joshuaboniface
|
||||
# Core must approve all changes within the repo config
|
||||
.github/ @jellyfin/core
|
||||
122
.github/ISSUE_TEMPLATE/1-bug-report.yml
vendored
122
.github/ISSUE_TEMPLATE/1-bug-report.yml
vendored
@@ -1,122 +0,0 @@
|
||||
name: Bug Report
|
||||
description: You have noticed a general issue or regression, and would like to report it
|
||||
labels:
|
||||
- bug
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: before-posting
|
||||
attributes:
|
||||
label: "This issue respects the following points:"
|
||||
description: All conditions are **required**.
|
||||
options:
|
||||
- label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin-web/issues?q=is%3Aissue) _(I've searched it)_.
|
||||
required: true
|
||||
- label: I agree to follow Jellyfin's [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct).
|
||||
required: true
|
||||
- label: This report addresses only a single issue; If you encounter multiple issues, kindly create separate reports for each one.
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Bug information
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: |
|
||||
A clear and concise description of the bug.
|
||||
You can also attach screenshots or screen recordings here to help explain your issue.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
description: |
|
||||
Steps to reproduce the behavior:
|
||||
placeholder: |
|
||||
1. Go to …
|
||||
2. Click on …
|
||||
3. Scroll down to …
|
||||
4. See error / the app crashes
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: behaviour
|
||||
attributes:
|
||||
label: Expected/Actual behaviour
|
||||
description: |
|
||||
Describe the behavior you were expecting versus what actually occurred.
|
||||
placeholder: |
|
||||
I expected the app to... However, the actual behavior was that...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: |
|
||||
Please paste any log errors.
|
||||
placeholder: Paste logs…
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Environment
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Server
|
||||
You will find these values in your Admin Dashboard
|
||||
- type: input
|
||||
id: server-version
|
||||
attributes:
|
||||
label: Server version
|
||||
placeholder: 10.10.2
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: web-version
|
||||
attributes:
|
||||
label: Web version
|
||||
placeholder: 10.10.2
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: build-version
|
||||
attributes:
|
||||
label: Build version
|
||||
placeholder: 10.10.2
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Client
|
||||
Information about the device you are seeing the issue on
|
||||
- type: input
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Specify the operating system or device where the issue occurs. If relevant, include details like version or model.
|
||||
placeholder: e.g. Linux, Windows, iPhone, Tizen
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: browser
|
||||
attributes:
|
||||
label: Browser
|
||||
description: Indicate which browser you're using when encountering the issue. If possible, mention the browser version as well.
|
||||
placeholder: e.g. Firefox, Chrome, Safari
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Additional
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Include any relevant details, resources, or screenshots that might help in understanding or implementing the request.
|
||||
placeholder: Add any additional context here.
|
||||
validations:
|
||||
required: false
|
||||
145
.github/ISSUE_TEMPLATE/2-playback-issue.yml
vendored
145
.github/ISSUE_TEMPLATE/2-playback-issue.yml
vendored
@@ -1,145 +0,0 @@
|
||||
name: Playback Issue
|
||||
description: Create a bug report related to media playback
|
||||
labels:
|
||||
- bug
|
||||
- playback
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: before-posting
|
||||
attributes:
|
||||
label: "This issue respects the following points:"
|
||||
description: All conditions are **required**.
|
||||
options:
|
||||
- label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin-web/issues?q=is%3Aissue) _(I've searched it)_.
|
||||
required: true
|
||||
- label: I agree to follow Jellyfin's [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct).
|
||||
required: true
|
||||
- label: This report addresses only a single issue; If you encounter multiple issues, kindly create separate reports for each one.
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Bug information
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: |
|
||||
A clear and concise description of the bug.
|
||||
You can also attach screenshots or screen recordings here to help explain your issue.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
description: |
|
||||
Steps to reproduce the behavior:
|
||||
placeholder: |
|
||||
1. Go to …
|
||||
2. Click on …
|
||||
3. Scroll down to …
|
||||
4. See error / the app crashes
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: behaviour
|
||||
attributes:
|
||||
label: Expected/Actual behaviour
|
||||
description: |
|
||||
Describe the behavior you were expecting versus what actually occurred.
|
||||
placeholder: |
|
||||
I expected the app to... However, the actual behavior was that...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: mediainfo
|
||||
attributes:
|
||||
label: Media info of the file
|
||||
description: |
|
||||
Please share the media information for the file causing issues. You can use a variety of tools to retrieve this information.
|
||||
- Use ffprobe (`ffprobe ./file.mp4`)
|
||||
- Copy the media info from the web interface
|
||||
placeholder: Paste media info…
|
||||
render: shell
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Logs
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: |
|
||||
Please paste your logs here if applicable.
|
||||
placeholder: Paste logs…
|
||||
- type: textarea
|
||||
id: logs-ffmpeg
|
||||
attributes:
|
||||
label: FFmpeg logs
|
||||
description: |
|
||||
Please paste your FFmpeg logs here if available. You can find these in your servers dashboard under "logs".
|
||||
placeholder: Paste logs…
|
||||
render: shell
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Environment
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Server
|
||||
You will find these values in your Admin Dashboard
|
||||
- type: input
|
||||
id: server-version
|
||||
attributes:
|
||||
label: Server version
|
||||
placeholder: 10.10.2
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: web-version
|
||||
attributes:
|
||||
label: Web version
|
||||
placeholder: 10.10.2
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: build-version
|
||||
attributes:
|
||||
label: Build version
|
||||
placeholder: 10.10.2
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Client
|
||||
Information about the device you are seeing the issue on
|
||||
- type: input
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Specify the operating system or device where the issue occurs. If relevant, include details like version or model.
|
||||
placeholder: e.g. Linux, Windows, iPhone, Tizen
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: browser
|
||||
attributes:
|
||||
label: Browser
|
||||
description: Indicate which browser you're using when encountering the issue. If possible, mention the browser version as well.
|
||||
placeholder: e.g. Firefox, Chrome, Safari
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Additional
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Include any relevant details, resources, or screenshots that might help in understanding or implementing the request.
|
||||
placeholder: Add any additional context here.
|
||||
validations:
|
||||
required: false
|
||||
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
<!-- Steps to reproduce the behavior: -->
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Logs**
|
||||
<!-- Please paste any log errors. -->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**System (please complete the following information):**
|
||||
- OS: [e.g. Docker, Debian, Windows]
|
||||
- Browser: [e.g. Firefox, Chrome, Safari]
|
||||
- Jellyfin Version: [e.g. 10.0.1]
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Feature Request
|
||||
url: https://features.jellyfin.org/
|
||||
about: Please head over to our feature request hub to vote on or submit a feature.
|
||||
- name: Help Or Question
|
||||
url: https://matrix.to/#/#jellyfin-troubleshooting:matrix.org
|
||||
about: Please join the troubleshooting Matrix channel to get some help.
|
||||
24
.github/SUPPORT.md
vendored
24
.github/SUPPORT.md
vendored
@@ -1,24 +0,0 @@
|
||||
# Support
|
||||
|
||||
Jellyfin contributors have limited availability to address general support
|
||||
questions. Please make sure you are using the latest version of Jellyfin.
|
||||
|
||||
When looking for support or information, please first search for your
|
||||
question in these venues:
|
||||
|
||||
* [Jellyfin Forum](https://forum.jellyfin.org)
|
||||
* [Jellyfin Documentation](https://jellyfin.org/docs/)
|
||||
* [Open or **closed** issues in the organization](https://github.com/issues?q=sort%3Aupdated-desc+org%3Ajellyfin+is%3Aissue+)
|
||||
|
||||
If you didn't find an answer in the resources above, contributors and other
|
||||
users are reachable through the following channels:
|
||||
|
||||
* #jellyfin on [Matrix](https://matrix.to/#/#jellyfin:matrix.org%22) or [IRC](ircs://irc.libera.chat:6697/#jellyfin)
|
||||
* #jellyfin-troubleshooting on [Matrix](https://matrix.to/#/#jellyfin-troubleshooting:matrix.org) or [IRC](ircs://irc.libera.chat:6697/#jellyfin-troubleshooting)
|
||||
* [/r/jellyfin on Reddit](https://www.reddit.com/r/jellyfin)
|
||||
|
||||
GitHub issues are for tracking enhancements and bugs, not general support.
|
||||
|
||||
The open source license grants you the freedom to use Jellyfin.
|
||||
It does not guarantee commitments of other people's time.
|
||||
Please be respectful and manage your expectations.
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -1,6 +1,6 @@
|
||||
<!--
|
||||
Ensure your title is short, descriptive, and in the imperative mood (Fix X, Change Y, instead of Fixed X, Changed Y).
|
||||
For a good inspiration of what to write in commit messages and PRs please review https://chris.beams.io/posts/git-commit/ and our https://jellyfin.org/docs/general/contributing/issues page.
|
||||
For a good inspiration of what to write in commit messages and PRs please review https://chris.beams.io/posts/git-commit/ and our https://docs.jellyfin.org/general/contributing/issues.html page.
|
||||
-->
|
||||
|
||||
**Changes**
|
||||
|
||||
28
.github/renovate.json
vendored
28
.github/renovate.json
vendored
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"github>jellyfin/.github//renovate-presets/nodejs",
|
||||
":dependencyDashboard"
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": false
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackageNames": [ "@jellyfin/sdk" ],
|
||||
"followTag": "unstable",
|
||||
"minimumReleaseAge": null,
|
||||
"prPriority": 5,
|
||||
"schedule": [ "after 7:00 am" ]
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [ "dompurify" ],
|
||||
"matchUpdateTypes": [ "major" ],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [ "hls.js" ],
|
||||
"prPriority": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
19
.github/stale.yml
vendored
Normal file
19
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 90
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 14
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- regression
|
||||
- future
|
||||
- feature
|
||||
- enhancement
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
Issues go stale after 90d of inactivity. Mark the issue as fresh by adding a comment or commit. Stale issues close after an additional 14d of inactivity.
|
||||
If this issue is safe to close now please do so.
|
||||
If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
15
.github/workflows/__automation.yml
vendored
15
.github/workflows/__automation.yml
vendored
@@ -1,15 +0,0 @@
|
||||
name: Automation 🎛️
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
conflicts:
|
||||
name: Merge conflict labeling 🏷️
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
|
||||
with:
|
||||
dirtyLabel: 'merge conflict'
|
||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
||||
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
||||
40
.github/workflows/__codeql.yml
vendored
40
.github/workflows/__codeql.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: GitHub CodeQL 🔬
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
commit:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze ${{ matrix.language }} 🔬
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language:
|
||||
- javascript-typescript
|
||||
|
||||
steps:
|
||||
- name: Checkout repository ⬇️
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
ref: ${{ inputs.commit }}
|
||||
show-progress: false
|
||||
|
||||
- name: Initialize CodeQL 🛠️
|
||||
uses: github/codeql-action/init@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3.31.4
|
||||
with:
|
||||
queries: security-and-quality
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild 📦
|
||||
uses: github/codeql-action/autobuild@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3.31.4
|
||||
|
||||
- name: Perform CodeQL Analysis 🧪
|
||||
uses: github/codeql-action/analyze@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3.31.4
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
59
.github/workflows/__deploy.yml
vendored
59
.github/workflows/__deploy.yml
vendored
@@ -1,59 +0,0 @@
|
||||
name: Deploy 🏗️
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
branch:
|
||||
required: true
|
||||
type: string
|
||||
commit:
|
||||
required: false
|
||||
type: string
|
||||
comment:
|
||||
required: false
|
||||
type: boolean
|
||||
artifact_name:
|
||||
required: false
|
||||
type: string
|
||||
default: frontend
|
||||
|
||||
jobs:
|
||||
cf-pages:
|
||||
name: CloudFlare Pages 📃
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: ${{ inputs.branch == 'master' && 'Production' || 'Preview' }}
|
||||
url: ${{ steps.cf.outputs.deployment-url }}
|
||||
outputs:
|
||||
url: ${{ steps.cf.outputs.deployment-url }}
|
||||
|
||||
steps:
|
||||
- name: Download workflow artifact ⬇️
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}
|
||||
path: dist
|
||||
|
||||
- name: Publish to Cloudflare Pages 📃
|
||||
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
|
||||
id: cf
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy dist --project-name=jellyfin-web --branch=${{ inputs.branch }}
|
||||
|
||||
compose-comment:
|
||||
name: Compose and push comment 📝
|
||||
# Always run so the comment is composed for the workflow summary
|
||||
if: ${{ always() }}
|
||||
uses: ./.github/workflows/__job_messages.yml
|
||||
secrets: inherit
|
||||
needs:
|
||||
- cf-pages
|
||||
|
||||
with:
|
||||
branch: ${{ inputs.branch }}
|
||||
commit: ${{ inputs.commit }}
|
||||
preview_url: ${{ needs.cf-pages.outputs.url }}
|
||||
in_progress: false
|
||||
comment: ${{ inputs.comment }}
|
||||
65
.github/workflows/__job_messages.yml
vendored
65
.github/workflows/__job_messages.yml
vendored
@@ -1,65 +0,0 @@
|
||||
name: Job messages ⚙️
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
branch:
|
||||
required: false
|
||||
type: string
|
||||
commit:
|
||||
required: true
|
||||
type: string
|
||||
preview_url:
|
||||
required: false
|
||||
type: string
|
||||
in_progress:
|
||||
required: true
|
||||
type: boolean
|
||||
comment:
|
||||
required: false
|
||||
type: boolean
|
||||
marker:
|
||||
description: Hidden marker to detect PR comments composed by the bot
|
||||
required: false
|
||||
type: string
|
||||
default: "CFPages-deployment"
|
||||
|
||||
|
||||
jobs:
|
||||
cf_pages_msg:
|
||||
name: CloudFlare Pages deployment 📃🚀
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Compose message 📃
|
||||
if: ${{ always() }}
|
||||
id: compose
|
||||
env:
|
||||
COMMIT: ${{ inputs.commit }}
|
||||
PREVIEW_URL: ${{ inputs.preview_url != '' && (inputs.branch != 'master' && inputs.preview_url || format('https://jellyfin-web.pages.dev ({0})', inputs.preview_url)) || 'Not available' }}
|
||||
DEPLOY_STATUS: ${{ inputs.in_progress && '🔄 Deploying...' || (inputs.preview_url != '' && '✅ Deployed!' || '❌ Failure. Check workflow logs for details') }}
|
||||
DEPLOYMENT_TYPE: ${{ inputs.branch != 'master' && '🔀 Preview' || '⚙️ Production' }}
|
||||
WORKFLOW_RUN: ${{ !inputs.in_progress && format('**[View build logs](https://github.com/{0}/actions/runs/{1})**', github.repository, github.run_id) || '' }}
|
||||
# EOF is needed for multiline environment variables in a GitHub Actions context
|
||||
run: |
|
||||
echo "## Cloudflare Pages deployment" > $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Latest commit** | <code>${COMMIT::7}</code> |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------------------------- |:----------------------------: |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Status** | $DEPLOY_STATUS |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Preview URL** | $PREVIEW_URL |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Type** | $DEPLOYMENT_TYPE |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "$WORKFLOW_RUN" >> $GITHUB_STEP_SUMMARY
|
||||
COMPOSED_MSG=$(cat $GITHUB_STEP_SUMMARY)
|
||||
echo "msg<<EOF" >> $GITHUB_ENV
|
||||
echo "$COMPOSED_MSG" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Push comment to Pull Request 🔼
|
||||
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1
|
||||
if: ${{ inputs.comment && steps.compose.conclusion == 'success' }}
|
||||
with:
|
||||
github-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
message: ${{ env.msg }}
|
||||
comment-tag: ${{ inputs.marker }}
|
||||
45
.github/workflows/__package.yml
vendored
45
.github/workflows/__package.yml
vendored
@@ -1,45 +0,0 @@
|
||||
name: Packaging 📦
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
commit:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
run-build-prod:
|
||||
name: Run production build 🏗️
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
ref: ${{ inputs.commit || github.sha }}
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
check-latest: true
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run a production build
|
||||
env:
|
||||
JELLYFIN_VERSION: ${{ inputs.commit || github.sha }}
|
||||
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
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: frontend
|
||||
path: dist
|
||||
61
.github/workflows/__quality_checks.yml
vendored
61
.github/workflows/__quality_checks.yml
vendored
@@ -1,61 +0,0 @@
|
||||
name: Quality checks 👌🧪
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
commit:
|
||||
required: true
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
name: Vulnerable dependencies 🔎
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
ref: ${{ inputs.commit }}
|
||||
show-progress: false
|
||||
|
||||
- name: Scan
|
||||
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
|
||||
with:
|
||||
## Workaround from https://github.com/actions/dependency-review-action/issues/456
|
||||
## TODO: Remove when necessary
|
||||
base-ref: ${{ github.event.pull_request.base.sha || 'master' }}
|
||||
head-ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
quality:
|
||||
name: Run ${{ matrix.command }} 🕵️♂️
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
command:
|
||||
- build:es-check
|
||||
- lint
|
||||
- stylelint
|
||||
- build:check
|
||||
- test
|
||||
|
||||
steps:
|
||||
- name: Checkout ⬇️
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
ref: ${{ inputs.commit }}
|
||||
show-progress: false
|
||||
|
||||
- name: Setup node environment ⚙️
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
check-latest: true
|
||||
|
||||
- name: Install dependencies 📦
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run ${{ matrix.command }} ⚙️
|
||||
run: npm run ${{ matrix.command }}
|
||||
100
.github/workflows/pull_request.yml
vendored
100
.github/workflows/pull_request.yml
vendored
@@ -1,100 +0,0 @@
|
||||
name: Pull Request 📥
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- master
|
||||
- release*
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
push-comment:
|
||||
name: Create comments ✍️
|
||||
if: ${{ always() && !cancelled() && github.repository == 'jellyfin/jellyfin-web' }}
|
||||
uses: ./.github/workflows/__job_messages.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha }}
|
||||
in_progress: true
|
||||
comment: true
|
||||
|
||||
build:
|
||||
name: Build 🏗️
|
||||
if: ${{ always() && !cancelled() }}
|
||||
uses: ./.github/workflows/__package.yml
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
automation:
|
||||
name: Automation 🎛️
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
uses: ./.github/workflows/__automation.yml
|
||||
secrets: inherit
|
||||
|
||||
quality_checks:
|
||||
name: Quality checks 👌🧪
|
||||
if: ${{ always() && !cancelled() }}
|
||||
uses: ./.github/workflows/__quality_checks.yml
|
||||
permissions: {}
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
codeql:
|
||||
name: GitHub CodeQL 🔬
|
||||
if: ${{ always() && !cancelled() }}
|
||||
uses: ./.github/workflows/__codeql.yml
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
deploy:
|
||||
name: Deploy 🚀
|
||||
uses: ./.github/workflows/__deploy.yml
|
||||
if: ${{ always() && !cancelled() && needs.build.result == 'success' && github.repository == 'jellyfin/jellyfin-web' }}
|
||||
needs:
|
||||
- push-comment
|
||||
- build
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
secrets: inherit
|
||||
with:
|
||||
# If the PR is from the master branch of a fork, append the fork's name to the branch name
|
||||
branch: ${{ github.event.pull_request.head.repo.full_name != github.repository && github.event.pull_request.head.ref == 'master' && format('{0}/{1}', github.event.pull_request.head.repo.full_name, github.event.pull_request.head.ref) || github.event.pull_request.head.ref }}
|
||||
comment: true
|
||||
commit: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
run-eslint:
|
||||
name: Run eslint suggestions
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
check-latest: true
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run eslint
|
||||
uses: CatChen/eslint-suggestion-action@4ee415529307a8ca0260b4a3775484802523e5af # v4.1.19
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
58
.github/workflows/push.yml
vendored
58
.github/workflows/push.yml
vendored
@@ -1,58 +0,0 @@
|
||||
name: Push & Release 🌍
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- release*
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
|
||||
jobs:
|
||||
automation:
|
||||
name: Automation 🎛️
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
uses: ./.github/workflows/__automation.yml
|
||||
secrets: inherit
|
||||
|
||||
main:
|
||||
name: 'Unstable release 🚀⚠️'
|
||||
uses: ./.github/workflows/__package.yml
|
||||
with:
|
||||
commit: ${{ github.sha }}
|
||||
|
||||
quality_checks:
|
||||
name: Quality checks 👌🧪
|
||||
if: ${{ always() && !cancelled() }}
|
||||
uses: ./.github/workflows/__quality_checks.yml
|
||||
permissions: {}
|
||||
with:
|
||||
commit: ${{ github.sha }}
|
||||
|
||||
codeql:
|
||||
name: GitHub CodeQL 🔬
|
||||
uses: ./.github/workflows/__codeql.yml
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
with:
|
||||
commit: ${{ github.sha }}
|
||||
|
||||
deploy:
|
||||
name: Deploy 🚀
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
uses: ./.github/workflows/__deploy.yml
|
||||
needs:
|
||||
- main
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
secrets: inherit
|
||||
with:
|
||||
branch: ${{ github.ref_name }}
|
||||
comment: false
|
||||
38
.github/workflows/schedule.yml
vendored
38
.github/workflows/schedule.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: Scheduled tasks 🕑
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
issues:
|
||||
name: Check stale issues and PRs
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
operations-per-run: 75
|
||||
# Issues receive a stale warning after 120 days and close after an additional 21 days
|
||||
days-before-stale: 120
|
||||
days-before-close: 21
|
||||
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
|
||||
stale-issue-label: stale
|
||||
stale-issue-message: |-
|
||||
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
|
||||
|
||||
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
|
||||
|
||||
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://jellyfin.org/contact).
|
||||
# PRs are closed after having unresolved merge conflicts for 90 days
|
||||
days-before-pr-stale: 0
|
||||
days-before-pr-close: 90
|
||||
only-pr-labels: merge conflict
|
||||
stale-pr-label: stale
|
||||
close-pr-message: |-
|
||||
This PR has been closed due to having unresolved merge conflicts.
|
||||
584
.gitignore
vendored
584
.gitignore
vendored
@@ -1,23 +1,575 @@
|
||||
# npm
|
||||
dist
|
||||
web
|
||||
node_modules
|
||||
|
||||
# test coverage
|
||||
# Created by https://www.gitignore.io/api/node,rider,macos,linux,windows,visualstudio,visualstudiocode
|
||||
# Edit at https://www.gitignore.io/?templates=node,rider,macos,linux,windows,visualstudio,visualstudiocode
|
||||
|
||||
### Linux ###
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# config
|
||||
config.json
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# ide
|
||||
.idea
|
||||
.vs
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# vim
|
||||
*.sw?
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# direnv
|
||||
.direnv/
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# environment related
|
||||
.envrc
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
### Rider ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
### VisualStudio ###
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
# ASP.NET Core default setup: bower directory is configured as wwwroot/lib/ and bower restore is true
|
||||
**/wwwroot/lib/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- Backup*.rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# End of https://www.gitignore.io/api/node,rider,macos,linux,windows,visualstudio,visualstudiocode
|
||||
|
||||
# dist for webpack output
|
||||
dist
|
||||
@@ -1,3 +0,0 @@
|
||||
# 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,163 +0,0 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@stylistic/stylelint-plugin",
|
||||
"stylelint-no-browser-hacks/lib"
|
||||
],
|
||||
"rules": {
|
||||
"at-rule-empty-line-before": [ "always", {
|
||||
"except": [
|
||||
"blockless-after-same-name-blockless",
|
||||
"first-nested"
|
||||
],
|
||||
"ignore": ["after-comment"]
|
||||
} ],
|
||||
"@stylistic/at-rule-name-case": "lower",
|
||||
"@stylistic/at-rule-name-space-after": "always-single-line",
|
||||
"at-rule-no-unknown": true,
|
||||
"at-rule-no-vendor-prefix": true,
|
||||
"@stylistic/at-rule-semicolon-newline-after": "always",
|
||||
"@stylistic/block-closing-brace-empty-line-before": "never",
|
||||
"@stylistic/block-closing-brace-newline-after": "always",
|
||||
"@stylistic/block-closing-brace-newline-before": "always-multi-line",
|
||||
"@stylistic/block-closing-brace-space-before": "always-single-line",
|
||||
"block-no-empty": true,
|
||||
"@stylistic/block-opening-brace-newline-after": "always-multi-line",
|
||||
"@stylistic/block-opening-brace-space-after": "always-single-line",
|
||||
"@stylistic/block-opening-brace-space-before": "always",
|
||||
"@stylistic/color-hex-case": "lower",
|
||||
"color-hex-length": "short",
|
||||
"color-no-invalid-hex": true,
|
||||
"comment-empty-line-before": [ "always", {
|
||||
"except": ["first-nested"],
|
||||
"ignore": ["stylelint-commands"]
|
||||
} ],
|
||||
"comment-no-empty": true,
|
||||
"comment-whitespace-inside": "always",
|
||||
"custom-property-empty-line-before": [ "always", {
|
||||
"except": [
|
||||
"after-custom-property",
|
||||
"first-nested"
|
||||
],
|
||||
"ignore": [
|
||||
"after-comment",
|
||||
"inside-single-line-block"
|
||||
]
|
||||
} ],
|
||||
"@stylistic/declaration-bang-space-after": "never",
|
||||
"@stylistic/declaration-bang-space-before": "always",
|
||||
"declaration-block-no-duplicate-properties": [
|
||||
true,
|
||||
{
|
||||
"ignore": ["consecutive-duplicates-with-different-values"]
|
||||
}
|
||||
],
|
||||
"declaration-block-no-shorthand-property-overrides": true,
|
||||
"@stylistic/declaration-block-semicolon-newline-after": "always-multi-line",
|
||||
"@stylistic/declaration-block-semicolon-space-after": "always-single-line",
|
||||
"@stylistic/declaration-block-semicolon-space-before": "never",
|
||||
"declaration-block-single-line-max-declarations": 1,
|
||||
"@stylistic/declaration-block-trailing-semicolon": "always",
|
||||
"@stylistic/declaration-colon-newline-after": "always-multi-line",
|
||||
"@stylistic/declaration-colon-space-after": "always-single-line",
|
||||
"@stylistic/declaration-colon-space-before": "never",
|
||||
"font-family-no-duplicate-names": true,
|
||||
"function-calc-no-unspaced-operator": true,
|
||||
"@stylistic/function-comma-newline-after": "always-multi-line",
|
||||
"@stylistic/function-comma-space-after": "always-single-line",
|
||||
"@stylistic/function-comma-space-before": "never",
|
||||
"function-linear-gradient-no-nonstandard-direction": true,
|
||||
"@stylistic/function-max-empty-lines": 0,
|
||||
"function-name-case": "lower",
|
||||
"@stylistic/function-parentheses-newline-inside": "always-multi-line",
|
||||
"@stylistic/function-parentheses-space-inside": "never-single-line",
|
||||
"@stylistic/function-whitespace-after": "always",
|
||||
"@stylistic/indentation": 4,
|
||||
"keyframe-declaration-no-important": true,
|
||||
"length-zero-no-unit": true,
|
||||
"@stylistic/max-empty-lines": 1,
|
||||
"@stylistic/media-feature-colon-space-after": "always",
|
||||
"@stylistic/media-feature-colon-space-before": "never",
|
||||
"@stylistic/media-feature-name-case": "lower",
|
||||
"media-feature-name-no-unknown": true,
|
||||
"media-feature-name-no-vendor-prefix": true,
|
||||
"@stylistic/media-feature-parentheses-space-inside": "never",
|
||||
"@stylistic/media-feature-range-operator-space-after": "always",
|
||||
"@stylistic/media-feature-range-operator-space-before": "always",
|
||||
"@stylistic/media-query-list-comma-newline-after": "always-multi-line",
|
||||
"@stylistic/media-query-list-comma-space-after": "always-single-line",
|
||||
"@stylistic/media-query-list-comma-space-before": "never",
|
||||
"no-descending-specificity": true,
|
||||
"no-duplicate-at-import-rules": true,
|
||||
"no-duplicate-selectors": true,
|
||||
"no-empty-source": true,
|
||||
"@stylistic/no-eol-whitespace": true,
|
||||
"@stylistic/no-extra-semicolons": true,
|
||||
"no-invalid-double-slash-comments": true,
|
||||
"@stylistic/no-missing-end-of-source-newline": true,
|
||||
"@stylistic/number-leading-zero": "always",
|
||||
"@stylistic/number-no-trailing-zeros": true,
|
||||
"plugin/no-browser-hacks": true,
|
||||
"@stylistic/property-case": "lower",
|
||||
"property-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreProperties": [
|
||||
"user-drag"
|
||||
]
|
||||
}
|
||||
],
|
||||
"property-no-vendor-prefix": true,
|
||||
"rule-empty-line-before": [ "always-multi-line", {
|
||||
"except": ["first-nested"],
|
||||
"ignore": ["after-comment"]
|
||||
} ],
|
||||
"@stylistic/selector-attribute-brackets-space-inside": "never",
|
||||
"@stylistic/selector-attribute-operator-space-after": "never",
|
||||
"@stylistic/selector-attribute-operator-space-before": "never",
|
||||
"@stylistic/selector-combinator-space-after": "always",
|
||||
"@stylistic/selector-combinator-space-before": "always",
|
||||
"@stylistic/selector-descendant-combinator-no-non-space": true,
|
||||
"@stylistic/selector-list-comma-newline-after": "always",
|
||||
"@stylistic/selector-list-comma-space-before": "never",
|
||||
"@stylistic/selector-max-empty-lines": 0,
|
||||
"selector-no-vendor-prefix": true,
|
||||
"@stylistic/selector-pseudo-class-case": "lower",
|
||||
"selector-pseudo-class-no-unknown": true,
|
||||
"@stylistic/selector-pseudo-class-parentheses-space-inside": "never",
|
||||
"@stylistic/selector-pseudo-element-case": "lower",
|
||||
"selector-pseudo-element-colon-notation": "double",
|
||||
"selector-pseudo-element-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignorePseudoElements": [
|
||||
"cue"
|
||||
]
|
||||
}
|
||||
],
|
||||
"selector-type-case": "lower",
|
||||
"selector-type-no-unknown": true,
|
||||
"string-no-newline": true,
|
||||
"@stylistic/unit-case": "lower",
|
||||
"unit-no-unknown": true,
|
||||
"value-no-vendor-prefix": true,
|
||||
"@stylistic/value-list-comma-newline-after": "always-multi-line",
|
||||
"@stylistic/value-list-comma-space-after": "always-single-line",
|
||||
"@stylistic/value-list-comma-space-before": "never",
|
||||
"@stylistic/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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
5
.vscode/extensions.json
vendored
5
.vscode/extensions.json
vendored
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
||||
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"[json][typescript][typescriptreact][javascript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"eslint.format.enable": true,
|
||||
"editor.formatOnSave": false
|
||||
}
|
||||
262
CONTRIBUTORS.md
262
CONTRIBUTORS.md
@@ -1,173 +1,97 @@
|
||||
# Jellyfin Contributors
|
||||
|
||||
- [JoshuaBoniface](https://github.com/joshuaboniface)
|
||||
- [nvllsvm](https://github.com/nvllsvm)
|
||||
- [JustAMan](https://github.com/JustAMan)
|
||||
- [dcrdev](https://github.com/dcrdev)
|
||||
- [EraYaN](https://github.com/EraYaN)
|
||||
- [flemse](https://github.com/flemse)
|
||||
- [bfayers](https://github.com/bfayers)
|
||||
- [Bond_009](https://github.com/Bond-009)
|
||||
- [AnthonyLavado](https://github.com/anthonylavado)
|
||||
- [dkanada](https://github.com/dkanada)
|
||||
- [sparky8251](https://github.com/sparky8251)
|
||||
- [LeoVerto](https://github.com/LeoVerto)
|
||||
- [cvium](https://github.com/cvium)
|
||||
- [grafixeyehero](https://github.com/grafixeyehero)
|
||||
- [Drago96](https://github.com/drago-96)
|
||||
- [ViXXoR](https://github.com/ViXXoR)
|
||||
- [nkmerrill](https://github.com/nkmerrill)
|
||||
- [TtheCreator](https://github.com/Tthecreator)
|
||||
- [RazeLighter777](https://github.com/RazeLighter777)
|
||||
- [LogicalPhallacy](https://github.com/LogicalPhallacy)
|
||||
- [thornbill](https://github.com/thornbill)
|
||||
- [redSpoutnik](https://github.com/redSpoutnik)
|
||||
- [DrPandemic](https://github.com/drpandemic)
|
||||
- [Oddstr13](https://github.com/oddstr13)
|
||||
- [petermcneil](https://github.com/petermcneil)
|
||||
- [lewazo](https://github.com/lewazo)
|
||||
- [Raghu Saxena](https://github.com/ckcr4lyf)
|
||||
- [Nickbert7](https://github.com/Nickbert7)
|
||||
- [ferferga](https://github.com/ferferga)
|
||||
- [bilde2910](https://github.com/bilde2910)
|
||||
- [Daniel Hartung](https://github.com/dhartung)
|
||||
- [Ryan Hartzell](https://github.com/ryan-hartzell)
|
||||
- [Thibault Nocchi](https://github.com/ThibaultNocchi)
|
||||
- [MrTimscampi](https://github.com/MrTimscampi)
|
||||
- [artiume](https://github.com/Artiume)
|
||||
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
|
||||
- [Sarab Singh](https://github.com/sarab97)
|
||||
- [DesertCookie](https://github.com/desertcookie)
|
||||
- [GuilhermeHideki](https://github.com/GuilhermeHideki)
|
||||
- [Andrei Oanca](https://github.com/OancaAndrei)
|
||||
- [Cromefire_](https://github.com/cromefire)
|
||||
- [Orry Verducci](https://github.com/orryverducci)
|
||||
- [Camc314](https://github.com/camc314)
|
||||
- [danieladov](https://github.com/danieladov)
|
||||
- [Stephane Senart](https://github.com/ssenart)
|
||||
- [imchasingshadows](https://github.com/imchasingshadows)
|
||||
- [Ömer Erdinç Yağmurlu](https://github.com/omeryagmurlu)
|
||||
- [Keegan Dahm](https://github.com/keegandahm)
|
||||
- [GodTamIt](https://github.com/GodTamIt)
|
||||
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
||||
- [Matthew Jones](https://github.com/matthew-jones-uk)
|
||||
- [taku0](https://github.com/taku0)
|
||||
- [Viperinius](https://github.com/Viperinius)
|
||||
- [is343](https://github.com/is343)
|
||||
- [Meet Pandya](https://github.com/meet-k-pandya)
|
||||
- [Peter Spenler](https://github.com/peterspenler)
|
||||
- [jomp16](https://github.com/jomp16)
|
||||
- [Leon de Klerk](https://github.com/leondeklerk)
|
||||
- [CrispyBaguette](https://github.com/CrispyBaguette)
|
||||
- [Vankerkom](https://github.com/vankerkom)
|
||||
- [edvwib](https://github.com/edvwib)
|
||||
- [Rob Farraher](https://github.com/farraherbg)
|
||||
- [TelepathicWalrus](https://github.com/TelepathicWalrus)
|
||||
- [Pier-Luc Ducharme](https://github.com/pl-ducharme)
|
||||
- [Anantharaju S](https://github.com/Anantharajus)
|
||||
- [Merlin Sievers](https://github.com/dann-merlin)
|
||||
- [Fishbigger](https://github.com/fishbigger)
|
||||
- [sleepycatcoding](https://github.com/sleepycatcoding)
|
||||
- [TheMelmacian](https://github.com/TheMelmacian)
|
||||
- [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)
|
||||
- [btopherjohnson](https://github.com/btopherjohnson)
|
||||
- [András Maróy](https://github.com/andrasmaroy)
|
||||
- [Chris-Codes-It](https://github.com/Chris-Codes-It)
|
||||
- [Vedant](https://github.com/viktory36)
|
||||
- [GeorgeH005](https://github.com/GeorgeH005)
|
||||
- [JPUC1143](https://github.com/Jpuc1143)
|
||||
- [David Angel](https://github.com/davidangel)
|
||||
- [Pithaya](https://github.com/Pithaya)
|
||||
- [Peter Santos](https://github.com/prsantos-com)
|
||||
- [Chaitanya Shahare](https://github.com/Chaitanya-Shahare)
|
||||
- [Venkat Karasani](https://github.com/venkat-karasani)
|
||||
- [Connor Smith](https://github.com/ConnorS1110)
|
||||
- [iFraan](https://github.com/iFraan)
|
||||
- [Ali](https://github.com/bu3alwa)
|
||||
- [K. Kyle Puchkov](https://github.com/kepper104)
|
||||
- [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode)
|
||||
- [Jxiced](https://github.com/Jxiced)
|
||||
- [Derek Huber](https://github.com/Derek4aty1)
|
||||
- [StableCrimson](https://github.com/StableCrimson)
|
||||
- [diegoeche](https://github.com/diegoeche)
|
||||
- [Free O'Toole](https://github.com/freeotoole)
|
||||
- [TheBosZ](https://github.com/thebosz)
|
||||
- [qm3jp](https://github.com/qm3jp)
|
||||
- [johnnyg](https://github.com/johnnyg)
|
||||
- [JoshuaBoniface](https://github.com/joshuaboniface)
|
||||
- [nvllsvm](https://github.com/nvllsvm)
|
||||
- [JustAMan](https://github.com/JustAMan)
|
||||
- [dcrdev](https://github.com/dcrdev)
|
||||
- [EraYaN](https://github.com/EraYaN)
|
||||
- [flemse](https://github.com/flemse)
|
||||
- [bfayers](https://github.com/bfayers)
|
||||
- [Bond_009](https://github.com/Bond-009)
|
||||
- [AnthonyLavado](https://github.com/anthonylavado)
|
||||
- [dkanada](https://github.com/dkanada)
|
||||
- [sparky8251](https://github.com/sparky8251)
|
||||
- [LeoVerto](https://github.com/LeoVerto)
|
||||
- [cvium](https://github.com/cvium)
|
||||
- [grafixeyehero](https://github.com/grafixeyehero)
|
||||
- [Drago96](https://github.com/drago-96)
|
||||
- [ViXXoR](https://github.com/ViXXoR)
|
||||
- [nkmerrill](https://github.com/nkmerrill)
|
||||
- [TtheCreator](https://github.com/Tthecreator)
|
||||
- [RazeLighter777](https://github.com/RazeLighter777)
|
||||
- [LogicalPhallacy](https://github.com/LogicalPhallacy)
|
||||
- [thornbill](https://github.com/thornbill)
|
||||
- [redSpoutnik](https://github.com/redSpoutnik)
|
||||
- [DrPandemic](https://github.com/drpandemic)
|
||||
- [Oddstr13](https://github.com/oddstr13)
|
||||
- [petermcneil](https://github.com/petermcneil)
|
||||
- [lewazo](https://github.com/lewazo)
|
||||
- [Raghu Saxena](https://github.com/ckcr4lyf)
|
||||
- [Nickbert7](https://github.com/Nickbert7)
|
||||
- [ferferga](https://github.com/ferferga)
|
||||
- [bilde2910](https://github.com/bilde2910)
|
||||
- [Daniel Hartung](https://github.com/dhartung)
|
||||
- [Ryan Hartzell](https://github.com/ryan-hartzell)
|
||||
|
||||
## Emby Contributors
|
||||
# Emby Contributors
|
||||
|
||||
- [LukePulverenti](https://github.com/LukePulverenti)
|
||||
- [ebr11](https://github.com/ebr11)
|
||||
- [lalmanzar](https://github.com/lalmanzar)
|
||||
- [schneifu](https://github.com/schneifu)
|
||||
- [Mark2xv](https://github.com/Mark2xv)
|
||||
- [ScottRapsey](https://github.com/ScottRapsey)
|
||||
- [skynet600](https://github.com/skynet600)
|
||||
- [Cheesegeezer](https://githum.com/Cheesegeezer)
|
||||
- [Radeon](https://github.com/radeonorama)
|
||||
- [gcw07](https://github.com/gcw07)
|
||||
- [SivaramAdhiappan](https://github.com/shivaram1190)
|
||||
- [CWatkinsNash](https://github.com/CWatkinsNash)
|
||||
- [sfnetwork](https://github.com/sfnetwork)
|
||||
- [Logos302](https://github.com/Logos302)
|
||||
- [TheWorkz](https://github.com/TheWorkz)
|
||||
- [mboehler](https://github.com/mboehler)
|
||||
- [KaHooli](https://github.com/KaHooli)
|
||||
- [xzener](https://github.com/xzener)
|
||||
- [CBers](https://github.com/CBers)
|
||||
- [Sagaia](https://github.com/Sagaia)
|
||||
- [JHawk111](https://github.com/JHawk111)
|
||||
- [David3663](https://github.com/david3663)
|
||||
- [Smyken](https://github.com/Smyken)
|
||||
- [doron1](https://github.com/doron1)
|
||||
- [brainfryd](https://github.com/brainfryd)
|
||||
- [DGMayor](http://github.com/DGMayor)
|
||||
- [Jon-theHTPC](https://github.com/Jon-theHTPC)
|
||||
- [aspdend](https://github.com/aspdend)
|
||||
- [RedshirtMB](https://github.com/RedshirtMB)
|
||||
- [thealienamongus](https://github.com/thealienamongus)
|
||||
- [brocass](https://github.com/brocass)
|
||||
- [pjrollo2000](https://github.com/pjrollo2000)
|
||||
- [abobader](https://github.com/abobader)
|
||||
- [milli260876](https://github.com/milli260876)
|
||||
- [vileboy](https://github.com/vileboy)
|
||||
- [starkadius](https://github.com/starkadius)
|
||||
- [wraslor](https://github.com/wraslor)
|
||||
- [mrwebsmith](https://github.com/mrwebsmith)
|
||||
- [rickster53](https://github.com/rickster53)
|
||||
- [Tharnax](https://github.com/Tharnax)
|
||||
- [0sm0](https://github.com/0sm0)
|
||||
- [swhitmore](https://github.com/swhitmore)
|
||||
- [DigiTM](https://github.com/DigiTM)
|
||||
- [crisliv / xliv](https://github.com/crisliv)
|
||||
- [Yogi](https://github.com/yogi12)
|
||||
- [madFloyd](https://github.com/madFloyd)
|
||||
- [yardameus](https://github.com/yardameus)
|
||||
- [rrb008](https://github.com/rrb008)
|
||||
- [Toonguy](https://github.com/Toonguy)
|
||||
- [Alwin Hummels](https://github.com/AlwinHummels)
|
||||
- [trooper11](https://github.com/trooper11)
|
||||
- [danlotfy](https://github.com/danlotfy)
|
||||
- [jordy1955](https://github.com/jordy1955)
|
||||
- [JoshFink](https://github.com/JoshFink)
|
||||
- [Detector1](https://github.com/Detector1)
|
||||
- [BlackIce013](https://github.com/blackice013)
|
||||
- [mporcas](https://github.com/mporcas)
|
||||
- [tikuf](https://github.com/tikuf/)
|
||||
- [Tim Hobbs](https://github.com/timhobbs)
|
||||
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
|
||||
<!--
|
||||
NOTE: This is the end of the list of past Emby Contributors.
|
||||
New Jellyfin contributors should add their name to the end
|
||||
of the list of Jellyfin Contributors above. NOT HERE ;)
|
||||
-->
|
||||
- [LukePulverenti](https://github.com/LukePulverenti)
|
||||
- [ebr11](https://github.com/ebr11)
|
||||
- [lalmanzar](https://github.com/lalmanzar)
|
||||
- [schneifu](https://github.com/schneifu)
|
||||
- [Mark2xv](https://github.com/Mark2xv)
|
||||
- [ScottRapsey](https://github.com/ScottRapsey)
|
||||
- [skynet600](https://github.com/skynet600)
|
||||
- [Cheesegeezer](https://githum.com/Cheesegeezer)
|
||||
- [Radeon](https://github.com/radeonorama)
|
||||
- [gcw07](https://github.com/gcw07)
|
||||
- [SivaramAdhiappan](https://github.com/shivaram1190)
|
||||
- [CWatkinsNash](https://github.com/CWatkinsNash)
|
||||
- [sfnetwork](https://github.com/sfnetwork)
|
||||
- [Logos302](https://github.com/Logos302)
|
||||
- [TheWorkz](https://github.com/TheWorkz)
|
||||
- [mboehler](https://github.com/mboehler)
|
||||
- [KaHooli](https://github.com/KaHooli)
|
||||
- [xzener](https://github.com/xzener)
|
||||
- [CBers](https://github.com/CBers)
|
||||
- [Sagaia](https://github.com/Sagaia)
|
||||
- [JHawk111](https://github.com/JHawk111)
|
||||
- [David3663](https://github.com/david3663)
|
||||
- [Smyken](https://github.com/Smyken)
|
||||
- [doron1](https://github.com/doron1)
|
||||
- [brainfryd](https://github.com/brainfryd)
|
||||
- [DGMayor](http://github.com/DGMayor)
|
||||
- [Jon-theHTPC](https://github.com/Jon-theHTPC)
|
||||
- [aspdend](https://github.com/aspdend)
|
||||
- [RedshirtMB](https://github.com/RedshirtMB)
|
||||
- [thealienamongus](https://github.com/thealienamongus)
|
||||
- [brocass](https://github.com/brocass)
|
||||
- [pjrollo2000](https://github.com/pjrollo2000)
|
||||
- [abobader](https://github.com/abobader)
|
||||
- [milli260876](https://github.com/milli260876)
|
||||
- [vileboy](https://github.com/vileboy)
|
||||
- [starkadius](https://github.com/starkadius)
|
||||
- [wraslor](https://github.com/wraslor)
|
||||
- [mrwebsmith](https://github.com/mrwebsmith)
|
||||
- [rickster53](https://github.com/rickster53)
|
||||
- [Tharnax](https://github.com/Tharnax)
|
||||
- [0sm0](https://github.com/0sm0)
|
||||
- [swhitmore](https://github.com/swhitmore)
|
||||
- [DigiTM](https://github.com/DigiTM)
|
||||
- [crisliv / xliv](https://github.com/crisliv)
|
||||
- [Yogi](https://github.com/yogi12)
|
||||
- [madFloyd](https://github.com/madFloyd)
|
||||
- [yardameus](https://github.com/yardameus)
|
||||
- [rrb008](https://github.com/rrb008)
|
||||
- [Toonguy](https://github.com/Toonguy)
|
||||
- [Alwin Hummels](https://github.com/AlwinHummels)
|
||||
- [trooper11](https://github.com/trooper11)
|
||||
- [danlotfy](https://github.com/danlotfy)
|
||||
- [jordy1955](https://github.com/jordy1955)
|
||||
- [JoshFink](https://github.com/JoshFink)
|
||||
- [Detector1](https://github.com/Detector1)
|
||||
- [BlackIce013](https://github.com/blackice013)
|
||||
- [mporcas](https://github.com/mporcas)
|
||||
- [tikuf](https://github.com/tikuf/)
|
||||
- [Tim Hobbs](https://github.com/timhobbs)
|
||||
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
|
||||
|
||||
108
README.md
108
README.md
@@ -1,111 +1,15 @@
|
||||
<h1 align="center">Jellyfin Web</h1>
|
||||
<h3 align="center">Part of the <a href="https://jellyfin.org">Jellyfin Project</a></h3>
|
||||
<h3 align="center">The Free Software Media System</h3>
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<img alt="Logo Banner" src="https://raw.githubusercontent.com/jellyfin/jellyfin-ux/master/branding/SVG/banner-logo-solid.svg?sanitize=true"/>
|
||||
<br/>
|
||||
<br/>
|
||||
<a href="https://github.com/jellyfin/jellyfin-web">
|
||||
<img alt="GPL 2.0 License" src="https://img.shields.io/github/license/jellyfin/jellyfin-web.svg"/>
|
||||
</a>
|
||||
<a href="https://github.com/jellyfin/jellyfin-web/releases">
|
||||
<img alt="Current Release" src="https://img.shields.io/github/release/jellyfin/jellyfin-web.svg"/>
|
||||
</a>
|
||||
<a href="https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/?utm_source=widget">
|
||||
<img src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-web/svg-badge.svg" alt="Translation Status"/>
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://opencollective.com/jellyfin">
|
||||
<img alt="Donate" src="https://img.shields.io/opencollective/all/jellyfin.svg?label=backers"/>
|
||||
</a>
|
||||
<a href="https://features.jellyfin.org">
|
||||
<img alt="Feature Requests" src="https://img.shields.io/badge/fider-vote%20on%20features-success.svg"/>
|
||||
</a>
|
||||
<a href="https://matrix.to/#/+jellyfin:matrix.org">
|
||||
<img alt="Chat on Matrix" src="https://img.shields.io/matrix/jellyfin:matrix.org.svg?logo=matrix"/>
|
||||
</a>
|
||||
<a href="https://www.reddit.com/r/jellyfin">
|
||||
<img alt="Join our Subreddit" src="https://img.shields.io/badge/reddit-r%2Fjellyfin-%23FF5700.svg"/>
|
||||
</a>
|
||||
<br/><br/>
|
||||
<a href="https://github.com/jellyfin/jellyfin-web"><img alt="GPL 2.0 License" src="https://img.shields.io/github/license/jellyfin/jellyfin-web.svg"/></a>
|
||||
<a href="https://github.com/jellyfin/jellyfin-web/releases"><img alt="Current Release" src="https://img.shields.io/github/release/jellyfin/jellyfin-web.svg"/></a>
|
||||
</p>
|
||||
|
||||
Jellyfin Web is the frontend used for most of the clients available for end users, such as desktop browsers, Android, and iOS. We welcome all contributions and pull requests! If you have a larger feature in mind please open an issue so we can discuss the implementation before you start. Translations can be improved very easily from our <a href="https://translate.jellyfin.org/projects/jellyfin/jellyfin-web">Weblate</a> instance. Look through the following graphic to see if your native language could use some work!
|
||||
---
|
||||
|
||||
<a href="https://translate.jellyfin.org/engage/jellyfin/?utm_source=widget">
|
||||
<img src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-web/multi-auto.svg" alt="Detailed Translation Status"/>
|
||||
</a>
|
||||
|
||||
## Build Process
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download)
|
||||
- npm (included in Node.js)
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. Clone or download this repository.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/jellyfin/jellyfin-web.git
|
||||
cd jellyfin-web
|
||||
```
|
||||
|
||||
2. Install build dependencies in the project directory.
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Run the web client with webpack for local development.
|
||||
|
||||
```sh
|
||||
npm start
|
||||
```
|
||||
|
||||
4. Build the client with sourcemaps available.
|
||||
|
||||
```sh
|
||||
npm run build:development
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
> [!NOTE]
|
||||
> We are in the process of refactoring to a [new structure](https://forum.jellyfin.org/t-proposed-update-to-the-structure-of-jellyfin-web) based on [Bulletproof React](https://github.com/alan2207/bulletproof-react/blob/master/docs/project-structure.md) architecture guidelines.
|
||||
> Most new code should be organized under the appropriate app directory unless it is common/shared.
|
||||
|
||||
```
|
||||
.
|
||||
└── src
|
||||
├── apps
|
||||
│ ├── dashboard # Admin dashboard app
|
||||
│ ├── experimental # New experimental app
|
||||
│ ├── stable # Classic (stable) app
|
||||
│ └── wizard # Startup wizard app
|
||||
├── assets # Static assets
|
||||
├── components # Higher order visual components and React components
|
||||
├── constants # Common constant values
|
||||
├── controllers # Legacy page views and controllers 🧹 ❌
|
||||
├── elements # Basic webcomponents and React equivalents 🧹
|
||||
├── hooks # Custom React hooks
|
||||
├── lib # Reusable libraries
|
||||
│ ├── globalize # Custom localization library
|
||||
│ ├── jellyfin-apiclient # Supporting code for the deprecated apiclient package
|
||||
│ ├── legacy # Polyfills for legacy browsers
|
||||
│ ├── navdrawer # Navigation drawer library for classic layout
|
||||
│ └── scroller # Content scrolling library
|
||||
├── plugins # Client plugins (features dynamically loaded at runtime)
|
||||
├── scripts # Random assortment of visual components and utilities 🐉 ❌
|
||||
├── strings # Translation files (only commit changes to en-us.json)
|
||||
├── styles # Common app Sass stylesheets
|
||||
├── themes # Sass and MUI themes
|
||||
├── types # Common TypeScript interfaces/types
|
||||
└── utils # Utility functions
|
||||
```
|
||||
|
||||
- ❌ — Deprecated, do **not** create new files here
|
||||
- 🧹 — Needs cleanup
|
||||
- 🐉 — Serious mess (Here be dragons)
|
||||
Jellyfin is a free software media system that puts you in control of managing and streaming your media.
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
module.exports = {
|
||||
babelrcRoots: [
|
||||
// Keep the root as a root
|
||||
'.'
|
||||
],
|
||||
sourceType: 'unambiguous',
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
useBuiltIns: 'usage',
|
||||
corejs: 3
|
||||
}
|
||||
],
|
||||
'@babel/preset-react'
|
||||
],
|
||||
plugins: [
|
||||
]
|
||||
};
|
||||
29
bump_version
29
bump_version
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# bump_version - increase the shared version and generate changelogs
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o xtrace
|
||||
|
||||
usage() {
|
||||
echo -e "bump_version - increase the shared version"
|
||||
echo -e ""
|
||||
echo -e "Usage:"
|
||||
echo -e " $ bump_version <new_version>"
|
||||
}
|
||||
|
||||
if [[ -z $1 ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
new_version="$1"
|
||||
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
|
||||
|
||||
# Bump the NPM version
|
||||
npm --no-git-tag-version --allow-same-version version v${new_version_sed}
|
||||
|
||||
# Stage the changed files for commit
|
||||
git add .
|
||||
git status -v
|
||||
@@ -1,10 +0,0 @@
|
||||
module.exports = {
|
||||
preset: [
|
||||
'default',
|
||||
// Turn off `mergeLonghand` because it combines `padding-*` and `margin-*`,
|
||||
// breaking fallback styles.
|
||||
// https://github.com/cssnano/cssnano/issues/1163
|
||||
// https://github.com/cssnano/cssnano/issues/1192
|
||||
{ mergeLonghand: false }
|
||||
]
|
||||
};
|
||||
@@ -1,417 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import eslint from '@eslint/js';
|
||||
import comments from '@eslint-community/eslint-plugin-eslint-comments/configs';
|
||||
import compat from 'eslint-plugin-compat';
|
||||
import globals from 'globals';
|
||||
// @ts-expect-error Missing type definition
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import restrictedGlobals from 'confusing-browser-globals';
|
||||
import sonarjs from 'eslint-plugin-sonarjs';
|
||||
import stylistic from '@stylistic/eslint-plugin';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
// @ts-expect-error Harmless type mismatch in dependency
|
||||
comments.recommended,
|
||||
compat.configs['flat/recommended'],
|
||||
importPlugin.flatConfigs.errors,
|
||||
sonarjs.configs.recommended,
|
||||
|
||||
reactPlugin.configs.flat.recommended,
|
||||
{
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
}
|
||||
},
|
||||
jsxA11y.flatConfigs.recommended,
|
||||
|
||||
// Global ignores
|
||||
{
|
||||
ignores: [
|
||||
'node_modules',
|
||||
'coverage',
|
||||
'dist',
|
||||
'.idea',
|
||||
'.vscode'
|
||||
]
|
||||
},
|
||||
|
||||
// Global style rules
|
||||
{
|
||||
plugins: {
|
||||
'@stylistic': stylistic
|
||||
},
|
||||
extends: [ importPlugin.flatConfigs.typescript ],
|
||||
rules: {
|
||||
'array-callback-return': ['error', { 'checkForEach': true }],
|
||||
'curly': ['error', 'multi-line', 'consistent'],
|
||||
'default-case-last': 'error',
|
||||
'max-params': ['error', 7],
|
||||
'new-cap': [
|
||||
'error',
|
||||
{
|
||||
'capIsNewExceptions': ['jQuery.Deferred'],
|
||||
'newIsCapExceptionPattern': '\\.default$'
|
||||
}
|
||||
],
|
||||
'no-duplicate-imports': 'error',
|
||||
'no-empty-function': 'error',
|
||||
'no-extend-native': 'error',
|
||||
'no-lonely-if': 'error',
|
||||
'no-nested-ternary': 'error',
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': ['error', { builtinGlobals: false }],
|
||||
'no-restricted-globals': ['error'].concat(restrictedGlobals),
|
||||
'no-return-assign': 'error',
|
||||
'no-return-await': 'error',
|
||||
'no-sequences': ['error', { 'allowInParentheses': false }],
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': 'error',
|
||||
'no-throw-literal': 'error',
|
||||
'no-undef-init': 'error',
|
||||
'no-unneeded-ternary': 'error',
|
||||
'no-unused-expressions': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
|
||||
'no-unused-private-class-members': 'error',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'no-useless-rename': 'error',
|
||||
'no-useless-constructor': 'off',
|
||||
'@typescript-eslint/no-useless-constructor': 'error',
|
||||
'no-var': 'error',
|
||||
'no-void': ['error', { 'allowAsStatement': true }],
|
||||
'no-warning-comments': ['warn', { 'terms': ['hack', 'xxx'] }],
|
||||
'one-var': ['error', 'never'],
|
||||
'prefer-const': ['error', { 'destructuring': 'all' }],
|
||||
'prefer-promise-reject-errors': ['warn', { 'allowEmptyReject': true }],
|
||||
'@typescript-eslint/prefer-for-of': 'error',
|
||||
'radix': 'error',
|
||||
'yoda': 'error',
|
||||
|
||||
'sonarjs/fixme-tag': 'warn',
|
||||
'sonarjs/todo-tag': 'off',
|
||||
'sonarjs/deprecation': 'off',
|
||||
'sonarjs/no-alphabetical-sort': 'warn',
|
||||
'sonarjs/no-inverted-boolean-check': 'error',
|
||||
'sonarjs/no-selector-parameter': 'off',
|
||||
'sonarjs/pseudo-random': 'warn',
|
||||
// TODO: Enable the following sonarjs rules and fix issues
|
||||
'sonarjs/no-duplicate-string': 'off',
|
||||
'sonarjs/no-nested-functions': 'warn',
|
||||
|
||||
// TODO: Replace with stylistic.configs.customize()
|
||||
'@stylistic/block-spacing': 'error',
|
||||
'@stylistic/brace-style': ['error', '1tbs', { 'allowSingleLine': true }],
|
||||
'@stylistic/comma-dangle': ['error', 'never'],
|
||||
'@stylistic/comma-spacing': 'error',
|
||||
'@stylistic/eol-last': 'error',
|
||||
'@stylistic/indent': ['error', 4, { 'SwitchCase': 1 }],
|
||||
'@stylistic/jsx-quotes': ['error', 'prefer-single'],
|
||||
'@stylistic/keyword-spacing': 'error',
|
||||
'@stylistic/max-statements-per-line': 'error',
|
||||
'@stylistic/no-floating-decimal': 'error',
|
||||
'@stylistic/no-mixed-spaces-and-tabs': 'error',
|
||||
'@stylistic/no-multi-spaces': 'error',
|
||||
'@stylistic/no-multiple-empty-lines': ['error', { 'max': 1 }],
|
||||
'@stylistic/no-trailing-spaces': 'error',
|
||||
'@stylistic/object-curly-spacing': ['error', 'always'],
|
||||
'@stylistic/operator-linebreak': ['error', 'before', { overrides: { '?': 'after', ':': 'after', '=': 'after' } }],
|
||||
'@stylistic/padded-blocks': ['error', 'never'],
|
||||
'@stylistic/quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
|
||||
'@stylistic/semi': 'error',
|
||||
'@stylistic/space-before-blocks': 'error',
|
||||
'@stylistic/space-infix-ops': 'error',
|
||||
|
||||
'@typescript-eslint/no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: '@jellyfin/sdk/lib/generated-client',
|
||||
message: 'Use direct file imports for tree-shaking',
|
||||
allowTypeImports: true
|
||||
},
|
||||
{
|
||||
name: '@jellyfin/sdk/lib/generated-client/api',
|
||||
message: 'Use direct file imports for tree-shaking',
|
||||
allowTypeImports: true
|
||||
},
|
||||
{
|
||||
name: '@jellyfin/sdk/lib/generated-client/models',
|
||||
message: 'Use direct file imports for tree-shaking',
|
||||
allowTypeImports: true
|
||||
},
|
||||
{
|
||||
name: '@mui/icons-material',
|
||||
message: 'Use direct file imports for tree-shaking',
|
||||
allowTypeImports: true
|
||||
},
|
||||
{
|
||||
name: '@mui/material',
|
||||
message: 'Use direct file imports for tree-shaking',
|
||||
allowTypeImports: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Config files use node globals
|
||||
{
|
||||
ignores: [ 'src' ],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Config files are commonjs by default
|
||||
{
|
||||
files: [ '**/*.{cjs,js}' ],
|
||||
ignores: [ 'src' ],
|
||||
languageOptions: {
|
||||
sourceType: 'commonjs'
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-require-imports': 'off'
|
||||
}
|
||||
},
|
||||
|
||||
// App files
|
||||
{
|
||||
files: [
|
||||
'src/**/*.{js,jsx,ts,tsx}'
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
// Tizen globals
|
||||
'tizen': false,
|
||||
'webapis': false,
|
||||
// WebOS globals
|
||||
'webOS': false,
|
||||
// Dependency globals
|
||||
'$': false,
|
||||
'jQuery': false,
|
||||
// Jellyfin globals
|
||||
'ApiClient': true,
|
||||
'Events': true,
|
||||
'chrome': true,
|
||||
'Emby': false,
|
||||
'Hls': true,
|
||||
'LibraryMenu': true,
|
||||
'Windows': false,
|
||||
// Build time definitions
|
||||
__COMMIT_SHA__: false,
|
||||
__JF_BUILD_VERSION__: false,
|
||||
__PACKAGE_JSON_NAME__: false,
|
||||
__PACKAGE_JSON_VERSION__: false,
|
||||
__USE_SYSTEM_FONTS__: false,
|
||||
__WEBPACK_SERVE__: false
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
node: {
|
||||
extensions: [
|
||||
'.js',
|
||||
'.ts',
|
||||
'.jsx',
|
||||
'.tsx'
|
||||
],
|
||||
moduleDirectory: [
|
||||
'node_modules',
|
||||
'src'
|
||||
]
|
||||
}
|
||||
},
|
||||
polyfills: [
|
||||
'Promise',
|
||||
// whatwg-fetch
|
||||
'fetch',
|
||||
// document-register-element
|
||||
'document.registerElement',
|
||||
// resize-observer-polyfill
|
||||
'ResizeObserver',
|
||||
// fast-text-encoding
|
||||
'TextEncoder',
|
||||
// intersection-observer
|
||||
'IntersectionObserver',
|
||||
// Core-js
|
||||
'Object.assign',
|
||||
'Object.is',
|
||||
'Object.setPrototypeOf',
|
||||
'Object.toString',
|
||||
'Object.freeze',
|
||||
'Object.seal',
|
||||
'Object.preventExtensions',
|
||||
'Object.isFrozen',
|
||||
'Object.isSealed',
|
||||
'Object.isExtensible',
|
||||
'Object.getOwnPropertyDescriptor',
|
||||
'Object.getPrototypeOf',
|
||||
'Object.keys',
|
||||
'Object.entries',
|
||||
'Object.getOwnPropertyNames',
|
||||
'Function.name',
|
||||
'Function.hasInstance',
|
||||
'Array.from',
|
||||
'Array.arrayOf',
|
||||
'Array.copyWithin',
|
||||
'Array.fill',
|
||||
'Array.find',
|
||||
'Array.findIndex',
|
||||
'Array.iterator',
|
||||
'String.fromCodePoint',
|
||||
'String.raw',
|
||||
'String.iterator',
|
||||
'String.codePointAt',
|
||||
'String.endsWith',
|
||||
'String.includes',
|
||||
'String.repeat',
|
||||
'String.startsWith',
|
||||
'String.trim',
|
||||
'String.anchor',
|
||||
'String.big',
|
||||
'String.blink',
|
||||
'String.bold',
|
||||
'String.fixed',
|
||||
'String.fontcolor',
|
||||
'String.fontsize',
|
||||
'String.italics',
|
||||
'String.link',
|
||||
'String.small',
|
||||
'String.strike',
|
||||
'String.sub',
|
||||
'String.sup',
|
||||
'RegExp',
|
||||
'Number',
|
||||
'Math',
|
||||
'Date',
|
||||
'async',
|
||||
'Symbol',
|
||||
'Map',
|
||||
'Set',
|
||||
'WeakMap',
|
||||
'WeakSet',
|
||||
'ArrayBuffer',
|
||||
'DataView',
|
||||
'Int8Array',
|
||||
'Uint8Array',
|
||||
'Uint8ClampedArray',
|
||||
'Int16Array',
|
||||
'Uint16Array',
|
||||
'Int32Array',
|
||||
'Uint32Array',
|
||||
'Float32Array',
|
||||
'Float64Array',
|
||||
'Reflect'
|
||||
]
|
||||
},
|
||||
rules: {
|
||||
// TODO: Add typescript recommended typed rules
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
{
|
||||
selector: 'default',
|
||||
format: [ 'camelCase', 'PascalCase' ],
|
||||
leadingUnderscore: 'allow'
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
format: [ 'camelCase', 'PascalCase', 'UPPER_CASE' ],
|
||||
leadingUnderscore: 'allowSingleOrDouble',
|
||||
trailingUnderscore: 'allowSingleOrDouble'
|
||||
},
|
||||
{
|
||||
selector: 'typeLike',
|
||||
format: [ 'PascalCase' ]
|
||||
},
|
||||
{
|
||||
selector: 'enumMember',
|
||||
format: [ 'PascalCase', 'UPPER_CASE' ]
|
||||
},
|
||||
{
|
||||
selector: [ 'objectLiteralProperty', 'typeProperty' ],
|
||||
format: [ 'camelCase', 'PascalCase' ],
|
||||
leadingUnderscore: 'allowSingleOrDouble',
|
||||
trailingUnderscore: 'allowSingleOrDouble'
|
||||
},
|
||||
// Ignore numbers, locale strings (en-us), aria/data attributes and CSS selectors
|
||||
{
|
||||
selector: [ 'objectLiteralProperty', 'typeProperty' ],
|
||||
format: null,
|
||||
filter: {
|
||||
regex: '[ &\\-]|^([0-9]+)$',
|
||||
match: true
|
||||
}
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/no-deprecated': 'warn',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/prefer-string-starts-ends-with': 'error'
|
||||
}
|
||||
},
|
||||
|
||||
// React files
|
||||
{
|
||||
files: [ 'src/**/*.{jsx,tsx}' ],
|
||||
plugins: {
|
||||
'react-hooks': reactHooks
|
||||
},
|
||||
rules: {
|
||||
'react/jsx-filename-extension': ['error', { 'extensions': ['.jsx', '.tsx'] }],
|
||||
'react/jsx-no-bind': 'error',
|
||||
'react/jsx-no-useless-fragment': 'error',
|
||||
'react/no-array-index-key': 'error',
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn'
|
||||
}
|
||||
},
|
||||
|
||||
// Service worker
|
||||
{
|
||||
files: [ 'src/serviceworker.js' ],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.serviceworker
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Legacy JS (less strict)
|
||||
{
|
||||
files: [ 'src/**/*.{js,jsx}' ],
|
||||
rules: {
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'@typescript-eslint/no-this-alias': 'off',
|
||||
|
||||
'sonarjs/public-static-readonly': 'off',
|
||||
|
||||
// TODO: Enable the following rules and fix issues
|
||||
'sonarjs/cognitive-complexity': 'off',
|
||||
'sonarjs/constructor-for-side-effects': 'off',
|
||||
'sonarjs/function-return-type': 'off',
|
||||
'sonarjs/no-async-constructor': 'off',
|
||||
'sonarjs/no-duplicate-string': 'off',
|
||||
'sonarjs/no-ignored-exceptions': 'off',
|
||||
'sonarjs/no-invariant-returns': 'warn',
|
||||
'sonarjs/no-nested-functions': 'off',
|
||||
'sonarjs/void-use': 'off'
|
||||
}
|
||||
}
|
||||
);
|
||||
60
flake.lock
generated
60
flake.lock
generated
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1739874174,
|
||||
"narHash": "sha256-XGxSVtojlwjYRYGvGXex0Cw+/363EVJlbY9TPX9bARk=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d2ab2691c798f6b633be91d74b1626980ddaff30",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
34
flake.nix
34
flake.nix
@@ -1,34 +0,0 @@
|
||||
{
|
||||
description = "jellyfin-web nix flake";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
in {
|
||||
devShell = with pkgs;
|
||||
mkShell rec {
|
||||
buildInputs = [
|
||||
nodejs_20
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
# Also see: https://github.com/sass/embedded-host-node/issues/334
|
||||
echo "Removing sass-embedded from node-modules as its broken on NixOS."
|
||||
rm -rf node_modules/sass-embedded*
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
41458
package-lock.json
generated
41458
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
186
package.json
186
package.json
@@ -1,175 +1,35 @@
|
||||
{
|
||||
"name": "jellyfin-web",
|
||||
"version": "10.12.0",
|
||||
"version": "0.0.0",
|
||||
"description": "Web interface for Jellyfin",
|
||||
"repository": "https://github.com/jellyfin/jellyfin-web",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.27.4",
|
||||
"@babel/plugin-transform-modules-umd": "7.27.1",
|
||||
"@babel/preset-env": "7.27.2",
|
||||
"@babel/preset-react": "7.27.1",
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "4.5.0",
|
||||
"@eslint/js": "9.30.1",
|
||||
"@stylistic/eslint-plugin": "5.6.1",
|
||||
"@stylistic/stylelint-plugin": "3.1.3",
|
||||
"@types/dompurify": "3.0.5",
|
||||
"@types/escape-html": "1.0.4",
|
||||
"@types/loadable__component": "5.13.9",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/markdown-it": "14.1.2",
|
||||
"@types/react": "18.3.23",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/react-lazy-load-image-component": "1.6.4",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@typescript-eslint/parser": "8.35.1",
|
||||
"@uupaa/dynamic-import-polyfill": "1.0.2",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"autoprefixer": "10.4.21",
|
||||
"babel-loader": "10.0.0",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"confusing-browser-globals": "1.0.11",
|
||||
"copy-webpack-plugin": "13.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "7.1.2",
|
||||
"cssnano": "7.0.7",
|
||||
"es-check": "9.1.4",
|
||||
"eslint": "9.30.1",
|
||||
"eslint-plugin-compat": "6.0.2",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "7.0.1",
|
||||
"eslint-plugin-sonarjs": "3.0.4",
|
||||
"expose-loader": "5.0.1",
|
||||
"fast-glob": "3.3.3",
|
||||
"fork-ts-checker-webpack-plugin": "9.1.0",
|
||||
"globals": "16.2.0",
|
||||
"html-loader": "5.1.0",
|
||||
"html-webpack-plugin": "5.6.3",
|
||||
"jsdom": "26.1.0",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"postcss": "8.5.6",
|
||||
"postcss-loader": "8.1.1",
|
||||
"postcss-preset-env": "10.2.3",
|
||||
"postcss-scss": "4.0.9",
|
||||
"sass": "1.89.2",
|
||||
"sass-loader": "16.0.5",
|
||||
"source-map-loader": "5.0.0",
|
||||
"speed-measure-webpack-plugin": "1.5.0",
|
||||
"style-loader": "4.0.0",
|
||||
"stylelint": "16.21.0",
|
||||
"stylelint-config-rational-order": "0.1.2",
|
||||
"stylelint-no-browser-hacks": "2.0.0",
|
||||
"stylelint-order": "7.0.0",
|
||||
"stylelint-scss": "6.12.1",
|
||||
"ts-loader": "9.5.2",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.35.1",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.2.4",
|
||||
"webpack": "5.99.9",
|
||||
"webpack-bundle-analyzer": "4.10.2",
|
||||
"webpack-cli": "6.0.1",
|
||||
"webpack-dev-server": "5.2.2",
|
||||
"webpack-merge": "6.0.1",
|
||||
"worker-loader": "3.0.8"
|
||||
"copy-webpack-plugin": "^5.0.3",
|
||||
"css-loader": "^2.1.0",
|
||||
"eslint": "^5.16.0",
|
||||
"file-loader": "^3.0.1",
|
||||
"style-loader": "^0.23.1",
|
||||
"webpack": "^4.41.0",
|
||||
"webpack-cli": "^3.3.9",
|
||||
"webpack-dev-server": "^3.8.1",
|
||||
"webpack-merge": "^4.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.14.0",
|
||||
"@emotion/styled": "11.14.0",
|
||||
"@fontsource/noto-sans": "5.2.7",
|
||||
"@fontsource/noto-sans-hk": "5.2.6",
|
||||
"@fontsource/noto-sans-jp": "5.2.6",
|
||||
"@fontsource/noto-sans-kr": "5.2.6",
|
||||
"@fontsource/noto-sans-sc": "5.2.6",
|
||||
"@fontsource/noto-sans-tc": "5.2.6",
|
||||
"@jellyfin/libass-wasm": "4.2.3",
|
||||
"@jellyfin/sdk": "0.0.0-unstable.202512091852",
|
||||
"@jellyfin/ux-web": "1.0.0",
|
||||
"@mui/icons-material": "6.4.12",
|
||||
"@mui/material": "6.4.12",
|
||||
"@mui/x-date-pickers": "7.29.4",
|
||||
"@react-hook/resize-observer": "2.0.2",
|
||||
"@tanstack/react-query": "5.80.10",
|
||||
"@tanstack/react-query-devtools": "5.80.10",
|
||||
"abortcontroller-polyfill": "1.7.8",
|
||||
"blurhash": "2.0.5",
|
||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.43.0",
|
||||
"date-fns": "2.30.0",
|
||||
"dompurify": "2.5.8",
|
||||
"element-closest-polyfill": "1.0.7",
|
||||
"epubjs": "0.3.93",
|
||||
"escape-html": "1.0.3",
|
||||
"fast-text-encoding": "1.0.6",
|
||||
"flv.js": "1.6.2",
|
||||
"headroom.js": "0.12.0",
|
||||
"history": "5.3.0",
|
||||
"hls.js": "1.6.13",
|
||||
"intersection-observer": "0.12.2",
|
||||
"jellyfin-apiclient": "1.11.0",
|
||||
"jquery": "3.7.1",
|
||||
"jstree": "3.3.17",
|
||||
"libarchive.js": "2.0.2",
|
||||
"libpgs": "0.8.1",
|
||||
"lodash-es": "4.17.21",
|
||||
"markdown-it": "14.1.0",
|
||||
"material-design-icons-iconfont": "6.7.0",
|
||||
"material-react-table": "3.2.1",
|
||||
"native-promise-only": "0.8.1",
|
||||
"pdfjs-dist": "3.11.174",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
"react": "18.3.1",
|
||||
"react-blurhash": "0.3.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-lazy-load-image-component": "1.6.3",
|
||||
"react-router-dom": "6.30.1",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"screenfull": "6.0.2",
|
||||
"sortablejs": "1.15.6",
|
||||
"swiper": "11.2.8",
|
||||
"usehooks-ts": "3.1.1",
|
||||
"webcomponents.js": "0.7.24",
|
||||
"whatwg-fetch": "3.6.20"
|
||||
"flv.js": "^1.5.0",
|
||||
"hls.js": "^0.12.4",
|
||||
"howler": "^2.1.2",
|
||||
"jquery": "^3.4.1",
|
||||
"jstree": "^3.3.7",
|
||||
"libjass": "^0.11.0",
|
||||
"shaka-player": "^2.5.5",
|
||||
"sortablejs": "^1.9.0",
|
||||
"swiper": "^3.4.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sass-embedded": "1.89.2"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 Firefox versions",
|
||||
"last 2 Chrome versions",
|
||||
"last 2 ChromeAndroid versions",
|
||||
"last 2 Safari versions",
|
||||
"iOS > 10",
|
||||
"last 2 Edge versions",
|
||||
"Chrome 27",
|
||||
"Chrome 38",
|
||||
"Chrome 47",
|
||||
"Chrome 53",
|
||||
"Chrome 56",
|
||||
"Chrome 63",
|
||||
"Edge 18",
|
||||
"Firefox ESR"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "npm run serve",
|
||||
"serve": "webpack serve --config webpack.dev.js",
|
||||
"build:analyze": "cross-env NODE_ENV=\"production\" webpack --config webpack.analyze.js",
|
||||
"build:development": "webpack --config webpack.dev.js",
|
||||
"build:production": "cross-env NODE_ENV=\"production\" webpack --config webpack.prod.js",
|
||||
"build:check": "tsc --noEmit",
|
||||
"build:es-check": "npm run build:production && npm run escheck",
|
||||
"escheck": "es-check",
|
||||
"lint": "eslint",
|
||||
"test": "vitest --watch=false --config vite.config.ts",
|
||||
"test:watch": "vitest --config vite.config.ts",
|
||||
"stylelint": "stylelint \"src/**/*.{css,scss}\""
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"npm": ">=9.6.4 <11.0.0",
|
||||
"yarn": "YARN NO LONGER USED - use npm instead."
|
||||
"serve": "webpack-dev-server --config webpack.dev.js --open",
|
||||
"build": "webpack --config webpack.prod.js",
|
||||
"lint": "eslint \"src\"",
|
||||
"prepare": "webpack --config webpack.prod.js"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
const packageConfig = require('./package.json');
|
||||
const postcssPresetEnv = require('postcss-preset-env');
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const cssnano = require('cssnano');
|
||||
|
||||
const config = () => ({
|
||||
plugins: [
|
||||
// Explicitly specify browserslist to override ones from node_modules
|
||||
// For example, Swiper has it in its package.json
|
||||
postcssPresetEnv({ browsers: packageConfig.browserslist }),
|
||||
autoprefixer({ overrideBrowserslist: packageConfig.browserslist }),
|
||||
cssnano()
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = config;
|
||||
@@ -1,33 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
|
||||
# load every string in the source language
|
||||
# print all duplicate values to a file
|
||||
|
||||
cwd = os.getcwd()
|
||||
source = cwd + '/../src/strings/en-us.json'
|
||||
|
||||
reverse = {}
|
||||
duplicates = {}
|
||||
|
||||
with open(source) as en:
|
||||
strings = json.load(en)
|
||||
for key, value in strings.items():
|
||||
if value not in reverse:
|
||||
reverse[value] = [key]
|
||||
else:
|
||||
reverse[value].append(key)
|
||||
|
||||
for key, value in reverse.items():
|
||||
if len(value) > 1:
|
||||
duplicates[key] = value
|
||||
|
||||
print('LENGTH: ' + str(len(duplicates)))
|
||||
with open('duplicates.txt', 'w') as out:
|
||||
for item in duplicates:
|
||||
out.write(json.dumps(item) + ': ')
|
||||
out.write(json.dumps(duplicates[item]) + '\n')
|
||||
out.close()
|
||||
|
||||
print('DONE')
|
||||
@@ -1,34 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
|
||||
# load text file containing unused keys
|
||||
# remove the keys from all string files
|
||||
|
||||
cwd = os.getcwd()
|
||||
langdir = cwd + '/../src/strings'
|
||||
langlst = os.listdir(langdir)
|
||||
|
||||
keys = []
|
||||
|
||||
with open('unused.txt', 'r') as f:
|
||||
for line in f:
|
||||
keys.append(line.strip('\n'))
|
||||
|
||||
for lang in langlst:
|
||||
with open(langdir + '/' + lang, 'r') as f:
|
||||
inde = 2
|
||||
if '\n \"' in f.read():
|
||||
inde = 4
|
||||
f.close()
|
||||
with open(langdir + '/' + lang, 'r+') as f:
|
||||
langjson = json.load(f)
|
||||
for key in keys:
|
||||
langjson.pop(key, None)
|
||||
f.seek(0)
|
||||
f.write(json.dumps(langjson, indent=inde, sort_keys=False, ensure_ascii=False))
|
||||
f.write('\n')
|
||||
f.truncate()
|
||||
f.close()
|
||||
|
||||
print('DONE')
|
||||
@@ -1,52 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
|
||||
# load every key in the source language
|
||||
# check the keys in all translations
|
||||
# remove keys that only exist in translations
|
||||
|
||||
cwd = os.getcwd()
|
||||
langdir = cwd + '/../src/strings'
|
||||
langlst = os.listdir(langdir)
|
||||
|
||||
langlst.remove('en-us.json')
|
||||
print(langlst)
|
||||
input('press enter to continue')
|
||||
|
||||
keysus = []
|
||||
missing = []
|
||||
|
||||
with open(langdir + '/' + 'en-us.json') as en:
|
||||
langus = json.load(en)
|
||||
for key in langus:
|
||||
keysus.append(key)
|
||||
|
||||
for lang in langlst:
|
||||
with open(langdir + '/' + lang, 'r') as f:
|
||||
inde = 2
|
||||
if '\n \"' in f.read():
|
||||
inde = 4
|
||||
f.close()
|
||||
with open(langdir + '/' + lang, 'r+') as f:
|
||||
langjson = json.load(f)
|
||||
langjnew = {}
|
||||
for key in langjson:
|
||||
if key in keysus:
|
||||
langjnew[key] = langjson[key]
|
||||
elif key not in missing:
|
||||
missing.append(key)
|
||||
f.seek(0)
|
||||
f.write(json.dumps(langjnew, indent=inde, sort_keys=False, ensure_ascii=False))
|
||||
f.write('\n')
|
||||
f.truncate()
|
||||
f.close()
|
||||
|
||||
print(missing)
|
||||
print('LENGTH: ' + str(len(missing)))
|
||||
with open('missing.txt', 'w') as out:
|
||||
for item in missing:
|
||||
out.write(item + '\n')
|
||||
out.close()
|
||||
|
||||
print('DONE')
|
||||
@@ -1,40 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
# load all keys in the source language
|
||||
# check entire codebase for usages
|
||||
# print unused keys to a text file
|
||||
# TODO: dynamic string usages cause false positives
|
||||
|
||||
cwd = os.getcwd()
|
||||
langdir = cwd + '/../src/strings'
|
||||
langlst = []
|
||||
langlst.append('en-us.json')
|
||||
|
||||
# unused keys
|
||||
dep = []
|
||||
|
||||
def grep(key):
|
||||
command = 'grep -r -E "(\\\"|\'|\{)%s(\\\"|\'|\})" --include=\*.{js,html} --exclude-dir=../src/strings ../src' % key
|
||||
p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
output = p.stdout.readlines()
|
||||
if output:
|
||||
print('DONE: ' + key)
|
||||
return True
|
||||
print('UNUSED: ' + key)
|
||||
dep.append(key)
|
||||
return False
|
||||
|
||||
for lang in langlst:
|
||||
with open(langdir + '/' + lang) as f:
|
||||
langjson = json.load(f)
|
||||
for key in langjson:
|
||||
grep(key)
|
||||
|
||||
print(dep)
|
||||
print('LENGTH: ' + str(len(dep)))
|
||||
with open('unused.txt', 'w') as out:
|
||||
for item in dep:
|
||||
out.write(item + '\n')
|
||||
out.close()
|
||||
@@ -1,31 +0,0 @@
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import React from 'react';
|
||||
|
||||
import { ApiProvider } from 'hooks/useApi';
|
||||
import { UserSettingsProvider } from 'hooks/useUserSettings';
|
||||
import { WebConfigProvider } from 'hooks/useWebConfig';
|
||||
import browser from 'scripts/browser';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
import RootAppRouter from 'RootAppRouter';
|
||||
|
||||
const useReactQueryDevtools = window.Proxy // '@tanstack/query-devtools' requires 'Proxy', which cannot be polyfilled for legacy browsers
|
||||
&& !browser.tv; // Don't use devtools on the TV as the navigation is weird
|
||||
|
||||
const RootApp = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ApiProvider>
|
||||
<UserSettingsProvider>
|
||||
<WebConfigProvider>
|
||||
<RootAppRouter />
|
||||
</WebConfigProvider>
|
||||
</UserSettingsProvider>
|
||||
</ApiProvider>
|
||||
{useReactQueryDevtools && (
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
export default RootApp;
|
||||
@@ -1,69 +0,0 @@
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import React from 'react';
|
||||
import {
|
||||
RouterProvider,
|
||||
createHashRouter,
|
||||
Outlet,
|
||||
useLocation
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { DASHBOARD_APP_PATHS, DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
||||
import { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/routes/routes';
|
||||
import { STABLE_APP_ROUTES } from 'apps/stable/routes/routes';
|
||||
import { WIZARD_APP_ROUTES } from 'apps/wizard/routes/routes';
|
||||
import AppHeader from 'components/AppHeader';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
import { SETTING_KEY as LAYOUT_SETTING_KEY } from 'components/layoutManager';
|
||||
import BangRedirect from 'components/router/BangRedirect';
|
||||
import { createRouterHistory } from 'components/router/routerHistory';
|
||||
import { LayoutMode } from 'constants/layoutMode';
|
||||
import browser from 'scripts/browser';
|
||||
import appTheme from 'themes';
|
||||
import { ThemeStorageManager } from 'themes/themeStorageManager';
|
||||
|
||||
const layoutMode = browser.tv ? LayoutMode.Tv : localStorage.getItem(LAYOUT_SETTING_KEY);
|
||||
const isExperimentalLayout = !layoutMode || layoutMode === LayoutMode.Experimental;
|
||||
|
||||
const router = createHashRouter([
|
||||
{
|
||||
element: <RootAppLayout />,
|
||||
children: [
|
||||
...(isExperimentalLayout ? EXPERIMENTAL_APP_ROUTES : STABLE_APP_ROUTES),
|
||||
...DASHBOARD_APP_ROUTES,
|
||||
...WIZARD_APP_ROUTES,
|
||||
{
|
||||
path: '!/*',
|
||||
Component: BangRedirect
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
export const history = createRouterHistory(router);
|
||||
|
||||
export default function RootAppRouter() {
|
||||
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() {
|
||||
const location = useLocation();
|
||||
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
|
||||
.some(path => location.pathname.startsWith(`/${path}`));
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
theme={appTheme}
|
||||
defaultMode='dark'
|
||||
storageManager={ThemeStorageManager}
|
||||
>
|
||||
<Backdrop />
|
||||
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
|
||||
|
||||
<Outlet />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
59
src/addplugin.html
Normal file
59
src/addplugin.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<div id="addPluginPage" data-role="page" class="page type-interior pluginConfigurationPage" data-backbutton="true">
|
||||
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
|
||||
<div class="readOnlyContent">
|
||||
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h1 class="sectionTitle pluginName"></h1>
|
||||
<a is="emby-linkbutton" class="raised button-alt headerHelpButton" target="_blank" href="https://docs.jellyfin.org/general/server/plugins/index.html">${Help}</a>
|
||||
</div>
|
||||
|
||||
<p id="tagline" style="font-style: italic;"></p>
|
||||
<p id="pPreviewImage"></p>
|
||||
|
||||
<p id="overview"></p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="verticalSection">
|
||||
<h2 class="sectionTitle">${HeaderInstall}</h2>
|
||||
|
||||
<form class="addPluginForm">
|
||||
<p id="pCurrentVersion"></p>
|
||||
<div id="pSelectVersion" class="hide selectContainer">
|
||||
<select id="selectVersion" name="selectVersion" is="emby-select" label="${LabelSelectVersionToInstall}"></select>
|
||||
</div>
|
||||
|
||||
<p id="btnInstallDiv" class="hide">
|
||||
<button is="emby-button" type="submit" id="btnInstall" class="raised button-submit block">
|
||||
<span>${Install}</span>
|
||||
</button>
|
||||
<div class="fieldDescription">${ServerRestartNeededAfterPluginInstall}</div>
|
||||
</p>
|
||||
<p id="nonServerMsg"></p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="readOnlyContent">
|
||||
<div is="emby-collapse" title="${HeaderDeveloperInfo}">
|
||||
<div class="collapseContent">
|
||||
<p id="developer"></p>
|
||||
<p id="pViewWebsite" style="display: none;">
|
||||
<a is="emby-linkbutton" class="button-link" href="#" target="_blank">${ButtonViewWebsite}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div is="emby-collapse" title="${HeaderRevisionHistory}">
|
||||
<div class="collapseContent">
|
||||
<div id="revisionHistory"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,14 +1,14 @@
|
||||
<div data-role="page" class="page standalonePage">
|
||||
<div class="padded-left padded-right padded-bottom-page">
|
||||
<form class="addServerForm" style="margin: 0 auto;" novalidate>
|
||||
<form class="addServerForm" style="margin: 0 auto;">
|
||||
<h1>${HeaderConnectToServer}</h1>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="url" id="txtServerHost" required="required" label="${LabelServerHost}"/>
|
||||
<input is="emby-input" type="text" id="txtServerHost" required="required" label="${LabelServerHost}" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off" />
|
||||
<div class="fieldDescription">${LabelServerHostHelp}</div>
|
||||
</div>
|
||||
<br />
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Connect}</span>
|
||||
<span>${ButtonConnect}</span>
|
||||
</button>
|
||||
<button is="emby-button" type="button" class="raised button-cancel block btnCancel">
|
||||
<span>${ButtonCancel}</span>
|
||||
362
src/apiclient.d.ts
vendored
362
src/apiclient.d.ts
vendored
@@ -1,362 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare module 'jellyfin-apiclient' {
|
||||
import type {
|
||||
AllThemeMediaResult,
|
||||
AuthenticationResult,
|
||||
BaseItemDto,
|
||||
BaseItemDtoQueryResult,
|
||||
BufferRequestDto,
|
||||
ClientCapabilities,
|
||||
CountryInfo,
|
||||
CultureDto,
|
||||
DeviceOptions,
|
||||
DisplayPreferencesDto,
|
||||
EndPointInfo,
|
||||
FileSystemEntryInfo,
|
||||
GeneralCommand,
|
||||
GroupInfoDto,
|
||||
GuideInfo,
|
||||
IgnoreWaitRequestDto,
|
||||
ImageInfo,
|
||||
ImageProviderInfo,
|
||||
ImageType,
|
||||
ItemCounts,
|
||||
LiveTvInfo,
|
||||
MovePlaylistItemRequestDto,
|
||||
NewGroupRequestDto,
|
||||
NextItemRequestDto,
|
||||
NotificationResultDto,
|
||||
NotificationsSummaryDto,
|
||||
ParentalRating,
|
||||
PingRequestDto,
|
||||
PlaybackInfoResponse,
|
||||
PlaybackProgressInfo,
|
||||
PlaybackStartInfo,
|
||||
PlaybackStopInfo,
|
||||
PlayCommand,
|
||||
PlaystateCommand,
|
||||
PluginInfo,
|
||||
PluginSecurityInfo,
|
||||
PreviousItemRequestDto,
|
||||
QueryFiltersLegacy,
|
||||
QueueRequestDto,
|
||||
QuickConnectResult,
|
||||
QuickConnectState,
|
||||
ReadyRequestDto,
|
||||
RecommendationDto,
|
||||
RemoteImageResult,
|
||||
RemoveFromPlaylistRequestDto,
|
||||
SearchHintResult,
|
||||
SeekRequestDto,
|
||||
SeriesTimerInfoDto,
|
||||
SeriesTimerInfoDtoQueryResult,
|
||||
ServerConfiguration,
|
||||
SessionInfo,
|
||||
SetPlaylistItemRequestDto,
|
||||
SetRepeatModeRequestDto,
|
||||
SetShuffleModeRequestDto,
|
||||
SystemInfo,
|
||||
TaskInfo,
|
||||
TaskTriggerInfo,
|
||||
TimerInfoDto,
|
||||
TimerInfoDtoQueryResult,
|
||||
UserConfiguration,
|
||||
UserDto,
|
||||
UserItemDataDto,
|
||||
UserPolicy,
|
||||
UtcTimeResponse,
|
||||
VirtualFolderInfo
|
||||
} from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { ConnectionState } from 'lib/jellyfin-apiclient';
|
||||
|
||||
class ApiClient {
|
||||
constructor(serverAddress: string, appName: string, appVersion: string, deviceName: string, deviceId: string);
|
||||
|
||||
accessToken(): string;
|
||||
addMediaPath(virtualFolderName: string, mediaPath: string, networkSharePath: string, refreshLibrary?: boolean): Promise<void>;
|
||||
addVirtualFolder(name: string, type?: string, refreshLibrary?: boolean, libraryOptions?: any): Promise<void>;
|
||||
ajax(request: any): Promise<any>;
|
||||
appName(): string;
|
||||
appVersion(): string;
|
||||
authenticateUserByName(name: string, password: string): Promise<AuthenticationResult>;
|
||||
cancelLiveTvSeriesTimer(id: string): Promise<void>;
|
||||
cancelLiveTvTimer(id: string): Promise<void>;
|
||||
cancelSyncItems(itemIds: string[], targetId?: string): Promise<void>;
|
||||
clearAuthenticationInfo(): void;
|
||||
clearUserItemRating(userId: string, itemId: string): Promise<UserItemDataDto>;
|
||||
closeWebSocket(): void;
|
||||
createLiveTvSeriesTimer(item: string): Promise<void>;
|
||||
createLiveTvTimer(item: string): Promise<void>;
|
||||
createPackageReview(review: any): Promise<any>;
|
||||
createSyncPlayGroup(options?: NewGroupRequestDto): Promise<void>;
|
||||
createUser(user: UserDto): Promise<UserDto>;
|
||||
deleteDevice(deviceId: string): Promise<void>;
|
||||
deleteItemImage(itemId: string, imageType: ImageType, imageIndex?: number): Promise<void>;
|
||||
deleteItem(itemId: string): Promise<void>;
|
||||
deleteLiveTvRecording(id: string): Promise<void>;
|
||||
deleteUserImage(userId: string, imageType: ImageType, imageIndex?: number): Promise<void>;
|
||||
deleteUser(userId: string): Promise<void>;
|
||||
detectBitrate(force: boolean): Promise<number>;
|
||||
deviceId(): string;
|
||||
deviceName(): string;
|
||||
disablePlugin(id: string, version: string): Promise<void>;
|
||||
downloadRemoteImage(options: any): Promise<void>;
|
||||
enablePlugin(id: string, version: string): Promise<void>;
|
||||
encodeName(name: string): string;
|
||||
ensureWebSocket(): void;
|
||||
fetch(request: any, includeAuthorization?: boolean): Promise<any>;
|
||||
fetchWithFailover(request: any, enableReconnection?: boolean): Promise<any>;
|
||||
getAdditionalVideoParts(userId?: string, itemId: string): Promise<BaseItemDtoQueryResult>;
|
||||
getAlbumArtists(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getAncestorItems(itemId: string, userId?: string): Promise<BaseItemDto[]>;
|
||||
getArtist(name: string, userId?: string): Promise<BaseItemDto>;
|
||||
getArtists(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getAvailablePlugins(options?: any): Promise<PluginInfo[]>;
|
||||
getAvailableRemoteImages(options: any): Promise<RemoteImageResult>;
|
||||
getContentUploadHistory(): Promise<any>;
|
||||
getCountries(): Promise<CountryInfo[]>;
|
||||
getCriticReviews(itemId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getCultures(): Promise<CultureDto[]>;
|
||||
getCurrentUser(cache?: boolean): Promise<UserDto>;
|
||||
getCurrentUserId(): string;
|
||||
getDateParamValue(date: Date): string;
|
||||
getDefaultImageQuality(imageType: ImageType): number;
|
||||
getDevicesOptions(): Promise<DeviceOptions>;
|
||||
getDirectoryContents(path: string, options?: any): Promise<FileSystemEntryInfo[]>;
|
||||
getDisplayPreferences(id: string, userId: string, app: string): Promise<DisplayPreferencesDto>;
|
||||
getDownloadSpeed(byteSize: number): Promise<number>;
|
||||
getDrives(): Promise<FileSystemEntryInfo[]>;
|
||||
getEndpointInfo(): Promise<EndPointInfo>;
|
||||
getEpisodes(itemId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getFilters(options?: any): Promise<QueryFiltersLegacy>;
|
||||
getGenre(name: string, userId?: string): Promise<BaseItemDto>;
|
||||
getGenres(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getImageUrl(itemId: string, options?: any): string;
|
||||
getInstalledPlugins(): Promise<PluginInfo[]>;
|
||||
getInstantMixFromItem(itemId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getIntros(itemId: string): Promise<BaseItemDtoQueryResult>;
|
||||
getItemCounts(userId?: string): Promise<ItemCounts>;
|
||||
/** @deprecated This function returns a URL with a legacy auth parameter.*/
|
||||
getItemDownloadUrl(itemId: string): string;
|
||||
getItemImageInfos(itemId: string): Promise<ImageInfo[]>;
|
||||
getItems(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getItem(userId: string, itemId: string): Promise<BaseItemDto>;
|
||||
getJSON(url: string, includeAuthorization?: boolean): Promise<any>;
|
||||
getLatestItems(options?: any): Promise<BaseItemDto[]>;
|
||||
getLiveStreamMediaInfo(liveStreamId: string): Promise<any>;
|
||||
getLiveTvChannel(id: string, userId?: string): Promise<BaseItemDto>;
|
||||
getLiveTvChannels(options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getLiveTvGuideInfo(userId: string): Promise<GuideInfo>;
|
||||
getLiveTvInfo(userId: string): Promise<LiveTvInfo>;
|
||||
getLiveTvProgram(id: string, userId?: string): Promise<BaseItemDto>;
|
||||
getLiveTvPrograms(options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getLiveTvRecommendedPrograms(options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getLiveTvRecordingGroup(id: string): Promise<BaseItemDto>;
|
||||
getLiveTvRecordingGroups(options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getLiveTvRecording(id: string, userId?: string): Promise<BaseItemDto>;
|
||||
getLiveTvRecordingSeries(options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getLiveTvRecordings(options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getLiveTvSeriesTimer(id: string): Promise<SeriesTimerInfoDto>;
|
||||
getLiveTvSeriesTimers(options?: any): Promise<SeriesTimerInfoDtoQueryResult>;
|
||||
getLiveTvTimer(id: string): Promise<TimerInfoDto>;
|
||||
getLiveTvTimers(options?: any): Promise<TimerInfoDtoQueryResult>;
|
||||
getLocalTrailers(userId: string, itemId: string): Promise<BaseItemDto[]>;
|
||||
getMovieRecommendations(options?: any): Promise<RecommendationDto[]>;
|
||||
getMusicGenre(name: string, userId?: string): Promise<BaseItemDto>;
|
||||
getMusicGenres(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getNamedConfiguration(name: string): Promise<any>;
|
||||
getNetworkDevices(): Promise<any>;
|
||||
getNetworkShares(path: string): Promise<FileSystemEntryInfo[]>;
|
||||
getNewLiveTvTimerDefaults(options?: any): Promise<SeriesTimerInfoDto>;
|
||||
getNextUpEpisodes(options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getNotificationSummary(userId: string): Promise<NotificationsSummaryDto>;
|
||||
getNotifications(userId: string, options?: any): Promise<NotificationResultDto>;
|
||||
getPackageInfo(name: string, guid: string): Promise<PackageInfo>;
|
||||
getPackageReviews(packageId: string, minRating?: string, maxRating?: string, limit?: string): Promise<any>;
|
||||
getParentalRatings(): Promise<ParentalRating[]>;
|
||||
getParentPath(path: string): Promise<string>;
|
||||
getPeople(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getPerson(name: string, userId?: string): Promise<BaseItemDto>;
|
||||
getPhysicalPaths(): Promise<string[]>;
|
||||
getPlaybackInfo(itemId: string, options: any, deviceProfile: any): Promise<PlaybackInfoResponse>;
|
||||
getPluginConfiguration(id: string): Promise<any>;
|
||||
getPublicSystemInfo(): Promise<PublicSystemInfo>;
|
||||
getPublicUsers(): Promise<UserDto[]>;
|
||||
getQuickConnect(verb: string): Promise<void | boolean | number | QuickConnectResult | QuickConnectState>;
|
||||
getReadySyncItems(deviceId: string): Promise<any>;
|
||||
getRecordingFolders(userId: string): Promise<BaseItemDtoQueryResult>;
|
||||
getRegistrationInfo(feature: string): Promise<any>;
|
||||
getRemoteImageProviders(options: any): Promise<ImageProviderInfo[]>;
|
||||
getResumableItems(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getRootFolder(userId: string): Promise<BaseItemDto>;
|
||||
getSavedEndpointInfo(): EndPointInfo;
|
||||
getScaledImageUrl(itemId: string, options?: any): string;
|
||||
getScheduledTask(id: string): Promise<TaskInfo>;
|
||||
getScheduledTasks(options?: any): Promise<TaskInfo[]>;
|
||||
getSearchHints(options?: any): Promise<SearchHintResult>;
|
||||
getSeasons(itemId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getServerConfiguration(): Promise<ServerConfiguration>;
|
||||
getServerTime(): Promise<UtcTimeResponse>;
|
||||
getSessions(options?: any): Promise<SessionInfo[]>;
|
||||
getSimilarItems(itemId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getSpecialFeatures(userId: string, itemId: string): Promise<BaseItemDto[]>;
|
||||
getStudio(name: string, userId?: string): Promise<BaseItemDto>;
|
||||
getStudios(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getSyncPlayGroups(): Promise<GroupInfoDto[]>;
|
||||
getSyncStatus(itemId: string): Promise<any>;
|
||||
getSystemInfo(): Promise<SystemInfo>;
|
||||
getThemeMedia(userId?: string, itemId: string, inherit?: boolean): Promise<AllThemeMediaResult>;
|
||||
getThumbImageUrl(item: BaseItemDto, options?: any): string;
|
||||
getUpcomingEpisodes(options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getUrl(name: string, params?: any, serverAddress?: string): string;
|
||||
get(url: string): Promise<any>;
|
||||
getUserImageUrl(userId: string, options?: any): string;
|
||||
getUsers(options?: any): Promise<UserDto[]>;
|
||||
getUser(userId: string): Promise<UserDto>;
|
||||
getUserViews(options?: any, userId: string): Promise<BaseItemDtoQueryResult>;
|
||||
getVirtualFolders(): Promise<VirtualFolderInfo[]>;
|
||||
handleMessageReceived(msg: any): void;
|
||||
installPlugin(name: string, guid: string, version?: string): Promise<void>;
|
||||
isLoggedIn(): boolean;
|
||||
isMessageChannelOpen(): boolean;
|
||||
isMinServerVersion(version: string): boolean;
|
||||
isWebSocketOpen(): boolean;
|
||||
isWebSocketOpenOrConnecting(): boolean;
|
||||
isWebSocketSupported(): boolean;
|
||||
joinSyncPlayGroup(options?: any): Promise<void>;
|
||||
leaveSyncPlayGroup(): Promise<void>;
|
||||
logout(): Promise<void>;
|
||||
markNotificationsRead(userId: string, idList: string[], isRead: boolean): Promise<void>;
|
||||
markPlayed(userId: string, itemId: string, date: Date): Promise<UserItemDataDto>;
|
||||
markUnplayed(userId: string, itemId: string, date: Date): Promise<UserItemDataDto>;
|
||||
openWebSocket(): void;
|
||||
quickConnect(secret: string): Promise<AuthenticationResult>;
|
||||
refreshItem(itemId: string, options?: any): Promise<void>;
|
||||
removeMediaPath(virtualFolderName: string, mediaPath: string, refreshLibrary?: boolean): Promise<void>;
|
||||
removeVirtualFolder(name: string, refreshLibrary?: boolean): Promise<void>;
|
||||
renameVirtualFolder(name: string, newName: string, refreshLibrary?: boolean): Promise<void>;
|
||||
reportCapabilities(capabilities: ClientCapabilities): Promise<void>;
|
||||
reportOfflineActions(actions: any): Promise<any>;
|
||||
reportPlaybackProgress(options: PlaybackProgressInfo): Promise<void>;
|
||||
reportPlaybackStart(options: PlaybackStartInfo): Promise<void>;
|
||||
reportPlaybackStopped(options: PlaybackStopInfo): Promise<void>;
|
||||
reportSyncJobItemTransferred(syncJobItemId: string): Promise<any>;
|
||||
requestSyncPlayBuffering(options?: BufferRequestDto): Promise<void>;
|
||||
requestSyncPlayMovePlaylistItem(options?: MovePlaylistItemRequestDto): Promise<void>;
|
||||
requestSyncPlayNextItem(options?: NextItemRequestDto): Promise<void>;
|
||||
requestSyncPlayPause(): Promise<void>;
|
||||
requestSyncPlayPreviousItem(options?: PreviousItemRequestDto): Promise<void>;
|
||||
requestSyncPlayQueue(options?: QueueRequestDto): Promise<void>;
|
||||
requestSyncPlayReady(options?: ReadyRequestDto): Promise<void>;
|
||||
requestSyncPlayRemoveFromPlaylist(options?: RemoveFromPlaylistRequestDto): Promise<void>;
|
||||
requestSyncPlaySeek(options?: SeekRequestDto): Promise<void>;
|
||||
requestSyncPlaySetIgnoreWait(options?: IgnoreWaitRequestDto): Promise<void>;
|
||||
requestSyncPlaySetNewQueue(options?: NewGroupRequestDto): Promise<void>;
|
||||
requestSyncPlaySetPlaylistItem(options?: SetPlaylistItemRequestDto): Promise<void>;
|
||||
requestSyncPlaySetRepeatMode(options?: SetRepeatModeRequestDto): Promise<void>;
|
||||
requestSyncPlaySetShuffleMode(options?: SetShuffleModeRequestDto): Promise<void>;
|
||||
requestSyncPlayUnpause(): Promise<void>;
|
||||
resetEasyPassword(userId: string): Promise<void>;
|
||||
resetLiveTvTuner(id: string): Promise<void>;
|
||||
resetUserPassword(userId: string): Promise<void>;
|
||||
restartServer(): Promise<void>;
|
||||
sendCommand(sessionId: string, command: any): Promise<void>;
|
||||
sendMessageCommand(sessionId: string, options: GeneralCommand): Promise<void>;
|
||||
sendMessage(name: string, data: any): void;
|
||||
sendPlayCommand(sessionId: string, options: PlayCommand): Promise<void>;
|
||||
sendPlayStateCommand(sessionId: string, command: PlaystateCommand, options?: any): Promise<void>;
|
||||
sendSyncPlayPing(options?: PingRequestDto): Promise<void>;
|
||||
sendWebSocketMessage(name: string, data: any): void;
|
||||
serverAddress(val?: string): string;
|
||||
serverId(): string;
|
||||
serverVersion(): string;
|
||||
setAuthenticationInfo(accessKey?: string, userId?: string): void;
|
||||
setRequestHeaders(headers: any): void;
|
||||
setSystemInfo(info: SystemInfo): void;
|
||||
shutdownServer(): Promise<void>;
|
||||
startScheduledTask(id: string): Promise<void>;
|
||||
stopActiveEncodings(playSessionId: string): Promise<void>;
|
||||
stopScheduledTask(id: string): Promise<void>;
|
||||
syncData(data: any): Promise<any>;
|
||||
uninstallPluginByVersion(id: string, version: string): Promise<void>;
|
||||
uninstallPlugin(id: string): Promise<void>;
|
||||
updateDisplayPreferences(id: string, obj: DisplayPreferencesDto, userId: string, app: string): Promise<void>;
|
||||
updateEasyPassword(userId: string, newPassword: string): Promise<void>;
|
||||
updateFavoriteStatus(userId: string, itemId: string, isFavorite: boolean): Promise<UserItemDataDto>;
|
||||
updateItemImageIndex(itemId: string, imageType: ImageType, imageIndex: number, newIndex: number): Promise<any>;
|
||||
updateItem(item: BaseItemDto): Promise<void>;
|
||||
updateLiveTvSeriesTimer(item: SeriesTimerInfoDto): Promise<void>;
|
||||
updateLiveTvTimer(item: TimerInfoDto): Promise<void>;
|
||||
updateMediaPath(virtualFolderName: string, pathInfo: any): Promise<void>;
|
||||
updateNamedConfiguration(name: string, configuration: any): Promise<void>;
|
||||
updatePluginConfiguration(id: string, configuration: any): Promise<void>;
|
||||
updatePluginSecurityInfo(info: PluginSecurityInfo): Promise<void>;
|
||||
updateScheduledTaskTriggers(id: string, triggers: TaskTriggerInfo[]): Promise<void>;
|
||||
updateServerConfiguration(configuration: ServerConfiguration): Promise<void>;
|
||||
updateServerInfo(server: any, serverUrl: string): void;
|
||||
updateUserConfiguration(userId: string, configuration: UserConfiguration): Promise<void>;
|
||||
updateUserItemRating(userId: string, itemId: string, likes: boolean): Promise<UserItemDataDto>;
|
||||
updateUserPassword(userId: string, currentPassword: string, newPassword: string): Promise<void>;
|
||||
updateUserPolicy(userId: string, policy: UserPolicy): Promise<void>;
|
||||
updateUser(user: UserDto): Promise<void>;
|
||||
updateVirtualFolderOptions(id: string, libraryOptions?: any): Promise<void>;
|
||||
uploadItemImage(itemId: string, imageType: ImageType, file: File): Promise<void>;
|
||||
uploadItemSubtitle(itemId: string, language: string, isForced: boolean, file: File): Promise<void>;
|
||||
uploadUserImage(userId: string, imageType: ImageType, file: File): Promise<void>;
|
||||
}
|
||||
|
||||
class AppStore {
|
||||
constructor();
|
||||
|
||||
getItem(name: string): string | null;
|
||||
removeItem(name: string): void;
|
||||
setItem(name: string, value: string): void;
|
||||
}
|
||||
|
||||
interface ConnectResponse {
|
||||
ApiClient: ApiClient
|
||||
Servers: any[]
|
||||
State: ConnectionState
|
||||
}
|
||||
|
||||
class ConnectionManager {
|
||||
constructor(credentialProvider: Credentials, appName: string, appVersion: string, deviceName: string, deviceId: string, capabilities: ClientCapabilities);
|
||||
|
||||
addApiClient(apiClient: ApiClient): void;
|
||||
clearData(): void;
|
||||
connect(options?: any): Promise<ConnectResponse>;
|
||||
connectToAddress(address: string, options?: any): Promise<any>;
|
||||
connectToServer(server: any, options?: any): Promise<any>;
|
||||
connectToServers(servers: any[], options?: any): Promise<any>;
|
||||
deleteServer(serverId: string): Promise<void>;
|
||||
getApiClient(item: BaseItemDto | string): ApiClient;
|
||||
getApiClients(): ApiClient[];
|
||||
getAvailableServers(): any[];
|
||||
getOrCreateApiClient(serverId: string): ApiClient;
|
||||
getSavedServers(): any[];
|
||||
handleMessageReceived(msg: any): void;
|
||||
logout(): Promise<void>;
|
||||
minServerVersion(val?: string): string;
|
||||
updateSavedServerId(server: any): Promise<void>;
|
||||
user(apiClient: ApiClient): Promise<any>;
|
||||
}
|
||||
|
||||
class Credentials {
|
||||
constructor(key?: string);
|
||||
|
||||
addOrUpdateServer(list: any[], server: any): any;
|
||||
clear(): void;
|
||||
credentials(data?: any): any;
|
||||
}
|
||||
|
||||
interface Event {
|
||||
type: string;
|
||||
}
|
||||
|
||||
const Events: {
|
||||
off(obj: any, eventName: string, fn: (e: Event, ...args: any[]) => void): void;
|
||||
on(obj: any, eventName: string, fn: (e: Event, ...args: any[]) => void): void;
|
||||
trigger(obj: any, eventName: string, ...args: any[]): void;
|
||||
};
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
27
src/apikeys.html
Normal file
27
src/apikeys.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<div id="apiKeysPage" data-role="page" class="page type-interior advancedConfigurationPage fullWidthContent">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="detailSectionHeader">
|
||||
<h2 style="margin:.6em 0;vertical-align:middle;display:inline-block;">
|
||||
${HeaderApiKeys}
|
||||
</h2>
|
||||
<button is="emby-button" type="button" class="fab btnNewKey submit" style="margin-left:1em;" title="${ButtonAdd}">
|
||||
<i class="md-icon">add</i>
|
||||
</button>
|
||||
</div>
|
||||
<p>${HeaderApiKeysHelp}</p>
|
||||
<br />
|
||||
<table class="tblApiKeys detailTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="detailTableHeaderCell"></th>
|
||||
<th class="detailTableHeaderCell">${HeaderApiKey}</th>
|
||||
<th class="detailTableHeaderCell">${HeaderApp}</th>
|
||||
<th class="detailTableHeaderCell">${HeaderDateIssued}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="resultBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,113 +0,0 @@
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Box from '@mui/material/Box';
|
||||
import { type Theme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
||||
import React, { FC, StrictMode, useCallback, useEffect, useState } from 'react';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
|
||||
import AppBody from 'components/AppBody';
|
||||
import AppToolbar from 'components/toolbar/AppToolbar';
|
||||
import ServerButton from 'components/toolbar/ServerButton';
|
||||
import ElevationScroll from 'components/ElevationScroll';
|
||||
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import ThemeCss from 'components/ThemeCss';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { useLocale } from 'hooks/useLocale';
|
||||
|
||||
import AppTabs from './components/AppTabs';
|
||||
import AppDrawer from './components/drawer/AppDrawer';
|
||||
import HelpButton from './components/toolbar/HelpButton';
|
||||
import { DASHBOARD_APP_PATHS } from './routes/routes';
|
||||
|
||||
import './AppOverrides.scss';
|
||||
|
||||
export const Component: FC = () => {
|
||||
const [ isDrawerActive, setIsDrawerActive ] = useState(false);
|
||||
const location = useLocation();
|
||||
const { user } = useApi();
|
||||
const { dateFnsLocale } = useLocale();
|
||||
|
||||
const isMediumScreen = useMediaQuery((t: Theme) => t.breakpoints.up('md'));
|
||||
const isMetadataManager = location.pathname.startsWith(`/${DASHBOARD_APP_PATHS.MetadataManager}`);
|
||||
const isDrawerAvailable = Boolean(user) && !isMetadataManager;
|
||||
const isDrawerOpen = isDrawerActive && isDrawerAvailable;
|
||||
|
||||
const onToggleDrawer = useCallback(() => {
|
||||
setIsDrawerActive(!isDrawerActive);
|
||||
}, [ isDrawerActive, setIsDrawerActive ]);
|
||||
|
||||
// Update body class
|
||||
useEffect(() => {
|
||||
document.body.classList.add('dashboardDocument');
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove('dashboardDocument');
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={dateFnsLocale}>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<StrictMode>
|
||||
<ElevationScroll elevate={false}>
|
||||
<AppBar
|
||||
position='fixed'
|
||||
sx={{
|
||||
width: {
|
||||
xs: '100%',
|
||||
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
|
||||
},
|
||||
ml: {
|
||||
xs: 0,
|
||||
md: isDrawerAvailable ? DRAWER_WIDTH : 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AppToolbar
|
||||
isBackButtonAvailable={appRouter.canGoBack()}
|
||||
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
|
||||
isDrawerOpen={isDrawerOpen}
|
||||
onDrawerButtonClick={onToggleDrawer}
|
||||
buttons={
|
||||
<HelpButton />
|
||||
}
|
||||
>
|
||||
{isMetadataManager && (
|
||||
<ServerButton />
|
||||
)}
|
||||
|
||||
<AppTabs isDrawerOpen={isDrawerOpen} />
|
||||
</AppToolbar>
|
||||
</AppBar>
|
||||
</ElevationScroll>
|
||||
|
||||
{
|
||||
isDrawerAvailable && (
|
||||
<AppDrawer
|
||||
open={isDrawerOpen}
|
||||
onClose={onToggleDrawer}
|
||||
onOpen={onToggleDrawer}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</StrictMode>
|
||||
|
||||
<Box
|
||||
component='main'
|
||||
sx={{
|
||||
width: '100%',
|
||||
flexGrow: 1
|
||||
}}
|
||||
>
|
||||
<AppBody>
|
||||
<Outlet />
|
||||
</AppBody>
|
||||
</Box>
|
||||
</Box>
|
||||
<ThemeCss dashboard />
|
||||
</LocalizationProvider>
|
||||
);
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
// Default MUI breakpoints
|
||||
// https://mui.com/material-ui/customization/breakpoints/#default-breakpoints
|
||||
$mui-bp-sm: 600px;
|
||||
$mui-bp-md: 900px;
|
||||
$mui-bp-lg: 1200px;
|
||||
$mui-bp-xl: 1536px;
|
||||
|
||||
$drawer-width: 240px;
|
||||
|
||||
// Fix dashboard pages layout to work with drawer
|
||||
.dashboardDocument {
|
||||
.mainAnimatedPage:not(.metadataEditorPage) {
|
||||
@media all and (min-width: $mui-bp-md) {
|
||||
left: $drawer-width;
|
||||
}
|
||||
}
|
||||
|
||||
.skinBody {
|
||||
position: unset !important;
|
||||
}
|
||||
|
||||
// Fix the padding of dashboard pages
|
||||
.content-primary {
|
||||
padding-top: 3.25rem;
|
||||
}
|
||||
// Tabbed pages
|
||||
.withTabs .content-primary {
|
||||
padding-top: 6.5rem;
|
||||
|
||||
@media all and (min-width: $mui-bp-lg) {
|
||||
padding-top: 3.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.metadataEditorPage {
|
||||
padding-top: 3.25rem !important;
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { EventType } from 'constants/eventType';
|
||||
import Events, { type Event } from 'utils/events';
|
||||
|
||||
interface AppTabsParams {
|
||||
isDrawerOpen: boolean
|
||||
}
|
||||
|
||||
interface TabDefinition {
|
||||
href: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const handleResize = debounce(() => window.dispatchEvent(new Event('resize')), 100);
|
||||
|
||||
const AppTabs: FC<AppTabsParams> = ({
|
||||
isDrawerOpen
|
||||
}) => {
|
||||
const documentRef = useRef<Document>(document);
|
||||
const [ activeIndex, setActiveIndex ] = useState(0);
|
||||
const [ tabs, setTabs ] = useState<TabDefinition[]>();
|
||||
|
||||
const isBigScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
|
||||
|
||||
const onTabsUpdate = useCallback((
|
||||
_e: Event,
|
||||
_newView?: string,
|
||||
newIndex: number | undefined = 0,
|
||||
newTabs?: TabDefinition[]
|
||||
) => {
|
||||
setActiveIndex(newIndex);
|
||||
|
||||
if (!isEqual(tabs, newTabs)) {
|
||||
setTabs(newTabs);
|
||||
}
|
||||
}, [ tabs ]);
|
||||
|
||||
useEffect(() => {
|
||||
const doc = documentRef.current;
|
||||
|
||||
if (doc) Events.on(doc, EventType.SET_TABS, onTabsUpdate);
|
||||
|
||||
return () => {
|
||||
if (doc) Events.off(doc, EventType.SET_TABS, onTabsUpdate);
|
||||
};
|
||||
}, [ onTabsUpdate ]);
|
||||
|
||||
// HACK: Force resizing to workaround upstream bug with tab resizing
|
||||
// https://github.com/mui/material-ui/issues/24011
|
||||
useEffect(() => {
|
||||
handleResize();
|
||||
}, [ isDrawerOpen ]);
|
||||
|
||||
if (!tabs?.length) return null;
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
value={activeIndex}
|
||||
sx={{
|
||||
width: '100%',
|
||||
flexShrink: {
|
||||
xs: 0,
|
||||
lg: 'unset'
|
||||
},
|
||||
order: {
|
||||
xs: 100,
|
||||
lg: 'unset'
|
||||
}
|
||||
}}
|
||||
variant={isBigScreen ? 'standard' : 'scrollable'}
|
||||
centered={isBigScreen}
|
||||
>
|
||||
{
|
||||
tabs.map(({ href, name }, index) => (
|
||||
<Tab
|
||||
key={`tab-${name}`}
|
||||
label={name}
|
||||
data-tab-index={`${index}`}
|
||||
component={Link}
|
||||
to={href}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppTabs;
|
||||
@@ -1,121 +0,0 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardMedia from '@mui/material/CardMedia';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
|
||||
import CardActionArea from '@mui/material/CardActionArea';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { Link, To } from 'react-router-dom';
|
||||
|
||||
interface BaseCardProps {
|
||||
title?: string;
|
||||
text?: string;
|
||||
image?: string | null;
|
||||
icon?: React.ReactNode;
|
||||
to?: To;
|
||||
onClick?: () => void;
|
||||
action?: boolean;
|
||||
actionRef?: React.MutableRefObject<HTMLButtonElement | null>;
|
||||
onActionClick?: () => void;
|
||||
height?: number;
|
||||
width?: number;
|
||||
};
|
||||
|
||||
const BaseCard = ({
|
||||
title,
|
||||
text,
|
||||
image,
|
||||
icon,
|
||||
to,
|
||||
onClick,
|
||||
action,
|
||||
actionRef,
|
||||
onActionClick,
|
||||
height,
|
||||
width
|
||||
}: BaseCardProps) => {
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: height || 240,
|
||||
width: width
|
||||
}}
|
||||
>
|
||||
<CardActionArea
|
||||
{...(to && {
|
||||
component: Link,
|
||||
to: to
|
||||
})}
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
alignItems: 'stretch'
|
||||
}}
|
||||
>
|
||||
{image ? (
|
||||
<CardMedia
|
||||
sx={{ flexGrow: 1 }}
|
||||
image={image}
|
||||
title={title}
|
||||
/>
|
||||
) : (
|
||||
<Box className={getDefaultBackgroundClass(title)} sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
{icon}
|
||||
</Box>
|
||||
)}
|
||||
</CardActionArea>
|
||||
<CardContent
|
||||
sx={{
|
||||
minHeight: 50,
|
||||
'&:last-child': {
|
||||
paddingBottom: 2,
|
||||
paddingRight: 1
|
||||
}
|
||||
}}>
|
||||
<Stack flexGrow={1} direction='row'>
|
||||
<Stack flexGrow={1}>
|
||||
<Typography gutterBottom sx={{
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis'
|
||||
}}>
|
||||
{title}
|
||||
</Typography>
|
||||
{text && (
|
||||
<Typography
|
||||
variant='body2'
|
||||
color='text.secondary'
|
||||
sx={{
|
||||
lineBreak: 'anywhere'
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
<Box>
|
||||
{action ? (
|
||||
<IconButton ref={actionRef} onClick={onActionClick}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</Box>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseCard;
|
||||
@@ -1,70 +0,0 @@
|
||||
import Search from '@mui/icons-material/Search';
|
||||
import InputBase, { type InputBaseProps } from '@mui/material/InputBase';
|
||||
import { alpha, styled } from '@mui/material/styles';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
const SearchContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: alpha(theme.palette.common.white, 0.15),
|
||||
'&:hover': {
|
||||
backgroundColor: alpha(theme.palette.common.white, 0.25)
|
||||
},
|
||||
width: '100%',
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: 'auto'
|
||||
}
|
||||
}));
|
||||
|
||||
const SearchIconWrapper = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(0, 2),
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}));
|
||||
|
||||
const StyledInputBase = styled(InputBase)(({ theme }) => ({
|
||||
color: 'inherit',
|
||||
flexGrow: 1,
|
||||
'& .MuiInputBase-input': {
|
||||
padding: theme.spacing(1, 1, 1, 0),
|
||||
// vertical padding + font size from searchIcon
|
||||
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
|
||||
transition: theme.transitions.create('width'),
|
||||
width: '100%',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
width: '20ch'
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
interface SearchInputProps extends InputBaseProps {
|
||||
label?: string
|
||||
}
|
||||
|
||||
const SearchInput: FC<SearchInputProps> = ({
|
||||
label,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<SearchContainer>
|
||||
<SearchIconWrapper>
|
||||
<Search />
|
||||
</SearchIconWrapper>
|
||||
<StyledInputBase
|
||||
placeholder={label}
|
||||
inputProps={{
|
||||
'aria-label': label,
|
||||
...props.inputProps
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</SearchContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchInput;
|
||||
@@ -1,30 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import Snackbar, { SnackbarProps } from '@mui/material/Snackbar';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
const Toast = (props: SnackbarProps) => {
|
||||
const onCloseClick = useCallback((e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
props.onClose?.(e, 'clickaway');
|
||||
}, [ props ]);
|
||||
|
||||
const action = (
|
||||
<IconButton
|
||||
size='small'
|
||||
color='inherit'
|
||||
onClick={onCloseClick}
|
||||
>
|
||||
<CloseIcon fontSize='small' />
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
autoHideDuration={3300}
|
||||
action={action}
|
||||
{ ...props }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toast;
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
|
||||
import type { SxProps, Theme } from '@mui/material';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import React, { type FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import UserAvatar from 'components/UserAvatar';
|
||||
|
||||
interface UserAvatarButtonProps {
|
||||
user?: UserDto
|
||||
sx?: SxProps<Theme>
|
||||
}
|
||||
|
||||
const UserAvatarButton: FC<UserAvatarButtonProps> = ({
|
||||
user,
|
||||
sx
|
||||
}) => (
|
||||
user?.Id ? (
|
||||
<IconButton
|
||||
size='large'
|
||||
color='inherit'
|
||||
sx={{
|
||||
padding: 0,
|
||||
...sx
|
||||
}}
|
||||
title={user.Name || undefined}
|
||||
component={Link}
|
||||
to={`/dashboard/users/profile?userId=${user.Id}`}
|
||||
>
|
||||
<UserAvatar user={user} />
|
||||
</IconButton>
|
||||
) : undefined
|
||||
);
|
||||
|
||||
export default UserAvatarButton;
|
||||
@@ -1,37 +0,0 @@
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import List from '@mui/material/List';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import DrawerHeaderLink from 'apps/experimental/components/drawers/DrawerHeaderLink';
|
||||
import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';
|
||||
|
||||
import ServerDrawerSection from './sections/ServerDrawerSection';
|
||||
import DevicesDrawerSection from './sections/DevicesDrawerSection';
|
||||
import LiveTvDrawerSection from './sections/LiveTvDrawerSection';
|
||||
import AdvancedDrawerSection from './sections/AdvancedDrawerSection';
|
||||
import PluginDrawerSection from './sections/PluginDrawerSection';
|
||||
|
||||
const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
||||
open = false,
|
||||
onClose,
|
||||
onOpen
|
||||
}) => (
|
||||
<ResponsiveDrawer
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onOpen={onOpen}
|
||||
>
|
||||
<List disablePadding>
|
||||
<ListItem disablePadding>
|
||||
<DrawerHeaderLink />
|
||||
</ListItem>
|
||||
</List>
|
||||
<ServerDrawerSection />
|
||||
<DevicesDrawerSection />
|
||||
<LiveTvDrawerSection />
|
||||
<PluginDrawerSection />
|
||||
<AdvancedDrawerSection />
|
||||
</ResponsiveDrawer>
|
||||
);
|
||||
|
||||
export default AppDrawer;
|
||||
@@ -1,70 +0,0 @@
|
||||
import Article from '@mui/icons-material/Article';
|
||||
import Backup from '@mui/icons-material/Backup';
|
||||
import Lan from '@mui/icons-material/Lan';
|
||||
import Schedule from '@mui/icons-material/Schedule';
|
||||
import VpnKey from '@mui/icons-material/VpnKey';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React from 'react';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
const AdvancedDrawerSection = () => {
|
||||
return (
|
||||
<List
|
||||
aria-labelledby='advanced-subheader'
|
||||
subheader={
|
||||
<ListSubheader component='div' id='advanced-subheader'>
|
||||
{globalize.translate('TabAdvanced')}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/networking'>
|
||||
<ListItemIcon>
|
||||
<Lan />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabNetworking')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/keys'>
|
||||
<ListItemIcon>
|
||||
<VpnKey />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderApiKeys')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/backups'>
|
||||
<ListItemIcon>
|
||||
<Backup />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderBackups')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/logs'>
|
||||
<ListItemIcon>
|
||||
<Article />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabLogs')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/tasks'>
|
||||
<ListItemIcon>
|
||||
<Schedule />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabScheduledTasks')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedDrawerSection;
|
||||
@@ -1,43 +0,0 @@
|
||||
import Analytics from '@mui/icons-material/Analytics';
|
||||
import Devices from '@mui/icons-material/Devices';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React from 'react';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
const DevicesDrawerSection = () => {
|
||||
return (
|
||||
<List
|
||||
aria-labelledby='devices-subheader'
|
||||
subheader={
|
||||
<ListSubheader component='div' id='devices-subheader'>
|
||||
{globalize.translate('HeaderDevices')}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/devices'>
|
||||
<ListItemIcon>
|
||||
<Devices />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderDevices')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/activity'>
|
||||
<ListItemIcon>
|
||||
<Analytics />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderActivity')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default DevicesDrawerSection;
|
||||
@@ -1,43 +0,0 @@
|
||||
import Dvr from '@mui/icons-material/Dvr';
|
||||
import LiveTv from '@mui/icons-material/LiveTv';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React from 'react';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
const LiveTvDrawerSection = () => {
|
||||
return (
|
||||
<List
|
||||
aria-labelledby='livetv-subheader'
|
||||
subheader={
|
||||
<ListSubheader component='div' id='livetv-subheader'>
|
||||
{globalize.translate('LiveTV')}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/livetv'>
|
||||
<ListItemIcon>
|
||||
<LiveTv />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('LiveTV')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/livetv/recordings'>
|
||||
<ListItemIcon>
|
||||
<Dvr />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderDVR')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default LiveTvDrawerSection;
|
||||
@@ -1,63 +0,0 @@
|
||||
import Extension from '@mui/icons-material/Extension';
|
||||
import Folder from '@mui/icons-material/Folder';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'lib/globalize';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import { useConfigurationPages } from 'apps/dashboard/features/plugins/api/useConfigurationPages';
|
||||
|
||||
const PluginDrawerSection = () => {
|
||||
const {
|
||||
data: pagesInfo,
|
||||
error
|
||||
} = useConfigurationPages({ enableInMainMenu: true });
|
||||
|
||||
useEffect(() => {
|
||||
if (error) console.error('[PluginDrawerSection] unable to fetch plugin config pages', error);
|
||||
}, [ error ]);
|
||||
|
||||
return (
|
||||
<List
|
||||
aria-labelledby='plugins-subheader'
|
||||
subheader={
|
||||
<ListSubheader component='div' id='plugins-subheader'>
|
||||
{globalize.translate('TabPlugins')}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItemLink
|
||||
to='/dashboard/plugins'
|
||||
includePaths={[
|
||||
'/configurationpage',
|
||||
'/dashboard/plugins/repositories'
|
||||
]}
|
||||
excludePaths={pagesInfo?.map(p => `/${Dashboard.getPluginUrl(p.Name)}`)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Extension />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabPlugins')} />
|
||||
</ListItemLink>
|
||||
|
||||
{pagesInfo?.map(pageInfo => (
|
||||
<ListItemLink
|
||||
key={pageInfo.PluginId}
|
||||
to={`/${Dashboard.getPluginUrl(pageInfo.Name)}`}
|
||||
>
|
||||
<ListItemIcon>
|
||||
{/* TODO: Support different icons? */}
|
||||
<Folder />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={pageInfo.DisplayName} />
|
||||
</ListItemLink>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginDrawerSection;
|
||||
@@ -1,147 +0,0 @@
|
||||
import Dashboard from '@mui/icons-material/Dashboard';
|
||||
import ExpandLess from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
import LibraryAdd from '@mui/icons-material/LibraryAdd';
|
||||
import Palette from '@mui/icons-material/Palette';
|
||||
import People from '@mui/icons-material/People';
|
||||
import PlayCircle from '@mui/icons-material/PlayCircle';
|
||||
import Settings from '@mui/icons-material/Settings';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemButton from '@mui/material/ListItemButton/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React, { type MouseEvent, useCallback, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
const LIBRARY_PATHS = [
|
||||
'/dashboard/libraries',
|
||||
'/dashboard/libraries/display',
|
||||
'/dashboard/libraries/metadata',
|
||||
'/dashboard/libraries/nfo'
|
||||
];
|
||||
|
||||
const PLAYBACK_PATHS = [
|
||||
'/dashboard/playback/transcoding',
|
||||
'/dashboard/playback/resume',
|
||||
'/dashboard/playback/streaming',
|
||||
'/dashboard/playback/trickplay'
|
||||
];
|
||||
|
||||
const ServerDrawerSection = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const [ isLibrarySectionOpen, setIsLibrarySectionOpen ] = useState(LIBRARY_PATHS.includes(location.pathname));
|
||||
const [ isPlaybackSectionOpen, setIsPlaybackSectionOpen ] = useState(PLAYBACK_PATHS.includes(location.pathname));
|
||||
|
||||
const onLibrarySectionClick = useCallback((e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsLibrarySectionOpen(isOpen => !isOpen);
|
||||
}, []);
|
||||
|
||||
const onPlaybackSectionClick = useCallback((e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsPlaybackSectionOpen(isOpen => !isOpen);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<List
|
||||
aria-labelledby='server-subheader'
|
||||
subheader={
|
||||
<ListSubheader component='div' id='server-subheader'>
|
||||
{globalize.translate('TabServer')}
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard'>
|
||||
<ListItemIcon>
|
||||
<Dashboard />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabDashboard')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/settings'>
|
||||
<ListItemIcon>
|
||||
<Settings />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('General')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItemLink to='/dashboard/branding'>
|
||||
<ListItemIcon>
|
||||
<Palette />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderBranding')} />
|
||||
</ListItemLink>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/users'>
|
||||
<ListItemIcon>
|
||||
<People />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderUsers')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton onClick={onLibrarySectionClick}>
|
||||
<ListItemIcon>
|
||||
<LibraryAdd />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderLibraries')} />
|
||||
{isLibrarySectionOpen ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<Collapse in={isLibrarySectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/dashboard/libraries' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('HeaderLibraries')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dashboard/libraries/display' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Display')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dashboard/libraries/metadata' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('LabelMetadata')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dashboard/libraries/nfo' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabNfoSettings')} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
</Collapse>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton onClick={onPlaybackSectionClick}>
|
||||
<ListItemIcon>
|
||||
<PlayCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TitlePlayback')} />
|
||||
{isPlaybackSectionOpen ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<Collapse in={isPlaybackSectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/dashboard/playback/transcoding' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Transcoding')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dashboard/playback/resume' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('ButtonResume')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dashboard/playback/streaming' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabStreaming')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dashboard/playback/trickplay' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Trickplay')} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
</Collapse>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerDrawerSection;
|
||||
@@ -1,17 +0,0 @@
|
||||
import format from 'date-fns/format';
|
||||
import type { MRT_Cell, MRT_RowData } from 'material-react-table';
|
||||
import { FC } from 'react';
|
||||
|
||||
import { useLocale } from 'hooks/useLocale';
|
||||
|
||||
interface CellProps {
|
||||
cell: MRT_Cell<MRT_RowData>
|
||||
}
|
||||
|
||||
const DateTimeCell: FC<CellProps> = ({ cell }) => {
|
||||
const { dateFnsLocale } = useLocale();
|
||||
|
||||
return format(cell.getValue<Date>(), 'Pp', { locale: dateFnsLocale });
|
||||
};
|
||||
|
||||
export default DateTimeCell;
|
||||
@@ -1,73 +0,0 @@
|
||||
import Box from '@mui/material/Box/Box';
|
||||
import Stack from '@mui/material/Stack/Stack';
|
||||
import type {} from '@mui/material/themeCssVarsAugmentation';
|
||||
import Typography from '@mui/material/Typography/Typography';
|
||||
import { type MRT_RowData, type MRT_TableInstance, type MRT_TableOptions, MaterialReactTable } from 'material-react-table';
|
||||
import React from 'react';
|
||||
|
||||
import Page, { type PageProps } from 'components/Page';
|
||||
|
||||
interface TablePageProps<T extends MRT_RowData> extends PageProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
table: MRT_TableInstance<T>
|
||||
}
|
||||
|
||||
export const DEFAULT_TABLE_OPTIONS: Partial<MRT_TableOptions<MRT_RowData>> = {
|
||||
// Enable custom features
|
||||
enableColumnPinning: true,
|
||||
enableColumnResizing: true,
|
||||
|
||||
// Sticky header/footer
|
||||
enableStickyFooter: true,
|
||||
enableStickyHeader: true,
|
||||
muiTableContainerProps: {
|
||||
sx: {
|
||||
maxHeight: 'calc(100% - 7rem)' // 2 x 3.5rem for header and footer
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const TablePage = <T extends MRT_RowData>({
|
||||
title,
|
||||
subtitle,
|
||||
table,
|
||||
children,
|
||||
...pageProps
|
||||
}: TablePageProps<T>) => {
|
||||
return (
|
||||
<Page
|
||||
title={title}
|
||||
{...pageProps}
|
||||
>
|
||||
<Box
|
||||
className='content-primary'
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
spacing={2}
|
||||
sx={{
|
||||
marginBottom: 1
|
||||
}}
|
||||
>
|
||||
<Typography variant='h1'>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle && (
|
||||
<Typography>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
<MaterialReactTable table={table} />
|
||||
</Box>
|
||||
{children}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default TablePage;
|
||||
@@ -1,36 +0,0 @@
|
||||
import HelpOutline from '@mui/icons-material/HelpOutline';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip/Tooltip';
|
||||
import React from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { HelpLinks } from 'apps/dashboard/constants/helpLinks';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
const HelpButton = () => (
|
||||
<Routes>
|
||||
{
|
||||
HelpLinks.map(({ paths, url }) => paths.map(path => (
|
||||
<Route
|
||||
key={[url, path].join('-')}
|
||||
path={path}
|
||||
element={
|
||||
<Tooltip title={globalize.translate('Help')}>
|
||||
<IconButton
|
||||
href={url}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
size='large'
|
||||
color='inherit'
|
||||
>
|
||||
<HelpOutline />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
))).flat()
|
||||
}
|
||||
</Routes>
|
||||
);
|
||||
|
||||
export default HelpButton;
|
||||
@@ -1,51 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import globalize from 'lib/globalize';
|
||||
import Widget from './Widget';
|
||||
import List from '@mui/material/List';
|
||||
import ActivityListItem from 'apps/dashboard/features/activity/components/ActivityListItem';
|
||||
import { useLogEntries } from 'apps/dashboard/features/activity/api/useLogEntries';
|
||||
import subSeconds from 'date-fns/subSeconds';
|
||||
import Skeleton from '@mui/material/Skeleton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
||||
const ActivityLogWidget = () => {
|
||||
const dayBefore = useMemo(() => (
|
||||
subSeconds(new Date(), 24 * 60 * 60).toISOString()
|
||||
), []);
|
||||
|
||||
const { data: logs, isPending } = useLogEntries({
|
||||
startIndex: 0,
|
||||
limit: 7,
|
||||
minDate: dayBefore,
|
||||
hasUserId: true
|
||||
});
|
||||
|
||||
return (
|
||||
<Widget
|
||||
title={globalize.translate('HeaderActivity')}
|
||||
href='/dashboard/activity?useractivity=true'
|
||||
>
|
||||
{isPending ? (
|
||||
<Stack spacing={2}>
|
||||
<Skeleton variant='rounded' height={60} />
|
||||
<Skeleton variant='rounded' height={60} />
|
||||
<Skeleton variant='rounded' height={60} />
|
||||
<Skeleton variant='rounded' height={60} />
|
||||
</Stack>
|
||||
) : (
|
||||
<List sx={{ bgcolor: 'background.paper' }}>
|
||||
{logs?.Items?.map(entry => (
|
||||
<ActivityListItem
|
||||
key={entry.Id}
|
||||
item={entry}
|
||||
displayShortOverview={true}
|
||||
to='/dashboard/activity?useractivity=true'
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityLogWidget;
|
||||
@@ -1,42 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import globalize from 'lib/globalize';
|
||||
import Widget from './Widget';
|
||||
import List from '@mui/material/List';
|
||||
import ActivityListItem from 'apps/dashboard/features/activity/components/ActivityListItem';
|
||||
import subSeconds from 'date-fns/subSeconds';
|
||||
import { useLogEntries } from 'apps/dashboard/features/activity/api/useLogEntries';
|
||||
|
||||
const AlertsLogWidget = () => {
|
||||
const weekBefore = useMemo(() => (
|
||||
subSeconds(new Date(), 7 * 24 * 60 * 60).toISOString()
|
||||
), []);
|
||||
|
||||
const { data: alerts, isPending } = useLogEntries({
|
||||
startIndex: 0,
|
||||
limit: 4,
|
||||
minDate: weekBefore,
|
||||
hasUserId: false
|
||||
});
|
||||
|
||||
if (isPending || alerts?.Items?.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Widget
|
||||
title={globalize.translate('Alerts')}
|
||||
href='/dashboard/activity?useractivity=false'
|
||||
>
|
||||
<List sx={{ bgcolor: 'background.paper' }}>
|
||||
{alerts?.Items?.map(entry => (
|
||||
<ActivityListItem
|
||||
key={entry.Id}
|
||||
item={entry}
|
||||
displayShortOverview={false}
|
||||
to='/dashboard/activity?useractivity=false'
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertsLogWidget;
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
import globalize from 'lib/globalize';
|
||||
import Widget from './Widget';
|
||||
import DeviceCard from 'apps/dashboard/features/devices/components/DeviceCard';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import useLiveSessions from 'apps/dashboard/features/sessions/hooks/useLiveSessions';
|
||||
|
||||
const DevicesWidget = () => {
|
||||
const { data: devices } = useLiveSessions();
|
||||
|
||||
return (
|
||||
<Widget
|
||||
title={globalize.translate('HeaderDevices')}
|
||||
href='/dashboard/devices'
|
||||
>
|
||||
<Stack direction='row' flexWrap='wrap' gap={2}>
|
||||
{devices?.map(device => (
|
||||
<DeviceCard
|
||||
key={device.Id}
|
||||
device={device}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export default DevicesWidget;
|
||||
@@ -1,103 +0,0 @@
|
||||
import type { ItemCounts } from '@jellyfin/sdk/lib/generated-client/models/item-counts';
|
||||
import Book from '@mui/icons-material/Book';
|
||||
import Movie from '@mui/icons-material/Movie';
|
||||
import MusicNote from '@mui/icons-material/MusicNote';
|
||||
import MusicVideo from '@mui/icons-material/MusicVideo';
|
||||
import Tv from '@mui/icons-material/Tv';
|
||||
import VideoLibrary from '@mui/icons-material/VideoLibrary';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import SvgIcon from '@mui/material/SvgIcon';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { useItemCounts } from 'apps/dashboard/features/metrics/api/useItemCounts';
|
||||
import MetricCard, { type MetricCardProps } from 'apps/dashboard/features/metrics/components/MetricCard';
|
||||
import globalize from 'lib/globalize';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
interface MetricDefinition {
|
||||
key: keyof ItemCounts
|
||||
i18n: string
|
||||
}
|
||||
|
||||
interface CardDefinition {
|
||||
Icon: typeof SvgIcon
|
||||
metrics: MetricDefinition[]
|
||||
}
|
||||
|
||||
const CARD_DEFINITIONS: CardDefinition[] = [
|
||||
{
|
||||
Icon: Movie,
|
||||
metrics: [{ key: 'MovieCount', i18n: 'Movies' }]
|
||||
}, {
|
||||
Icon: Tv,
|
||||
metrics: [
|
||||
{ key: 'SeriesCount', i18n: 'Series' },
|
||||
{ key: 'EpisodeCount', i18n: 'Episodes' }
|
||||
]
|
||||
}, {
|
||||
Icon: MusicNote,
|
||||
metrics: [
|
||||
{ key: 'AlbumCount', i18n: 'Albums' },
|
||||
{ key: 'SongCount', i18n: 'Songs' }
|
||||
]
|
||||
}, {
|
||||
Icon: MusicVideo,
|
||||
metrics: [{ key: 'MusicVideoCount', i18n: 'MusicVideos' }]
|
||||
}, {
|
||||
Icon: Book,
|
||||
metrics: [{ key: 'BookCount', i18n: 'Books' }]
|
||||
}, {
|
||||
Icon: VideoLibrary,
|
||||
metrics: [{ key: 'BoxSetCount', i18n: 'Collections' }]
|
||||
}
|
||||
];
|
||||
|
||||
const ItemCountsWidget = () => {
|
||||
const {
|
||||
data: counts,
|
||||
isPending
|
||||
} = useItemCounts();
|
||||
|
||||
const cards: MetricCardProps[] = useMemo(() => {
|
||||
return CARD_DEFINITIONS
|
||||
.filter(def => (
|
||||
// Include all cards while the request is pending
|
||||
isPending
|
||||
// Check if the metrics are present in counts
|
||||
|| def.metrics.some(({ key }) => counts?.[key])
|
||||
))
|
||||
.map(({ Icon, metrics }) => ({
|
||||
Icon,
|
||||
metrics: metrics.map(({ i18n, key }) => ({
|
||||
label: globalize.translate(i18n),
|
||||
value: counts?.[key]
|
||||
}))
|
||||
}));
|
||||
}, [ counts, isPending ]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Grid
|
||||
container
|
||||
spacing={2}
|
||||
sx={{
|
||||
alignItems: 'stretch'
|
||||
}}
|
||||
>
|
||||
{cards.map(card => (
|
||||
<Grid
|
||||
key={card.metrics.map(metric => metric.label).join('-')}
|
||||
item
|
||||
xs={12}
|
||||
sm={6}
|
||||
lg={4}
|
||||
>
|
||||
<MetricCard {...card} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemCountsWidget;
|
||||
@@ -1,42 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import globalize from 'lib/globalize';
|
||||
import Widget from './Widget';
|
||||
import type { TaskInfo } from '@jellyfin/sdk/lib/generated-client/models/task-info';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import { TaskState } from '@jellyfin/sdk/lib/generated-client/models/task-state';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import TaskProgress from 'apps/dashboard/features/tasks/components/TaskProgress';
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
||||
type RunningTasksWidgetProps = {
|
||||
tasks?: TaskInfo[];
|
||||
};
|
||||
|
||||
const RunningTasksWidget = ({ tasks }: RunningTasksWidgetProps) => {
|
||||
const runningTasks = useMemo(() => {
|
||||
return tasks?.filter(v => v.State == TaskState.Running) || [];
|
||||
}, [ tasks ]);
|
||||
|
||||
if (runningTasks.length == 0) return null;
|
||||
|
||||
return (
|
||||
<Widget
|
||||
title={globalize.translate('HeaderRunningTasks')}
|
||||
href='/dashboard/tasks'
|
||||
>
|
||||
<Paper sx={{ padding: 2 }}>
|
||||
<Stack spacing={2} maxWidth={'330px'}>
|
||||
{runningTasks.map((task => (
|
||||
<Box key={task.Id}>
|
||||
<Typography>{task.Name}</Typography>
|
||||
<TaskProgress task={task} />
|
||||
</Box>
|
||||
)))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export default RunningTasksWidget;
|
||||
@@ -1,104 +0,0 @@
|
||||
import React from 'react';
|
||||
import globalize from 'lib/globalize';
|
||||
import Widget from './Widget';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Button from '@mui/material/Button';
|
||||
import Skeleton from '@mui/material/Skeleton';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
import { useSystemInfo } from 'hooks/useSystemInfo';
|
||||
|
||||
type ServerInfoWidgetProps = {
|
||||
onScanLibrariesClick?: () => void;
|
||||
onRestartClick?: () => void;
|
||||
onShutdownClick?: () => void;
|
||||
isScanning?: boolean;
|
||||
};
|
||||
|
||||
const ServerInfoWidget = ({
|
||||
onScanLibrariesClick,
|
||||
onRestartClick,
|
||||
onShutdownClick,
|
||||
isScanning
|
||||
}: ServerInfoWidgetProps) => {
|
||||
const { data: systemInfo, isPending } = useSystemInfo();
|
||||
|
||||
return (
|
||||
<Widget
|
||||
title={globalize.translate('TabServer')}
|
||||
href='/dashboard/settings'
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<Paper sx={{
|
||||
padding: 2
|
||||
}}>
|
||||
<Stack direction='row'>
|
||||
<Stack flexGrow={1} spacing={1}>
|
||||
<Typography fontWeight='bold'>{globalize.translate('LabelServerName')}</Typography>
|
||||
<Typography fontWeight='bold'>{globalize.translate('LabelServerVersion')}</Typography>
|
||||
<Typography fontWeight='bold'>{globalize.translate('LabelWebVersion')}</Typography>
|
||||
<Typography fontWeight='bold'>{globalize.translate('LabelBuildVersion')}</Typography>
|
||||
</Stack>
|
||||
<Stack flexGrow={5} spacing={1}>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Skeleton />
|
||||
<Skeleton />
|
||||
<Skeleton />
|
||||
<Skeleton />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography>{systemInfo?.ServerName}</Typography>
|
||||
<Typography>{systemInfo?.Version}</Typography>
|
||||
<Typography>{__PACKAGE_JSON_VERSION__}</Typography>
|
||||
<Typography>{__JF_BUILD_VERSION__}</Typography>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Stack direction='row' spacing={1.5}>
|
||||
<Button
|
||||
onClick={onScanLibrariesClick}
|
||||
startIcon={<RefreshIcon />}
|
||||
sx={{
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
disabled={isScanning}
|
||||
>
|
||||
{globalize.translate('ButtonScanAllLibraries')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={onRestartClick}
|
||||
startIcon={<RestartAltIcon />}
|
||||
color='error'
|
||||
sx={{
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{globalize.translate('Restart')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={onShutdownClick}
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
color='error'
|
||||
sx={{
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{globalize.translate('ButtonShutdown')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerInfoWidget;
|
||||
@@ -1,50 +0,0 @@
|
||||
import List from '@mui/material/List';
|
||||
import React from 'react';
|
||||
import StorageListItem from 'apps/dashboard/features/storage/components/StorageListItem';
|
||||
import globalize from 'lib/globalize';
|
||||
import Widget from './Widget';
|
||||
import { useSystemStorage } from 'apps/dashboard/features/storage/api/useSystemStorage';
|
||||
|
||||
const ServerPathWidget = () => {
|
||||
const { data: systemStorage } = useSystemStorage();
|
||||
|
||||
return (
|
||||
<Widget
|
||||
title={globalize.translate('HeaderPaths')}
|
||||
href='/dashboard/settings'
|
||||
>
|
||||
<List sx={{ bgcolor: 'background.paper' }}>
|
||||
<StorageListItem
|
||||
label={globalize.translate('LabelCache')}
|
||||
folder={systemStorage?.CacheFolder}
|
||||
/>
|
||||
<StorageListItem
|
||||
label={globalize.translate('LabelImageCache')}
|
||||
folder={systemStorage?.ImageCacheFolder}
|
||||
/>
|
||||
<StorageListItem
|
||||
label={globalize.translate('LabelProgramData')}
|
||||
folder={systemStorage?.ProgramDataFolder}
|
||||
/>
|
||||
<StorageListItem
|
||||
label={globalize.translate('LabelLogs')}
|
||||
folder={systemStorage?.LogFolder}
|
||||
/>
|
||||
<StorageListItem
|
||||
label={globalize.translate('LabelMetadata')}
|
||||
folder={systemStorage?.InternalMetadataFolder}
|
||||
/>
|
||||
<StorageListItem
|
||||
label={globalize.translate('LabelTranscodes')}
|
||||
folder={systemStorage?.TranscodingTempFolder}
|
||||
/>
|
||||
<StorageListItem
|
||||
label={globalize.translate('LabelWeb')}
|
||||
folder={systemStorage?.WebFolder}
|
||||
/>
|
||||
</List>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerPathWidget;
|
||||
@@ -1,38 +0,0 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import ChevronRight from '@mui/icons-material/ChevronRight';
|
||||
import React from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
|
||||
type WidgetProps = {
|
||||
title: string;
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const Widget = ({ title, href, children }: WidgetProps) => {
|
||||
return (
|
||||
<Box>
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to={href}
|
||||
variant='text'
|
||||
color='inherit'
|
||||
endIcon={<ChevronRight />}
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
marginBottom: 1
|
||||
}}
|
||||
>
|
||||
<Typography variant='h3' component='span'>
|
||||
{title}
|
||||
</Typography>
|
||||
</Button>
|
||||
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Widget;
|
||||
@@ -1,54 +0,0 @@
|
||||
export const HelpLinks = [
|
||||
{
|
||||
paths: ['/dashboard/devices'],
|
||||
url: 'https://jellyfin.org/docs/general/server/devices'
|
||||
}, {
|
||||
paths: ['/dashboard/libraries'],
|
||||
url: 'https://jellyfin.org/docs/general/server/libraries'
|
||||
}, {
|
||||
paths: [
|
||||
'/dashboard/livetv',
|
||||
'/dashboard/livetv/tuner',
|
||||
'/dashboard/recordings'
|
||||
],
|
||||
url: 'https://jellyfin.org/docs/general/server/live-tv/'
|
||||
}, {
|
||||
paths: ['/dashboard/livetv/guide'],
|
||||
url: 'https://jellyfin.org/docs/general/server/live-tv/setup-guide#adding-guide-data'
|
||||
}, {
|
||||
paths: ['/dashboard/networking'],
|
||||
url: 'https://jellyfin.org/docs/general/networking/'
|
||||
}, {
|
||||
paths: ['/dashboard/playback/transcoding'],
|
||||
url: 'https://jellyfin.org/docs/general/server/transcoding'
|
||||
}, {
|
||||
paths: ['/dashboard/plugins'],
|
||||
url: 'https://jellyfin.org/docs/general/server/plugins/'
|
||||
}, {
|
||||
paths: ['/dashboard/plugins/repositories'],
|
||||
url: 'https://jellyfin.org/docs/general/server/plugins/#repositories'
|
||||
}, {
|
||||
paths: [
|
||||
'/dashboard/branding',
|
||||
'/dashboard/settings'
|
||||
],
|
||||
url: 'https://jellyfin.org/docs/general/server/settings'
|
||||
}, {
|
||||
paths: ['/dashboard/tasks'],
|
||||
url: 'https://jellyfin.org/docs/general/server/tasks'
|
||||
}, {
|
||||
paths: ['/dashboard/users'],
|
||||
url: 'https://jellyfin.org/docs/general/server/users/adding-managing-users'
|
||||
}, {
|
||||
paths: [
|
||||
'/dashboard/users/access',
|
||||
'/dashboard/users/parentalcontrol',
|
||||
'/dashboard/users/password',
|
||||
'/dashboard/users/profile'
|
||||
],
|
||||
url: 'https://jellyfin.org/docs/general/server/users/'
|
||||
}, {
|
||||
paths: ['/dashboard/backups'],
|
||||
url: 'https://jellyfin.org/docs/general/administration/backup-and-restore/'
|
||||
}
|
||||
];
|
||||
@@ -1,30 +0,0 @@
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import Dashboard, { pageIdOn } from 'utils/dashboard';
|
||||
import { getParameterByName } from 'utils/url';
|
||||
import Events from 'utils/events';
|
||||
|
||||
function onListingsSubmitted() {
|
||||
Dashboard.navigate('dashboard/livetv');
|
||||
}
|
||||
|
||||
function init(page, type, providerId) {
|
||||
import(`components/tvproviders/${type}`).then(({ default: ProviderFactory }) => {
|
||||
const instance = new ProviderFactory(page, providerId, {});
|
||||
Events.on(instance, 'submitted', onListingsSubmitted);
|
||||
instance.init();
|
||||
});
|
||||
}
|
||||
|
||||
function loadTemplate(page, type, providerId) {
|
||||
import(`components/tvproviders/${type}.template.html`).then(({ default: html }) => {
|
||||
page.querySelector('.providerTemplate').innerHTML = globalize.translateHtml(html);
|
||||
init(page, type, providerId);
|
||||
});
|
||||
}
|
||||
|
||||
pageIdOn('pageshow', 'liveTvGuideProviderPage', function () {
|
||||
loading.show();
|
||||
const providerId = getParameterByName('id');
|
||||
loadTemplate(this, getParameterByName('type'), providerId);
|
||||
});
|
||||
@@ -1,257 +0,0 @@
|
||||
import globalize from 'lib/globalize';
|
||||
import loading from 'components/loading/loading';
|
||||
import dom from 'utils/dom';
|
||||
import 'elements/emby-input/emby-input';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-select/emby-select';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import { getParameterByName } from 'utils/url';
|
||||
|
||||
function isM3uVariant(type) {
|
||||
return ['nextpvr'].indexOf(type || '') !== -1;
|
||||
}
|
||||
|
||||
function fillTypes(view, currentId) {
|
||||
return ApiClient.getJSON(ApiClient.getUrl('LiveTv/TunerHosts/Types')).then(function (types) {
|
||||
const selectType = view.querySelector('.selectType');
|
||||
let html = '';
|
||||
html += types.map(function (tuner) {
|
||||
return '<option value="' + tuner.Id + '">' + tuner.Name + '</option>';
|
||||
}).join('');
|
||||
html += '<option value="other">';
|
||||
html += globalize.translate('TabOther');
|
||||
html += '</option>';
|
||||
selectType.innerHTML = html;
|
||||
selectType.disabled = currentId != null;
|
||||
selectType.value = '';
|
||||
onTypeChange.call(selectType);
|
||||
});
|
||||
}
|
||||
|
||||
function reload(view, providerId) {
|
||||
view.querySelector('.txtDevicePath').value = '';
|
||||
view.querySelector('.chkFavorite').checked = false;
|
||||
view.querySelector('.txtDevicePath').value = '';
|
||||
|
||||
if (providerId) {
|
||||
ApiClient.getNamedConfiguration('livetv').then(function (config) {
|
||||
const info = config.TunerHosts.filter(function (i) {
|
||||
return i.Id === providerId;
|
||||
})[0];
|
||||
fillTunerHostInfo(view, info);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function fillTunerHostInfo(view, info) {
|
||||
const selectType = view.querySelector('.selectType');
|
||||
let type = info.Type || '';
|
||||
|
||||
if (info.Source && isM3uVariant(info.Source)) {
|
||||
type = info.Source;
|
||||
}
|
||||
|
||||
selectType.value = type;
|
||||
onTypeChange.call(selectType);
|
||||
view.querySelector('.txtDevicePath').value = info.Url || '';
|
||||
view.querySelector('.txtFriendlyName').value = info.FriendlyName || '';
|
||||
view.querySelector('.txtUserAgent').value = info.UserAgent || '';
|
||||
view.querySelector('.fldDeviceId').value = info.DeviceId || '';
|
||||
view.querySelector('.chkFavorite').checked = info.ImportFavoritesOnly;
|
||||
view.querySelector('.chkTranscode').checked = info.AllowHWTranscoding;
|
||||
view.querySelector('.chkStreamLoop').checked = info.EnableStreamLooping;
|
||||
view.querySelector('.chkFmp4Container').checked = info.AllowFmp4TranscodingContainer;
|
||||
view.querySelector('.chkStreamSharing').checked = info.AllowStreamSharing;
|
||||
view.querySelector('.chkIgnoreDts').checked = info.IgnoreDts;
|
||||
view.querySelector('.chkReadInputAtNativeFramerate').checked = info.ReadAtNativeFramerate;
|
||||
view.querySelector('.txtFallbackMaxStreamingBitrate').value = info.FallbackMaxStreamingBitrate / 1e6 || '30';
|
||||
view.querySelector('.txtTunerCount').value = info.TunerCount || '0';
|
||||
}
|
||||
|
||||
function submitForm(page) {
|
||||
loading.show();
|
||||
const info = {
|
||||
Type: page.querySelector('.selectType').value,
|
||||
Url: page.querySelector('.txtDevicePath').value || null,
|
||||
UserAgent: page.querySelector('.txtUserAgent').value || null,
|
||||
FriendlyName: page.querySelector('.txtFriendlyName').value || null,
|
||||
DeviceId: page.querySelector('.fldDeviceId').value || null,
|
||||
TunerCount: page.querySelector('.txtTunerCount').value || 0,
|
||||
FallbackMaxStreamingBitrate: parseInt(1e6 * parseFloat(page.querySelector('.txtFallbackMaxStreamingBitrate').value || '30'), 10),
|
||||
ImportFavoritesOnly: page.querySelector('.chkFavorite').checked,
|
||||
AllowHWTranscoding: page.querySelector('.chkTranscode').checked,
|
||||
AllowFmp4TranscodingContainer: page.querySelector('.chkFmp4Container').checked,
|
||||
AllowStreamSharing: page.querySelector('.chkStreamSharing').checked,
|
||||
EnableStreamLooping: page.querySelector('.chkStreamLoop').checked,
|
||||
IgnoreDts: page.querySelector('.chkIgnoreDts').checked,
|
||||
ReadAtNativeFramerate: page.querySelector('.chkReadInputAtNativeFramerate').checked
|
||||
};
|
||||
|
||||
if (isM3uVariant(info.Type)) {
|
||||
info.Source = info.Type;
|
||||
info.Type = 'm3u';
|
||||
}
|
||||
|
||||
if (getParameterByName('id')) {
|
||||
info.Id = getParameterByName('id');
|
||||
}
|
||||
|
||||
ApiClient.ajax({
|
||||
type: 'POST',
|
||||
url: ApiClient.getUrl('LiveTv/TunerHosts'),
|
||||
data: JSON.stringify(info),
|
||||
contentType: 'application/json'
|
||||
}).then(function () {
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
Dashboard.navigate('dashboard/livetv');
|
||||
}, function () {
|
||||
loading.hide();
|
||||
Dashboard.alert({
|
||||
message: globalize.translate('ErrorSavingTvProvider')
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getDetectedDevice() {
|
||||
return import('components/tunerPicker').then(({ default: TunerPicker }) => {
|
||||
return new TunerPicker().show({
|
||||
serverId: ApiClient.serverId()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onTypeChange() {
|
||||
const value = this.value;
|
||||
const view = dom.parentWithClass(this, 'page');
|
||||
const mayIncludeUnsupportedDrmChannels = value === 'hdhomerun';
|
||||
const supportsTranscoding = value === 'hdhomerun';
|
||||
const supportsFavorites = value === 'hdhomerun';
|
||||
const supportsTunerIpAddress = value === 'hdhomerun';
|
||||
const supportsTunerFileOrUrl = value === 'm3u';
|
||||
const supportsStreamLooping = value === 'm3u';
|
||||
const supportsIgnoreDts = value === 'm3u';
|
||||
const supportsReadInputAtNativeFramerate = value === 'm3u';
|
||||
const supportsTunerCount = value === 'm3u';
|
||||
const supportsUserAgent = value === 'm3u';
|
||||
const supportsFmp4Container = value === 'm3u';
|
||||
const supportsStreamSharing = value === 'm3u';
|
||||
const supportsFallbackBitrate = value === 'm3u' || value === 'hdhomerun';
|
||||
const suppportsSubmit = value !== 'other';
|
||||
const supportsSelectablePath = supportsTunerFileOrUrl;
|
||||
const txtDevicePath = view.querySelector('.txtDevicePath');
|
||||
|
||||
if (supportsTunerIpAddress) {
|
||||
txtDevicePath.label(globalize.translate('LabelTunerIpAddress'));
|
||||
view.querySelector('.fldPath').classList.remove('hide');
|
||||
} else if (supportsTunerFileOrUrl) {
|
||||
txtDevicePath.label(globalize.translate('LabelFileOrUrl'));
|
||||
view.querySelector('.fldPath').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldPath').classList.add('hide');
|
||||
}
|
||||
|
||||
if (supportsSelectablePath) {
|
||||
view.querySelector('.btnSelectPath').classList.remove('hide');
|
||||
view.querySelector('.txtDevicePath').setAttribute('required', 'required');
|
||||
} else {
|
||||
view.querySelector('.btnSelectPath').classList.add('hide');
|
||||
view.querySelector('.txtDevicePath').removeAttribute('required');
|
||||
}
|
||||
|
||||
if (supportsUserAgent) {
|
||||
view.querySelector('.fldUserAgent').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldUserAgent').classList.add('hide');
|
||||
}
|
||||
|
||||
if (supportsFavorites) {
|
||||
view.querySelector('.fldFavorites').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldFavorites').classList.add('hide');
|
||||
}
|
||||
|
||||
if (supportsTranscoding) {
|
||||
view.querySelector('.fldTranscode').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldTranscode').classList.add('hide');
|
||||
}
|
||||
|
||||
view.querySelector('.fldFmp4Container').classList.toggle('hide', !supportsFmp4Container);
|
||||
view.querySelector('.fldStreamSharing').classList.toggle('hide', !supportsStreamSharing);
|
||||
view.querySelector('.fldFallbackMaxStreamingBitrate').classList.toggle('hide', !supportsFallbackBitrate);
|
||||
|
||||
if (supportsStreamLooping) {
|
||||
view.querySelector('.fldStreamLoop').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldStreamLoop').classList.add('hide');
|
||||
}
|
||||
|
||||
if (supportsIgnoreDts) {
|
||||
view.querySelector('.fldIgnoreDts').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldIgnoreDts').classList.add('hide');
|
||||
}
|
||||
|
||||
view.querySelector('.fldReadInputAtNativeFramerate').classList.toggle('hide', !supportsReadInputAtNativeFramerate);
|
||||
|
||||
if (supportsTunerCount) {
|
||||
view.querySelector('.fldTunerCount').classList.remove('hide');
|
||||
view.querySelector('.txtTunerCount').setAttribute('required', 'required');
|
||||
} else {
|
||||
view.querySelector('.fldTunerCount').classList.add('hide');
|
||||
view.querySelector('.txtTunerCount').removeAttribute('required');
|
||||
}
|
||||
|
||||
if (mayIncludeUnsupportedDrmChannels) {
|
||||
view.querySelector('.drmMessage').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.drmMessage').classList.add('hide');
|
||||
}
|
||||
|
||||
if (suppportsSubmit) {
|
||||
view.querySelector('.button-submit').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.button-submit').classList.add('hide');
|
||||
}
|
||||
}
|
||||
|
||||
export default function (view, params) {
|
||||
if (!params.id) {
|
||||
view.querySelector('.btnDetect').classList.remove('hide');
|
||||
}
|
||||
|
||||
view.addEventListener('viewshow', function () {
|
||||
const currentId = params.id;
|
||||
fillTypes(view, currentId).then(function () {
|
||||
reload(view, currentId);
|
||||
});
|
||||
});
|
||||
view.querySelector('form').addEventListener('submit', function (e) {
|
||||
submitForm(view);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
});
|
||||
view.querySelector('.selectType').addEventListener('change', onTypeChange);
|
||||
view.querySelector('.btnDetect').addEventListener('click', function () {
|
||||
getDetectedDevice().then(function (info) {
|
||||
fillTunerHostInfo(view, info);
|
||||
});
|
||||
});
|
||||
view.querySelector('.btnSelectPath').addEventListener('click', function () {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
includeFiles: true,
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
view.querySelector('.txtDevicePath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
<div id="networkingPage" data-role="page" class="page type-interior advancedConfigurationPage" data-title="${TabNetworking}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="dashboardHostingForm">
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${TabNetworking}</h2>
|
||||
</div>
|
||||
|
||||
<fieldset class='verticalSection verticalSection-extrabottompadding'>
|
||||
<legend><h3>${HeaderServerAddressSettings}</h3></legend>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtPortNumber" label="${LabelLocalHttpServerPortNumber}" pattern="[0-9]*" required="required" min="1" max="65535" />
|
||||
<div class="fieldDescription">${LabelLocalHttpServerPortNumberHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableHttps" />
|
||||
<span>${LabelEnableHttps}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelEnableHttpsHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtHttpsPort" pattern="[0-9]*" required="required" min="1" max="65535" label="${LabelHttpsPort}" />
|
||||
<div class="fieldDescription">${LabelHttpsPortHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer fldBaseUrl">
|
||||
<input is="emby-input" id="txtBaseUrl" type="text" label="${LabelBaseUrl}" />
|
||||
<div class="fieldDescription">${LabelBaseUrlHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="txtLocalAddress" label="${LabelBindToLocalNetworkAddress}" />
|
||||
<div class="fieldDescription">${LabelBindToLocalNetworkAddressHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="txtLanNetworks" label="${LabelLanNetworks}" />
|
||||
<div class="fieldDescription">${LanNetworksHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="txtKnownProxies" label="${LabelKnownProxies}" />
|
||||
<div class="fieldDescription">${KnownProxiesHelp}</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class='verticalSection verticalSection-extrabottompadding'>
|
||||
<legend><h3>${HeaderHttpsSettings}</h3></legend>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkRequireHttps" />
|
||||
<span>${LabelRequireHttps}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelRequireHttpsHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer fldCertificatePath">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow:1;">
|
||||
<input is="emby-input" type="text" id="txtCertificatePath" label="${LabelCustomCertificatePath}" autocomplete="off" />
|
||||
</div>
|
||||
<button type="button" is="paper-icon-button-light" id="btnSelectCertPath" title="${ButtonSelectDirectory}" class="emby-input-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
<div class="fieldDescription">${LabelCustomCertificatePathHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer fldCertPassword">
|
||||
<input is="emby-input" id="txtCertPassword" type="password" label="${LabelCertificatePassword}" autocomplete="new-password" />
|
||||
<div class="fieldDescription">${LabelCertificatePasswordHelp}</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class='verticalSection verticalSection-extrabottompadding'>
|
||||
<legend><h3>${HeaderRemoteAccessSettings}</h3></legend>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkRemoteAccess" />
|
||||
<span>${AllowRemoteAccess}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowRemoteAccessHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer fldExternalAddressFilter hide">
|
||||
<input is="emby-input" type="text" id="txtExternalAddressFilter" label="${LabelAllowedRemoteAddresses}" />
|
||||
<div class="fieldDescription">${AllowedRemoteAddressesHelp}</div>
|
||||
</div>
|
||||
<div class="selectContainer fldExternalAddressFilterMode hide">
|
||||
<select is="emby-select" id="selectExternalAddressFilterMode" label="${LabelAllowedRemoteAddressesMode}">
|
||||
<option value="whitelist">${Whitelist}</option>
|
||||
<option value="blacklist">${Blacklist}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="inputContainer fldPublicHttpPort hide">
|
||||
<input is="emby-input" type="number" label="${LabelPublicHttpPort}" id="txtPublicHttpPort" pattern="[0-9]*" required="required" min="1" max="65535" />
|
||||
<div class="fieldDescription">${LabelPublicHttpPortHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer fldPublicHttpsPort hide">
|
||||
<input is="emby-input" type="number" id="txtPublicHttpsPort" pattern="[0-9]*" required="required" min="1" max="65535" label="${LabelPublicHttpsPort}" />
|
||||
<div class="fieldDescription">${LabelPublicHttpsPortHelp}</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class='verticalSection verticalSection-extrabottompadding'>
|
||||
<legend><h3>${HeaderNetworking}</h3></legend>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableIP4" />
|
||||
<span>${LabelEnableIP4}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelEnableIP4Help}</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableIP6" />
|
||||
<span>${LabelEnableIP6}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelEnableIP6Help}</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
|
||||
<fieldset class='verticalSection verticalSection-extrabottompadding'>
|
||||
<legend><h3>${HeaderAutoDiscovery}</h3></legend>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkAutodiscovery" />
|
||||
<span>${LabelAutomaticDiscovery}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelAutomaticDiscoveryHelp}</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class='verticalSection verticalSection-extrabottompadding'>
|
||||
<legend><h3>${HeaderPortRanges}</h3></legend>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="txtPublishedServer" label="${LabelPublishedServerUri}" />
|
||||
<div class="fieldDescription">${LabelPublishedServerUriHelp}</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,186 +0,0 @@
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-select/emby-select';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import alert from 'components/alert';
|
||||
|
||||
function onSubmit(e) {
|
||||
const form = this;
|
||||
const localAddress = form.querySelector('#txtLocalAddress').value;
|
||||
confirmSelections(localAddress, function () {
|
||||
const validationResult = getValidationAlert(form);
|
||||
|
||||
if (validationResult) {
|
||||
showAlertText(validationResult);
|
||||
return;
|
||||
}
|
||||
|
||||
validateHttps(form).then(function () {
|
||||
loading.show();
|
||||
ApiClient.getNamedConfiguration('network').then(function (config) {
|
||||
config.LocalNetworkSubnets = form.querySelector('#txtLanNetworks').value.split(',').map(function (s) {
|
||||
return s.trim();
|
||||
}).filter(function (s) {
|
||||
return s.length > 0;
|
||||
});
|
||||
config.RemoteIPFilter = form.querySelector('#txtExternalAddressFilter').value.split(',').map(function (s) {
|
||||
return s.trim();
|
||||
}).filter(function (s) {
|
||||
return s.length > 0;
|
||||
});
|
||||
config.KnownProxies = form.querySelector('#txtKnownProxies').value.split(',').map(function (s) {
|
||||
return s.trim();
|
||||
}).filter(function (s) {
|
||||
return s.length > 0;
|
||||
});
|
||||
config.LocalNetworkAddresses = form.querySelector('#txtLocalAddress').value.split(',').map(function (s) {
|
||||
return s.trim();
|
||||
}).filter(function (s) {
|
||||
return s.length > 0;
|
||||
});
|
||||
|
||||
config.PublishedServerUriBySubnet = form.querySelector('#txtPublishedServer').value.split(',').map(function (s) {
|
||||
return s.trim();
|
||||
}).filter(function (s) {
|
||||
return s.length > 0;
|
||||
});
|
||||
|
||||
config.IsRemoteIPFilterBlacklist = form.querySelector('#selectExternalAddressFilterMode').value === 'blacklist';
|
||||
config.PublicHttpPort = form.querySelector('#txtPublicHttpPort').value;
|
||||
config.PublicHttpsPort = form.querySelector('#txtPublicHttpsPort').value;
|
||||
config.InternalHttpPort = form.querySelector('#txtPortNumber').value;
|
||||
config.InternalHttpsPort = form.querySelector('#txtHttpsPort').value;
|
||||
config.EnableHttps = form.querySelector('#chkEnableHttps').checked;
|
||||
config.RequireHttps = form.querySelector('#chkRequireHttps').checked;
|
||||
config.BaseUrl = form.querySelector('#txtBaseUrl').value;
|
||||
config.EnableRemoteAccess = form.querySelector('#chkRemoteAccess').checked;
|
||||
config.CertificatePath = form.querySelector('#txtCertificatePath').value || null;
|
||||
config.CertificatePassword = form.querySelector('#txtCertPassword').value || null;
|
||||
config.AutoDiscovery = form.querySelector('#chkAutodiscovery').checked;
|
||||
config.EnableIPv6 = form.querySelector('#chkEnableIP6').checked;
|
||||
config.EnableIPv4 = form.querySelector('#chkEnableIP4').checked;
|
||||
ApiClient.updateNamedConfiguration('network', config).then(Dashboard.processServerConfigurationUpdateResult, Dashboard.processErrorResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function triggerChange(select) {
|
||||
const evt = new Event('change', { bubbles: false, cancelable: true });
|
||||
select.dispatchEvent(evt);
|
||||
}
|
||||
|
||||
function getValidationAlert(form) {
|
||||
if (form.querySelector('#txtPublicHttpPort').value === form.querySelector('#txtPublicHttpsPort').value) {
|
||||
return 'The public http and https ports must be different.';
|
||||
}
|
||||
|
||||
if (form.querySelector('#txtPortNumber').value === form.querySelector('#txtHttpsPort').value) {
|
||||
return 'The http and https ports must be different.';
|
||||
}
|
||||
|
||||
if (!form.querySelector('#chkEnableIP6').checked && !form.querySelector('#chkEnableIP4').checked) {
|
||||
return 'Either IPv4 or IPv6 need to be checked.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateHttps(form) {
|
||||
const certPath = form.querySelector('#txtCertificatePath').value || null;
|
||||
const httpsEnabled = form.querySelector('#chkEnableHttps').checked;
|
||||
|
||||
if (httpsEnabled && !certPath) {
|
||||
return showAlertText({
|
||||
title: globalize.translate('TitleHostingSettings'),
|
||||
text: globalize.translate('HttpsRequiresCert')
|
||||
}).then(Promise.reject);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function showAlertText(options) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
alert(options).then(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
function confirmSelections(localAddress, callback) {
|
||||
if (localAddress) {
|
||||
showAlertText({
|
||||
title: globalize.translate('TitleHostingSettings'),
|
||||
text: globalize.translate('SettingsWarning')
|
||||
}).then(callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
export default function (view) {
|
||||
function loadPage(page, config) {
|
||||
page.querySelector('#txtPortNumber').value = config.InternalHttpPort;
|
||||
page.querySelector('#txtPublicHttpPort').value = config.PublicHttpPort;
|
||||
page.querySelector('#txtPublicHttpsPort').value = config.PublicHttpsPort;
|
||||
page.querySelector('#txtLocalAddress').value = (config.LocalNetworkAddresses || []).join(', ');
|
||||
page.querySelector('#txtLanNetworks').value = (config.LocalNetworkSubnets || []).join(', ');
|
||||
page.querySelector('#txtKnownProxies').value = (config.KnownProxies || []).join(', ');
|
||||
page.querySelector('#txtExternalAddressFilter').value = (config.RemoteIPFilter || []).join(', ');
|
||||
page.querySelector('#selectExternalAddressFilterMode').value = config.IsRemoteIPFilterBlacklist ? 'blacklist' : 'whitelist';
|
||||
page.querySelector('#chkRemoteAccess').checked = config.EnableRemoteAccess == null || config.EnableRemoteAccess;
|
||||
page.querySelector('#txtHttpsPort').value = config.InternalHttpsPort;
|
||||
page.querySelector('#chkEnableHttps').checked = config.EnableHttps;
|
||||
page.querySelector('#chkRequireHttps').checked = config.RequireHttps;
|
||||
page.querySelector('#txtBaseUrl').value = config.BaseUrl || '';
|
||||
const txtCertificatePath = page.querySelector('#txtCertificatePath');
|
||||
txtCertificatePath.value = config.CertificatePath || '';
|
||||
page.querySelector('#txtCertPassword').value = config.CertificatePassword || '';
|
||||
triggerChange(page.querySelector('#chkRemoteAccess'));
|
||||
page.querySelector('#chkAutodiscovery').checked = config.AutoDiscovery;
|
||||
page.querySelector('#chkEnableIP6').checked = config.EnableIPv6;
|
||||
page.querySelector('#chkEnableIP4').checked = config.EnableIPv4;
|
||||
page.querySelector('#txtPublishedServer').value = (config.PublishedServerUriBySubnet || []).join(', ');
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
view.querySelector('#chkRemoteAccess').addEventListener('change', function () {
|
||||
if (this.checked) {
|
||||
view.querySelector('.fldExternalAddressFilter').classList.remove('hide');
|
||||
view.querySelector('.fldExternalAddressFilterMode').classList.remove('hide');
|
||||
view.querySelector('.fldPublicHttpPort').classList.remove('hide');
|
||||
view.querySelector('.fldPublicHttpsPort').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldExternalAddressFilter').classList.add('hide');
|
||||
view.querySelector('.fldExternalAddressFilterMode').classList.add('hide');
|
||||
view.querySelector('.fldPublicHttpPort').classList.add('hide');
|
||||
view.querySelector('.fldPublicHttpsPort').classList.add('hide');
|
||||
}
|
||||
});
|
||||
view.querySelector('#btnSelectCertPath').addEventListener('click', function () {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
includeFiles: true,
|
||||
includeDirectories: true,
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
view.querySelector('#txtCertificatePath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
header: globalize.translate('HeaderSelectCertificatePath')
|
||||
});
|
||||
});
|
||||
});
|
||||
view.querySelector('.dashboardHostingForm').addEventListener('submit', onSubmit);
|
||||
view.addEventListener('viewshow', function () {
|
||||
loading.show();
|
||||
ApiClient.getNamedConfiguration('network').then(function (config) {
|
||||
loadPage(view, config);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { ActivityLogApiGetLogEntriesRequest } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import type { Api } from '@jellyfin/sdk';
|
||||
import { getActivityLogApi } from '@jellyfin/sdk/lib/utils/api/activity-log-api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
const fetchLogEntries = async (
|
||||
api: Api,
|
||||
requestParams?: ActivityLogApiGetLogEntriesRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
const response = await getActivityLogApi(api).getLogEntries(requestParams, {
|
||||
signal: options?.signal
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const useLogEntries = (
|
||||
requestParams: ActivityLogApiGetLogEntriesRequest
|
||||
) => {
|
||||
const { api } = useApi();
|
||||
return useQuery({
|
||||
queryKey: ['ActivityLogEntries', requestParams],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchLogEntries(api!, requestParams, { signal }),
|
||||
enabled: !!api,
|
||||
refetchOnMount: false
|
||||
});
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import PermMedia from '@mui/icons-material/PermMedia';
|
||||
import React, { type FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ActivityLogEntryCell } from 'apps/dashboard/features/activity/types/ActivityLogEntryCell';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
const ActionsCell: FC<ActivityLogEntryCell> = ({ row }) => (
|
||||
row.original.ItemId ? (
|
||||
<IconButton
|
||||
size='large'
|
||||
title={globalize.translate('LabelMediaDetails')}
|
||||
component={Link}
|
||||
to={`/details?id=${row.original.ItemId}`}
|
||||
>
|
||||
<PermMedia fontSize='inherit' />
|
||||
</IconButton>
|
||||
) : undefined
|
||||
);
|
||||
|
||||
export default ActionsCell;
|
||||
@@ -1,75 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
|
||||
import Notifications from '@mui/icons-material/Notifications';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemAvatar from '@mui/material/ListItemAvatar';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import formatRelative from 'date-fns/formatRelative';
|
||||
import { getLocale } from 'utils/dateFnsLocale';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import getLogLevelColor from '../utils/getLogLevelColor';
|
||||
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
|
||||
type ActivityListItemProps = {
|
||||
item: ActivityLogEntry;
|
||||
displayShortOverview: boolean;
|
||||
to: string;
|
||||
};
|
||||
|
||||
const ActivityListItem = ({ item, displayShortOverview, to }: ActivityListItemProps) => {
|
||||
const relativeDate = useMemo(() => {
|
||||
if (item.Date) {
|
||||
return formatRelative(Date.parse(item.Date), Date.now(), { locale: getLocale() });
|
||||
} else {
|
||||
return 'N/A';
|
||||
}
|
||||
}, [ item ]);
|
||||
|
||||
return (
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to={to}>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: getLogLevelColor(item.Severity || LogLevel.Information) + '.main' }}>
|
||||
<Notifications sx={{ color: '#fff' }} />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
|
||||
<ListItemText
|
||||
primary={<Typography sx={{ whiteSpace: 'pre-wrap' }}>{item.Name}</Typography>}
|
||||
secondary={(
|
||||
<Stack>
|
||||
<Typography
|
||||
sx={{
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
variant='body1'
|
||||
color='text.secondary'
|
||||
>
|
||||
{relativeDate}
|
||||
</Typography>
|
||||
{displayShortOverview && (
|
||||
<Typography
|
||||
sx={{
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
variant='body1'
|
||||
color='text.secondary'
|
||||
>
|
||||
{item.ShortOverview}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
disableTypography
|
||||
/>
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityListItem;
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { ActivityLogEntryCell } from '../types/ActivityLogEntryCell';
|
||||
import LogLevelChip from './LogLevelChip';
|
||||
|
||||
const LogLevelCell: FC<ActivityLogEntryCell> = ({ cell }) => {
|
||||
const level = cell.getValue<LogLevel | undefined>();
|
||||
return level ? (
|
||||
<LogLevelChip level={level} />
|
||||
) : undefined;
|
||||
};
|
||||
|
||||
export default LogLevelCell;
|
||||
@@ -1,21 +0,0 @@
|
||||
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import getLogLevelColor from '../utils/getLogLevelColor';
|
||||
|
||||
const LogLevelChip = ({ level }: { level: LogLevel }) => {
|
||||
const levelText = useMemo(() => globalize.translate(`LogLevel.${level}`), [level]);
|
||||
|
||||
return (
|
||||
<Chip
|
||||
size='small'
|
||||
color={getLogLevelColor(level)}
|
||||
label={levelText}
|
||||
title={levelText}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogLevelChip;
|
||||
@@ -1,66 +0,0 @@
|
||||
import Info from '@mui/icons-material/Info';
|
||||
import Box from '@mui/material/Box';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import React, { type FC, useCallback, useState } from 'react';
|
||||
|
||||
import type { ActivityLogEntryCell } from '../types/ActivityLogEntryCell';
|
||||
|
||||
const OverviewCell: FC<ActivityLogEntryCell> = ({ row }) => {
|
||||
const { ShortOverview, Overview } = row.original;
|
||||
const displayValue = ShortOverview ?? Overview;
|
||||
const [ open, setOpen ] = useState(false);
|
||||
|
||||
const onTooltipClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, []);
|
||||
|
||||
const onTooltipOpen = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, []);
|
||||
|
||||
if (!displayValue) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}
|
||||
component='div'
|
||||
title={displayValue}
|
||||
>
|
||||
{displayValue}
|
||||
</Box>
|
||||
{ShortOverview && Overview && (
|
||||
<ClickAwayListener onClickAway={onTooltipClose}>
|
||||
<Tooltip
|
||||
title={Overview}
|
||||
placement='top'
|
||||
arrow
|
||||
onClose={onTooltipClose}
|
||||
open={open}
|
||||
disableFocusListener
|
||||
disableHoverListener
|
||||
disableTouchListener
|
||||
>
|
||||
<IconButton onClick={onTooltipOpen}>
|
||||
<Info />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ClickAwayListener>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverviewCell;
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
|
||||
import type { MRT_Cell, MRT_Row } from 'material-react-table';
|
||||
|
||||
export interface ActivityLogEntryCell {
|
||||
cell: MRT_Cell<ActivityLogEntry>
|
||||
row: MRT_Row<ActivityLogEntry>
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
||||
|
||||
const getLogLevelColor = (level: LogLevel) => {
|
||||
switch (level) {
|
||||
case LogLevel.Information:
|
||||
return 'info';
|
||||
case LogLevel.Warning:
|
||||
return 'warning';
|
||||
case LogLevel.Error:
|
||||
case LogLevel.Critical:
|
||||
return 'error';
|
||||
}
|
||||
};
|
||||
|
||||
export default getLogLevelColor;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { BackupApi } from '@jellyfin/sdk/lib/generated-client/api/backup-api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
export const QUERY_KEY = 'Backups';
|
||||
|
||||
const fetchBackups = async (api: Api, options?: AxiosRequestConfig) => {
|
||||
// FIXME: Replace with getBackupApi when available in SDK
|
||||
const backupApi = new BackupApi(api.configuration, undefined, api.axiosInstance);
|
||||
|
||||
const response = await backupApi.listBackups(options);
|
||||
|
||||
const backups = response.data;
|
||||
|
||||
backups.sort((a, b) => {
|
||||
if (a.DateCreated && b.DateCreated) {
|
||||
return new Date(b.DateCreated).getTime() - new Date(a.DateCreated).getTime();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return backups;
|
||||
};
|
||||
|
||||
export const useBackups = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [ QUERY_KEY ],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchBackups(api!, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { BackupOptionsDto } from '@jellyfin/sdk/lib/generated-client/models/backup-options-dto';
|
||||
import { BackupApi } from '@jellyfin/sdk/lib/generated-client/api/backup-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { QUERY_KEY } from './useBackups';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
export const useCreateBackup = () => {
|
||||
const { api } = useApi();
|
||||
// FIXME: Replace with getBackupApi when available in SDK
|
||||
const backupApi = new BackupApi(api?.configuration, undefined, api?.axiosInstance);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (backupOptions: BackupOptionsDto) => (
|
||||
backupApi.createBackup({
|
||||
backupOptionsDto: backupOptions
|
||||
})
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QUERY_KEY ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import { BackupApi } from '@jellyfin/sdk/lib/generated-client/api/backup-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
export const useRestoreBackup = () => {
|
||||
const { api } = useApi();
|
||||
// FIXME: Replace with getBackupApi when available in SDK
|
||||
const backupApi = new BackupApi(api?.configuration, undefined, api?.axiosInstance);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (fileName: string) => (
|
||||
backupApi.startRestoreBackup({
|
||||
backupRestoreRequestDto: {
|
||||
ArchiveFileName: fileName
|
||||
}
|
||||
})
|
||||
)
|
||||
});
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
import React, { FunctionComponent, useCallback, useState } from 'react';
|
||||
import type { BackupManifestDto } from '@jellyfin/sdk/lib/generated-client/models/backup-manifest-dto';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import Restore from '@mui/icons-material/Restore';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import globalize from 'lib/globalize';
|
||||
import BackupInfoDialog from './BackupInfoDialog';
|
||||
|
||||
type BackupProps = {
|
||||
backup: BackupManifestDto;
|
||||
onRestore: (backup: BackupManifestDto) => void;
|
||||
};
|
||||
|
||||
const Backup: FunctionComponent<BackupProps> = ({ backup, onRestore }) => {
|
||||
const [ isInfoDialogOpen, setIsInfoDialogOpen ] = useState(false);
|
||||
|
||||
const onDialogClose = useCallback(() => {
|
||||
setIsInfoDialogOpen(false);
|
||||
}, []);
|
||||
|
||||
const openDialog = useCallback(() => {
|
||||
setIsInfoDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const restore = useCallback(() => {
|
||||
onRestore(backup);
|
||||
}, [ backup, onRestore ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackupInfoDialog
|
||||
backup={backup}
|
||||
onClose={onDialogClose}
|
||||
open={isInfoDialogOpen}
|
||||
/>
|
||||
<ListItem
|
||||
disablePadding
|
||||
secondaryAction={
|
||||
<Tooltip disableInteractive title={globalize.translate('LabelRestore')}>
|
||||
<IconButton onClick={restore}>
|
||||
<Restore />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<ListItemButton onClick={openDialog}>
|
||||
<ListItemText
|
||||
primary={backup.DateCreated}
|
||||
secondary={backup.Path}
|
||||
slotProps={{
|
||||
primary: {
|
||||
variant: 'h3'
|
||||
},
|
||||
secondary: {
|
||||
variant: 'body1'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Backup;
|
||||
@@ -1,146 +0,0 @@
|
||||
import type { BackupManifestDto } from '@jellyfin/sdk/lib/generated-client/models/backup-manifest-dto';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import Box from '@mui/material/Box';
|
||||
import globalize from 'lib/globalize';
|
||||
import React, { FunctionComponent, useCallback, useState } from 'react';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import ContentCopy from '@mui/icons-material/ContentCopy';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { copy } from 'scripts/clipboard';
|
||||
import Toast from 'apps/dashboard/components/Toast';
|
||||
|
||||
type IProps = {
|
||||
backup: BackupManifestDto;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const BackupInfoDialog: FunctionComponent<IProps> = ({ backup, open, onClose }: IProps) => {
|
||||
const [ isCopiedToastOpen, setIsCopiedToastOpen ] = useState(false);
|
||||
|
||||
const handleToastClose = useCallback(() => {
|
||||
setIsCopiedToastOpen(false);
|
||||
}, []);
|
||||
|
||||
const copyPath = useCallback(async () => {
|
||||
if (backup.Path) {
|
||||
await copy(backup.Path);
|
||||
setIsCopiedToastOpen(true);
|
||||
}
|
||||
}, [ backup.Path ]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
maxWidth={'sm'}
|
||||
fullWidth
|
||||
>
|
||||
<Toast
|
||||
open={isCopiedToastOpen}
|
||||
onClose={handleToastClose}
|
||||
message={globalize.translate('Copied')}
|
||||
/>
|
||||
<DialogTitle>
|
||||
{backup.DateCreated}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Stack spacing={2}>
|
||||
<Box>
|
||||
<Stack
|
||||
direction='row'
|
||||
spacing={2}
|
||||
>
|
||||
<Typography fontWeight='bold'>{globalize.translate('LabelPath')}</Typography>
|
||||
<Stack direction='row'>
|
||||
<Typography color='text.secondary'>{backup.Path}</Typography>
|
||||
<IconButton size='small' onClick={copyPath}>
|
||||
<ContentCopy fontSize='small' />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction='row'
|
||||
spacing={2}
|
||||
>
|
||||
<Typography fontWeight='bold'>{globalize.translate('LabelVersion')}</Typography>
|
||||
<Typography color='text.secondary'>{backup.ServerVersion}</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<FormGroup>
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Database'
|
||||
defaultChecked={true}
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('LabelDatabase')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Metadata'
|
||||
defaultChecked={backup.Options?.Metadata}
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('LabelMetadata')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Subtitles'
|
||||
defaultChecked={backup.Options?.Subtitles}
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('Subtitles')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Trickplay'
|
||||
defaultChecked={backup.Options?.Trickplay}
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('Trickplay')}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>
|
||||
{globalize.translate('ButtonOk')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackupInfoDialog;
|
||||
@@ -1,27 +0,0 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
type IProps = {
|
||||
open: boolean
|
||||
};
|
||||
|
||||
const BackupProgressDialog: FunctionComponent<IProps> = ({ open }) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
maxWidth={'xs'}
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>{globalize.translate('MessageBackupInProgress')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<LinearProgress />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackupProgressDialog;
|
||||
@@ -1,123 +0,0 @@
|
||||
import React, { FunctionComponent, useCallback } from 'react';
|
||||
import globalize from 'lib/globalize';
|
||||
import type { BackupOptionsDto } from '@jellyfin/sdk/lib/generated-client/models/backup-options-dto';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
|
||||
type IProps = {
|
||||
open: boolean,
|
||||
onClose?: () => void,
|
||||
onCreate: (backupOptions: BackupOptionsDto) => void
|
||||
};
|
||||
|
||||
const CreateBackupForm: FunctionComponent<IProps> = ({ open, onClose, onCreate }) => {
|
||||
const onSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
const backupOptions: BackupOptionsDto = {
|
||||
'Metadata': data.Metadata?.toString() === 'on',
|
||||
'Trickplay': data.Trickplay?.toString() === 'on',
|
||||
'Subtitles': data.Subtitles?.toString() === 'on'
|
||||
};
|
||||
|
||||
onCreate(backupOptions);
|
||||
}, [ onCreate ]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
maxWidth={'xs'}
|
||||
fullWidth
|
||||
onClose={onClose}
|
||||
slotProps={{
|
||||
paper: {
|
||||
component: 'form',
|
||||
onSubmit
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle>{globalize.translate('ButtonCreateBackup')}</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Stack spacing={2}>
|
||||
<DialogContentText>
|
||||
{globalize.translate('MessageBackupDisclaimer')}
|
||||
</DialogContentText>
|
||||
<FormGroup>
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Database'
|
||||
defaultChecked={true}
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('LabelDatabase')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Metadata'
|
||||
defaultChecked={false}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('LabelMetadata')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Subtitles'
|
||||
defaultChecked={false}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('Subtitles')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Trickplay'
|
||||
defaultChecked={false}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('Trickplay')}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant='text'
|
||||
>{globalize.translate('ButtonCancel')}</Button>
|
||||
<Button type='submit'>{globalize.translate('Create')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateBackupForm;
|
||||
@@ -1,46 +0,0 @@
|
||||
import Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import globalize from 'lib/globalize';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
type IProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
||||
const RestoreConfirmationDialog: FunctionComponent<IProps> = ({ open, onClose, onConfirm }: IProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth={'xs'}
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
{globalize.translate('LabelRestore')}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{globalize.translate('MessageRestoreDisclaimer')}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant='text'>
|
||||
{globalize.translate('ButtonCancel')}
|
||||
</Button>
|
||||
<Button onClick={onConfirm}>
|
||||
{globalize.translate('LabelRestore')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default RestoreConfirmationDialog;
|
||||
@@ -1,32 +0,0 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
type IProps = {
|
||||
open: boolean
|
||||
};
|
||||
|
||||
const RestoreProgressDialog: FunctionComponent<IProps> = ({ open }) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
maxWidth={'xs'}
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>{globalize.translate('MessageRestoreInProgress')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack spacing={2}>
|
||||
<DialogContentText>{globalize.translate('MessageWaitingForServer')}</DialogContentText>
|
||||
<LinearProgress />
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default RestoreProgressDialog;
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { getBrandingApi } from '@jellyfin/sdk/lib/utils/api/branding-api';
|
||||
import { queryOptions, useQuery } from '@tanstack/react-query';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
export const QUERY_KEY = 'BrandingOptions';
|
||||
|
||||
const fetchBrandingOptions = async (
|
||||
api: Api,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
return getBrandingApi(api)
|
||||
.getBrandingOptions(options)
|
||||
.then(({ data }) => data);
|
||||
};
|
||||
|
||||
export const getBrandingOptionsQuery = (
|
||||
api?: Api
|
||||
) => queryOptions({
|
||||
queryKey: [ QUERY_KEY ],
|
||||
queryFn: ({ signal }) => fetchBrandingOptions(api!, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
|
||||
export const useBrandingOptions = () => {
|
||||
const { api } = useApi();
|
||||
return useQuery(getBrandingOptionsQuery(api));
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { DevicesApiDeleteDeviceRequest } from '@jellyfin/sdk/lib/generated-client/api/devices-api';
|
||||
import { getDevicesApi } from '@jellyfin/sdk/lib/utils/api/devices-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import { QUERY_KEY } from './useDevices';
|
||||
|
||||
export const useDeleteDevice = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (params: DevicesApiDeleteDeviceRequest) => (
|
||||
getDevicesApi(api!)
|
||||
.deleteDevice(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QUERY_KEY ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user