Compare commits

..

49 Commits

Author SHA1 Message Date
Joshua M. Boniface
c25db80cbd Bump version to 10.5.5 2020-04-26 15:26:31 -04:00
dkanada
e33ffbe9aa Merge pull request #1113 from MrTimscampi/tv-genres-title
Add title and year to posters in TV genres view

(cherry picked from commit ddc094dbfa)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-26 14:39:22 -04:00
dkanada
f68bdaa276 Merge pull request #1109 from jellyfin/change-to-google-cast
Change Chromecast player name to Google Cast

(cherry picked from commit f78bcd556d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-26 14:39:21 -04:00
dkanada
e804227d6d Merge pull request #1108 from nielsvanvelzen/androidtv-exists
Add Android icon for Android TV in devices page

(cherry picked from commit 14fcf7e3a5)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-26 14:39:20 -04:00
dkanada
9092d7fc06 Merge pull request #1105 from thornbill/restore-user-menu
Restore user menu on mobile

(cherry picked from commit 511fe3b61c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-26 14:39:19 -04:00
dkanada
148920f2f9 Merge pull request #1104 from dmitrylyzo/fix-undefined-variable
Fix reference to undefined variable

(cherry picked from commit 7646c17688)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-26 14:39:19 -04:00
Vasily
482fa20364 Merge pull request #1095 from JustAMan/fix-subs-on-mobile
Fix .ass subtitles not starting on mobile

(cherry picked from commit a4324665f0)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-26 14:39:18 -04:00
dkanada
b102201607 Merge pull request #1093 from MrTimscampi/mobile-fixes-2
Fix some mobile navigation issues

(cherry picked from commit 542a738926)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-26 14:39:17 -04:00
dkanada
7909c0874b Merge pull request #1074 from dmitrylyzo/playback_delayed_update
Move delayed volume update to playbackManager

(cherry picked from commit 5063c4f050)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-26 14:39:16 -04:00
dkanada
d691f4c38f Merge pull request #1022 from ferferga/fix-mobile-layout
Fix mobile layout for itemdetails

(cherry picked from commit d4427e8a37)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-26 14:39:14 -04:00
dkanada
b4adc677fd Merge pull request #872 from Artiume/patch-10
Update ISO playback warning

(cherry picked from commit 9f6ec50715)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-26 14:39:10 -04:00
dkanada
fd66c5a3fb Merge pull request #1111 from dmitrylyzo/fix-old-browser-template-string
Change template string to generic one to support older browsers
2020-04-21 13:15:24 +09:00
Dmitry Lyzo
364bbc988d Change template string to generic one to support older browsers 2020-04-20 19:04:49 +03:00
Joshua M. Boniface
51722fd225 Bump version to 10.5.4 2020-04-12 19:31:34 -04:00
Joshua M. Boniface
a158b1f85a Merge pull request #1072 from ferferga/fix-recent
Move "hide watched media" checkbox

(cherry picked from commit 6246dce320)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 19:31:07 -04:00
dkanada
0bb732c60a Merge pull request #1070 from redSpoutnik/subtitle-sync-subtitleOctopus
Set subtitle-sync for SubtitlesOctopus

(cherry picked from commit b311ad120e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 19:31:01 -04:00
Vasily
2782127ba8 Merge pull request #1068 from jellyfin/download
Pass title and filename to native shell

(cherry picked from commit ed3b140379)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 19:30:59 -04:00
dkanada
6a80b4caeb Merge pull request #1049 from JustAMan/fix-ff-newline-subs
Fix newline breaks in SRT subtitles shown in Firefox

(cherry picked from commit b44c8c0c52)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 19:29:07 -04:00
dkanada
b2e95b0ee6 Merge pull request #1048 from JustAMan/bump-octopus
Use patched octopus that works on Cordova

(cherry picked from commit ce84ead81f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 19:29:05 -04:00
Vasily
b262b98e83 Merge pull request #1047 from ZadenRB/date-added-dropdown-fix
Fix inconsistent value in drop down list on library page

(cherry picked from commit 14df43c6cb)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 19:27:26 -04:00
Vasily
b52cb34319 Merge pull request #1040 from JustAMan/fix-native-hls-resume
Fix native hls resume

(cherry picked from commit 2223a16813)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 19:26:28 -04:00
Anthony Lavado
914ef1e566 Merge pull request #963 from MrTimscampi/fix-logo-size
Don't scale logo on details page

(cherry picked from commit 64a61201d6)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-12 19:26:25 -04:00
Joshua M. Boniface
f832206145 Merge pull request #1018 from MrTimscampi/backport-octopus
Backport changes to Octopus
2020-04-05 13:01:49 -04:00
Joshua M. Boniface
46fcdf91b8 Merge pull request #1028 from ZadenRB/itemdetails-alignment
Fixed alignment of subsections on item details page

(cherry picked from commit 7215e14c39)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:58:40 -04:00
Vasily
3c953d5ffd Merge pull request #1020 from JustAMan/fix-attachment-urls
Fix attachment delivery urls

(cherry picked from commit 50122d0253)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:58:16 -04:00
Vasily
6c28570e82 Merge pull request #1005 from JustAMan/update-octopus
Switch to new version of JavascriptSubtitlesOctopus, enable new options

(cherry picked from commit b782688505)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:57:38 -04:00
Vasily
acf632e77e Merge pull request #1003 from MrTimscampi/admin-logo
Fix admin drawer logo showing up everywhere

(cherry picked from commit 9a65e0351a)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:55:32 -04:00
dkanada
44c273f531 Merge pull request #1000 from Nazar78/tizen5.0-h264-level-52
Support H264 Level 52 (Tizen 5.0)

(cherry picked from commit 4c72a8381c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:55:02 -04:00
dkanada
ef09b24f0c Merge pull request #970 from MrTimscampi/mobile-menu
Clean up the menus on mobile

(cherry picked from commit 601b75a1a8)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:54:41 -04:00
dkanada
87bcf40e81 Merge pull request #937 from dmitrylyzo/fix_radio_style
Fix radio style

(cherry picked from commit 8a1262eedd)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-04-05 12:54:21 -04:00
Joshua M. Boniface
3cae48be23 Bump version for 10.5.3 2020-04-05 12:53:18 -04:00
MrTimscampi
ecc65dfa3b Backport changes to Octopus 2020-04-04 12:49:15 +02:00
Joshua M. Boniface
9be3f2e731 Merge pull request #969 from dmitrylyzo/tizen_xvid
Fix XviD playback on Tizen

(cherry picked from commit 31d9b35615)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-22 15:26:45 -04:00
dkanada
44c7b75dbb Merge pull request #932 from MrTimscampi/mobile-fixes
Show hamburger menu on mobile and fix title padding

(cherry picked from commit c49c45ee53)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-22 15:26:25 -04:00
dkanada
253b0d96d6 Merge pull request #904 from dmitrylyzo/fix_player_data
Clear player data after stop

(cherry picked from commit d5779e115d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-22 15:25:53 -04:00
Joshua M. Boniface
7fb75c6d82 Bump version to 10.5.2 2020-03-22 12:09:05 -04:00
dkanada
98816bcce0 Merge pull request #958 from MrTimscampi/listview-missing
Show missing indicator in ListView

(cherry picked from commit 1688a3999c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-22 11:52:13 -04:00
Vasily
067e43c0d3 Merge pull request #955 from thornbill/fix-schedules-direct
Fix schedules direct buttons being hidden by default

(cherry picked from commit 298e36388f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-22 11:51:27 -04:00
dkanada
a956d602f9 Merge pull request #951 from dtparr/fixUrl
Correct the url in the wizardstart.html to remove the administrator folder

(cherry picked from commit 857ed5401e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-22 11:50:48 -04:00
dkanada
9cf6ccc73c Merge pull request #931 from dmitrylyzo/fix_radio
Fix radiobutton and checkbox

(cherry picked from commit 9b54dec5e8)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-22 11:50:17 -04:00
dkanada
b3b9f355c3 Merge pull request #919 from dmitrylyzo/fix_icons-2
Fix icons doubling and WebOS support

(cherry picked from commit c5171a2fa0)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-22 11:49:43 -04:00
dkanada
b2d2b1360c Merge pull request #907 from MrTimscampi/artwork-optimal-size
Improve image loading speed and sizes

(cherry picked from commit bdfa8b0121)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-22 11:49:15 -04:00
dkanada
af7f626a43 Merge pull request #948 from MrTimscampi/fix-artist-backdrop
Fix artist details ribbon

(cherry picked from commit 6b22a1113e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-15 23:11:48 -04:00
Joshua M. Boniface
5568d945a7 Merge pull request #943 from jellyfin/dependabot/npm_and_yarn/acorn-6.4.1
Bump acorn from 6.2.1 to 6.4.1

(cherry picked from commit 08919544e4)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-15 23:11:17 -04:00
dkanada
4dc5535a02 Merge pull request #936 from macr/master
Fix "Copy Stream URL" for iOS.

(cherry picked from commit 0b107b3770)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-15 23:08:49 -04:00
dkanada
b6f5435750 Merge pull request #929 from mark-monteiro/fix-networking-config
Fix Network Settings Page

(cherry picked from commit 00c01d2a4b)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-15 23:08:21 -04:00
dkanada
8db63ab520 Merge pull request #926 from ThibaultNocchi/fix_925
Fixed broken links described by #925

(cherry picked from commit 5de205e44d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-15 23:07:57 -04:00
dkanada
527c5fb43e Merge pull request #920 from dmitrylyzo/fix_slideshow-2
Fix slideshow 2

(cherry picked from commit 95f379021e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-03-15 23:07:25 -04:00
Joshua M. Boniface
9ef4e95467 Bump version to 10.5.1 2020-03-15 23:04:37 -04:00
1429 changed files with 113210 additions and 269148 deletions

63
.ci/azure-pipelines.yml Normal file
View File

@@ -0,0 +1,63 @@
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'
inputs:
versionSpec: '10.x'
- script: 'yarn install'
displayName: 'Install Dependencies'
- script: 'test -d dist'
displayName: 'Check Build'
- script: 'yarn pack --filename jellyfin-web.tgz'
displayName: 'Bundle Release'
- task: PublishPipelineArtifact@1
displayName: 'Publish Release'
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'
inputs:
versionSpec: '10.x'
- script: 'yarn install'
displayName: 'Install Dependencies'
- script: 'yarn run lint'
displayName: 'Run ESLint'
- script: |
yarn run stylelint
displayName: 'Run stylelint'

View File

@@ -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"
}

View File

@@ -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

View File

@@ -1,9 +0,0 @@
{
"ecmaVersion": "es5",
"files": "./dist/**/*.js",
"not": [
"./dist/libraries/pdf.worker.js",
"./dist/libraries/worker-bundle.js",
"./dist/serviceworker.js"
]
}

1
.eslintignore Normal file
View File

@@ -0,0 +1 @@
libraries/

71
.eslintrc.yml Normal file
View File

@@ -0,0 +1,71 @@
env:
es6: false
browser: true
amd: true
globals:
# New browser globals
DataView: readonly
MediaMetadata: readonly
Promise: readonly
# Deprecated browser globals
DocumentTouch: readonly
# Tizen globals
tizen: readonly
webapis: readonly
# WebOS globals
webOS: readonly
# Dependency globals
$: readonly
jQuery: readonly
queryString: readonly
requirejs: readonly
# Jellyfin globals
ApiClient: writable
AppInfo: writable
chrome: writable
ConnectionManager: writable
DlnaProfilePage: writable
Dashboard: writable
DashboardPage: writable
Emby: readonly
Events: writable
getParameterByName: writable
getWindowLocationSearch: writable
Globalize: writable
Hls: writable
humaneDate: writable
humaneElapsed: writable
LibraryMenu: writable
LinkParser: writable
LiveTvHelpers: writable
MetadataEditor: writable
pageClassOn: writable
pageIdOn: writable
PlaylistViewer: writable
UserParentalControlPage: writable
Windows: readonly
extends:
- eslint:recommended
rules:
block-spacing: ["error"]
brace-style: ["error"]
comma-dangle: ["error", "never"]
comma-spacing: ["error"]
eol-last: ["error"]
indent: ["error", 4, { "SwitchCase": 1 }]
keyword-spacing: ["error"]
max-statements-per-line: ["error"]
no-floating-decimal: ["error"]
no-multi-spaces: ["error"]
no-multiple-empty-lines: ["error", { "max": 1 }]
no-trailing-spaces: ["error"]
one-var: ["error", "never"]
semi: ["warn"]
space-before-blocks: ["error"]
# TODO: Fix warnings and remove these rules
no-redeclare: ["warn"]
no-unused-vars: ["warn"]
no-useless-escape: ["warn"]

36
.gitattributes vendored
View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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. -->

View File

@@ -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
View File

@@ -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.

View File

@@ -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
View File

@@ -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
}
]
}

20
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
# 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
- confirmed
# 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

View File

@@ -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 }}

View File

@@ -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}}'

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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.

587
.gitignore vendored
View File

@@ -1,23 +1,578 @@
# 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/
# Dependency lockfile
package-lock.json
# 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

3
.npmrc
View File

@@ -1,3 +0,0 @@
engine-strict=true
fund=false
save-exact=true

1
.nvmrc
View File

@@ -1 +0,0 @@
20

View File

@@ -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

143
.stylelintrc Normal file
View File

@@ -0,0 +1,143 @@
{
"plugins": [
"stylelint-no-browser-hacks/lib",
],
"rules": {
"at-rule-empty-line-before": [ "always", {
except: [
"blockless-after-same-name-blockless",
"first-nested",
],
ignore: ["after-comment"],
} ],
"at-rule-name-case": "lower",
"at-rule-name-space-after": "always-single-line",
"at-rule-no-unknown": true,
"at-rule-semicolon-newline-after": "always",
"block-closing-brace-empty-line-before": "never",
"block-closing-brace-newline-after": "always",
"block-closing-brace-newline-before": "always-multi-line",
"block-closing-brace-space-before": "always-single-line",
"block-no-empty": true,
"block-opening-brace-newline-after": "always-multi-line",
"block-opening-brace-space-after": "always-single-line",
"block-opening-brace-space-before": "always",
"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",
],
} ],
"declaration-bang-space-after": "never",
"declaration-bang-space-before": "always",
"declaration-block-no-duplicate-properties": [
true,
{
ignore: ["consecutive-duplicates-with-different-values"]
}
],
"declaration-block-no-shorthand-property-overrides": true,
"declaration-block-semicolon-newline-after": "always-multi-line",
"declaration-block-semicolon-space-after": "always-single-line",
"declaration-block-semicolon-space-before": "never",
"declaration-block-single-line-max-declarations": 1,
"declaration-block-trailing-semicolon": "always",
"declaration-colon-newline-after": "always-multi-line",
"declaration-colon-space-after": "always-single-line",
"declaration-colon-space-before": "never",
"font-family-no-duplicate-names": true,
"function-calc-no-invalid": true,
"function-calc-no-unspaced-operator": true,
"function-comma-newline-after": "always-multi-line",
"function-comma-space-after": "always-single-line",
"function-comma-space-before": "never",
"function-linear-gradient-no-nonstandard-direction": true,
"function-max-empty-lines": 0,
"function-name-case": "lower",
"function-parentheses-newline-inside": "always-multi-line",
"function-parentheses-space-inside": "never-single-line",
"function-whitespace-after": "always",
"indentation": 4,
"keyframe-declaration-no-important": true,
"length-zero-no-unit": true,
"max-empty-lines": 1,
"media-feature-colon-space-after": "always",
"media-feature-colon-space-before": "never",
"media-feature-name-case": "lower",
"media-feature-name-no-unknown": true,
"media-feature-parentheses-space-inside": "never",
"media-feature-range-operator-space-after": "always",
"media-feature-range-operator-space-before": "always",
"media-query-list-comma-newline-after": "always-multi-line",
"media-query-list-comma-space-after": "always-single-line",
"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,
"no-eol-whitespace": true,
"no-extra-semicolons": true,
"no-invalid-double-slash-comments": true,
"no-missing-end-of-source-newline": true,
"number-leading-zero": "always",
"number-no-trailing-zeros": true,
"plugin/no-browser-hacks": true,
"property-case": "lower",
"property-no-unknown": [
true,
{
"ignoreProperties": [
"user-drag"
]
}
],
"rule-empty-line-before": [ "always-multi-line", {
except: ["first-nested"],
ignore: ["after-comment"],
} ],
"selector-attribute-brackets-space-inside": "never",
"selector-attribute-operator-space-after": "never",
"selector-attribute-operator-space-before": "never",
"selector-combinator-space-after": "always",
"selector-combinator-space-before": "always",
"selector-descendant-combinator-no-non-space": true,
"selector-list-comma-newline-after": "always",
"selector-list-comma-space-before": "never",
"selector-max-empty-lines": 0,
"selector-pseudo-class-case": "lower",
"selector-pseudo-class-no-unknown": true,
"selector-pseudo-class-parentheses-space-inside": "never",
"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,
"unit-case": "lower",
"unit-no-unknown": true,
"value-list-comma-newline-after": "always-multi-line",
"value-list-comma-space-after": "always-single-line",
"value-list-comma-space-before": "never",
"value-list-max-empty-lines": 0,
}
}

View File

@@ -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
}
}
]
}

View File

@@ -1,5 +0,0 @@
{
"recommendations": [
"dbaeumer.vscode-eslint"
]
}

10
.vscode/settings.json vendored
View File

@@ -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
}

View File

@@ -1,173 +1,99 @@
# 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)
- [Thibault Nocchi](https://github.com/ThibaultNocchi)
- [MrTimscampi](https://github.com/MrTimscampi)
## 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)

View File

@@ -23,6 +23,9 @@
<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://forum.jellyfin.org">
<img alt="Discuss on our Forum" src="https://img.shields.io/discourse/https/forum.jellyfin.org/users.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>
@@ -41,71 +44,21 @@ Jellyfin Web is the frontend used for most of the clients available for end user
### Dependencies
- [Node.js](https://nodejs.org/en/download)
- npm (included in Node.js)
- Yarn
### 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
yarn install
```
3. Run the web client with webpack for local development.
```sh
npm start
yarn serve
```
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
```
- ❌ &mdash; Deprecated, do **not** create new files here
- 🧹 &mdash; Needs cleanup
- 🐉 &mdash; Serious mess (Here be dragons)

View File

@@ -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: [
]
};

View File

@@ -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

View File

@@ -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 }
]
};

View File

@@ -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
View File

@@ -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
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -1,175 +1,66 @@
{
"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"
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.4.2",
"eslint": "^6.8.0",
"file-loader": "^5.0.2",
"html-webpack-plugin": "^3.2.0",
"style-loader": "^1.1.3",
"stylelint": "^13.1.0",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-no-browser-hacks": "^1.2.1",
"stylelint-order": "^4.0.0",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-concat-plugin": "^3.0.0",
"webpack-dev-server": "^3.10.3",
"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"
},
"optionalDependencies": {
"sass-embedded": "1.89.2"
"alameda": "^1.4.0",
"document-register-element": "^1.14.3",
"flv.js": "^1.5.0",
"hls.js": "^0.13.1",
"howler": "^2.1.3",
"jellyfin-noto": "https://github.com/jellyfin/jellyfin-noto",
"jquery": "^3.4.1",
"jstree": "^3.3.7",
"libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-cordova",
"libjass": "^0.11.0",
"material-design-icons-iconfont": "^5.0.1",
"native-promise-only": "^0.8.0-a",
"resize-observer-polyfill": "^1.5.1",
"shaka-player": "^2.5.9",
"sortablejs": "^1.10.2",
"swiper": "^5.3.1",
"webcomponents.js": "^0.7.24",
"whatwg-fetch": "^3.0.0"
},
"browserslist": [
"last 2 Firefox versions",
"last 2 Chrome versions",
"last 2 ChromeAndroid versions",
"last 2 Safari versions",
"iOS > 10",
"last 2 iOS versions",
"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\"",
"stylelint": "stylelint \"src/**/*.css\"",
"prepare": "webpack --config webpack.prod.js"
}
}

View File

@@ -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;

View File

@@ -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')

View File

@@ -15,8 +15,6 @@ 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:
@@ -34,19 +32,10 @@ for lang in langlst:
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')

View File

@@ -16,7 +16,7 @@ langlst.append('en-us.json')
dep = []
def grep(key):
command = 'grep -r -E "(\\\"|\'|\{)%s(\\\"|\'|\})" --include=\*.{js,html} --exclude-dir=../src/strings ../src' % 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:
@@ -34,7 +34,7 @@ for lang in langlst:
print(dep)
print('LENGTH: ' + str(len(dep)))
with open('unused.txt', 'w') as out:
with open('scout.txt', 'w') as out:
for item in dep:
out.write(item + '\n')
out.close()

View File

@@ -11,7 +11,7 @@ langlst = os.listdir(langdir)
keys = []
with open('unused.txt', 'r') as f:
with open('scout.txt', 'r') as f:
for line in f:
keys.append(line.strip('\n'))

View File

@@ -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;

View File

@@ -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>
);
}

54
src/addplugin.html Normal file
View File

@@ -0,0 +1,54 @@
<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>
<div 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>
</div>
<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>

View File

@@ -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
View File

@@ -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 */

25
src/apikeys.html Normal file
View File

@@ -0,0 +1,25 @@
<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="material-icons">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>

View File

@@ -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>
);
};

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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/'
}
];

View File

@@ -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);
});

View File

@@ -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();
}
});
});
});
}

View File

@@ -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>

View File

@@ -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);
});
});
}

View File

@@ -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
});
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
}

View File

@@ -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;

View File

@@ -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
});
};

View File

@@ -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 ]
});
}
});
};

View File

@@ -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
}
})
)
});
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

Some files were not shown because too many files have changed in this diff Show More