Compare commits

..

38 Commits

Author SHA1 Message Date
Joshua M. Boniface
fc461a830d Fix Release ref for Fedora 2020-08-30 16:45:54 -04:00
Joshua M. Boniface
e6e5f38a0d Bump version to 10.6.4 2020-08-30 16:45:40 -04:00
dkanada
756e322e1c Merge pull request #1820 from thornbill/fix-guide-margin
Remove horizontal margins on guide

(cherry picked from commit 9e3a27266c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-08-30 16:15:07 -04:00
Anthony Lavado
c9ed8c750e Merge pull request #1823 from thornbill/extra-blury
Set background color on blurhash image load

(cherry picked from commit 5a2c0beec5)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-08-30 16:14:54 -04:00
Joshua M. Boniface
5f1af65c2e Merge pull request #1849 from brianjmurrell/patch-1
Add BuildRequires: git to Fedora specfile
2020-08-30 16:05:29 -04:00
Brian J. Murrell
ff88922f19 Add BuildRequires: git to Fedora specfile
Sadly the yarn RPM at https://dl.yarnpkg.com/rpm/ uses git but doesn't
add it as a BuildRequires:.

This really should be reported upstream at yarnpkg.com.

Signed-off-by: Brian J. Murrell <brian@interlinx.bc.ca>
2020-08-27 12:29:07 -04:00
dkanada
47700dc29c Merge pull request #1818 from dmitrylyzo/fix-non-es6
Fix non-ES6 import [10.6.z]
2020-08-18 23:15:12 +09:00
Dmitry Lyzo
c2ebc35080 Fix non-ES6 import 2020-08-18 15:01:41 +03:00
Joshua M. Boniface
d040177525 Bump version to 10.6.3 2020-08-16 19:48:04 -04:00
dkanada
cac6b42dab Merge pull request #1799 from matjaz321/checkbox-space-ff-issue
Hitting space to check/uncheck checkboxes doesn't work on firefox

(cherry picked from commit fea28f7277)
2020-08-16 19:47:09 -04:00
dkanada
15e9b645f2 Merge pull request #1794 from bugfixin/master
Remove extraneous pageContainer element from videoOSD

(cherry picked from commit 926f1cbbcc)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-08-16 19:29:20 -04:00
dkanada
1833b348e9 Merge pull request #1748 from dmitrylyzo/fix-old-edge
Fix old Edge loading

(cherry picked from commit 73e3a94755)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-08-16 19:29:17 -04:00
dkanada
ccb74610aa Merge pull request #1542 from dmitrylyzo/fix-osd-lock
Fix OSD lock

(cherry picked from commit 04b2763447)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-08-16 19:21:38 -04:00
Joshua M. Boniface
5c37ee6276 Bump version to 10.6.2 2020-08-02 20:26:00 -04:00
dkanada
e25721cb34 Merge pull request #1734 from dmitrylyzo/fix-plugin-configuration
Fix injecting of view with embedded script

(cherry picked from commit 6607718edb)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-08-02 20:24:11 -04:00
dkanada
f11b27fc14 Merge pull request #1701 from MrTimscampi/mobile-logo
Use emblem when loading on mobile

(cherry picked from commit 60d9af0d75)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-08-02 20:15:48 -04:00
dkanada
26d29dc2cb Merge pull request #1686 from Larvitar/master
Fix issue with nowplaying page when item.Album or item.Artists are null.

(cherry picked from commit b1aa18e7e7)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-08-02 20:15:48 -04:00
Anthony Lavado
537b3a3351 Merge pull request #1684 from thornbill/fix-ios-fullscreeen
Fix fullscreen video in iOS Safari

(cherry picked from commit 9b6a90ffdd)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-08-02 20:15:47 -04:00
dkanada
ab58d3c3a1 Merge pull request #1681 from jellyfin/book-paging
Add paging in book player with touch events

(cherry picked from commit cefdde1848)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-08-02 20:15:47 -04:00
dkanada
e7be594b8f Merge pull request #1678 from jellyfin/banner
Fix banner height and use primary image as fallback

(cherry picked from commit 34d0b67f0e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-08-02 20:15:47 -04:00
dkanada
229294aecc Merge pull request #1672 from MrTimscampi/browsers
Adjust target browsers

(cherry picked from commit feab1aca89)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-08-02 20:15:47 -04:00
dkanada
e41c1854d0 Merge pull request #1669 from joshuaboniface/fix-ci-docker
Flip quoting in variable set command

(cherry picked from commit 40b98bb3d4)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-08-02 20:15:47 -04:00
Anthony Lavado
e355fcdac0 Merge pull request #1667 from joshuaboniface/fix-ci-docker
Get and tag with actual release version in CI

(cherry picked from commit 2c53a329e7)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-08-02 20:15:47 -04:00
dkanada
453d225c84 Merge pull request #1665 from joshuaboniface/fix-bump-version
Fix bump_version so it works properly

(cherry picked from commit c8966a7f7d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-08-02 20:15:47 -04:00
dkanada
ec9ab39d26 Merge pull request #1656 from dmitrylyzo/fix-plugin-install
Fix CircleOfDeath on plugin install

(cherry picked from commit 89e584686e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-08-02 20:15:47 -04:00
Joshua M. Boniface
c2d9ca2af9 Fix bad Debian package name in changelog 2020-07-27 18:57:44 -04:00
Joshua M. Boniface
eba30bda75 Bump version to 10.6.1 2020-07-27 18:30:12 -04:00
Anthony Lavado
5c9847468f Merge pull request #1662 from dmitrylyzo/fix-ios-transcode
Add h264 codec profile for TS container

(cherry picked from commit 58198df5ce)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-07-27 18:19:52 -04:00
Joshua M. Boniface
027cb4f744 Merge pull request #1660 from nyanmisaka/edge-chromium
Fix mkv directplay on Edge chromium

(cherry picked from commit 0408ff0867)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-07-27 18:11:21 -04:00
dkanada
37cde45d12 Merge pull request #1641 from dmitrylyzo/fix-event-source-2
Fix event source for inputManager in case of multiple open dialogs

(cherry picked from commit f5e93a18de)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-07-27 18:11:21 -04:00
dkanada
875c4e0882 Merge pull request #1632 from dmitrylyzo/remove-leftovers-1
Remove debug leftovers

(cherry picked from commit 71646b6131)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-07-27 18:11:21 -04:00
dkanada
ee9d651246 Merge pull request #1628 from dmitrylyzo/fix-webos3
Use NodeList instead of HTMLCollection

(cherry picked from commit 5bc2d567ab)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-07-27 18:11:20 -04:00
dkanada
7f133ff8cd Merge pull request #1627 from rom4nik/master
Fix required track count for subtitle selector to appear

(cherry picked from commit 120ce4f0ff)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-07-27 18:11:20 -04:00
dkanada
76f9b2c741 Merge pull request #1625 from MrTimscampi/syncplay-version-check
Add version check for SyncPlay

(cherry picked from commit 03a9e73b3b)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-07-27 18:11:20 -04:00
Bond-009
51768eb119 Merge pull request #1624 from MrTimscampi/apiclient-update
Update apiclient to 1.4.1

(cherry picked from commit 930197ac9e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-07-27 18:11:20 -04:00
dkanada
cdaf03a769 Merge pull request #1620 from MrTimscampi/metadata-more-fix
Restore More button in metadata editor

(cherry picked from commit 995cbdde16)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-07-27 18:11:20 -04:00
dkanada
f1acfda7d2 Merge pull request #1612 from Maxr1998/master
Add support for seeking with milliseconds

(cherry picked from commit 6b45755480)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-07-27 18:11:20 -04:00
dkanada
fef30a0be3 Merge pull request #1595 from jellyfin/error
Fix issue with sync menu and excessive logging

(cherry picked from commit 7c2472785b)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-07-27 18:11:20 -04:00
1096 changed files with 108705 additions and 217910 deletions

View File

@@ -0,0 +1,63 @@
jobs:
- job: Build
displayName: 'Build'
strategy:
matrix:
Development:
BuildConfiguration: development
Production:
BuildConfiguration: production
Standalone:
BuildConfiguration: standalone
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
displayName: 'Install Node'
inputs:
versionSpec: '12.x'
- task: Cache@2
displayName: 'Check Cache'
inputs:
key: 'yarn | yarn.lock'
path: 'node_modules'
cacheHitVar: CACHE_RESTORED
- script: 'yarn install --frozen-lockfile'
displayName: 'Install Dependencies'
condition: ne(variables.CACHE_RESTORED, 'true')
- script: 'yarn build:development'
displayName: 'Build Development'
condition: eq(variables['BuildConfiguration'], 'development')
- script: 'yarn build:production'
displayName: 'Build Production'
condition: eq(variables['BuildConfiguration'], 'production')
- script: 'yarn build:standalone'
displayName: 'Build Standalone'
condition: eq(variables['BuildConfiguration'], 'standalone')
- script: 'test -d dist'
displayName: 'Check Build'
- script: 'mv dist jellyfin-web'
displayName: 'Rename Directory'
- task: ArchiveFiles@2
displayName: 'Archive Directory'
inputs:
rootFolderOrFile: 'jellyfin-web'
includeRootFolder: true
archiveFile: 'jellyfin-web-$(BuildConfiguration)'
- task: PublishPipelineArtifact@1
displayName: 'Publish Release'
inputs:
targetPath: '$(Build.SourcesDirectory)/jellyfin-web-$(BuildConfiguration).zip'
artifactName: 'jellyfin-web-$(BuildConfiguration)'

View File

@@ -0,0 +1,29 @@
jobs:
- job: Lint
displayName: 'Lint'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
displayName: 'Install Node'
inputs:
versionSpec: '12.x'
- task: Cache@2
displayName: 'Check Cache'
inputs:
key: 'yarn | yarn.lock'
path: 'node_modules'
cacheHitVar: CACHE_RESTORED
- script: 'yarn install --frozen-lockfile'
displayName: 'Install Dependencies'
condition: ne(variables.CACHE_RESTORED, 'true')
- script: 'yarn run lint --quiet'
displayName: 'Run ESLint'
- script: 'yarn run stylelint'
displayName: 'Run Stylelint'

View File

@@ -0,0 +1,122 @@
jobs:
- job: BuildPackage
displayName: 'Build Packages'
strategy:
matrix:
CentOS:
BuildConfiguration: centos
Debian:
BuildConfiguration: debian
Fedora:
BuildConfiguration: fedora
Portable:
BuildConfiguration: portable
pool:
vmImage: 'ubuntu-latest'
steps:
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-web-$(BuildConfiguration) deployment'
displayName: 'Build Dockerfile'
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-web-$(BuildConfiguration)'
displayName: 'Run Dockerfile (unstable)'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-web-$(BuildConfiguration)'
displayName: 'Run Dockerfile (stable)'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
- task: PublishPipelineArtifact@1
displayName: 'Publish Release'
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
inputs:
targetPath: '$(Build.SourcesDirectory)/deployment/dist'
artifactName: 'jellyfin-web-$(BuildConfiguration)'
- task: SSH@0
displayName: 'Create target directory on repository server'
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- task: CopyFilesOverSSH@0
displayName: 'Upload artifacts to repository server'
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
inputs:
sshEndpoint: repository
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
contents: '**'
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- job: BuildDocker
displayName: 'Build Docker'
pool:
vmImage: 'ubuntu-latest'
variables:
- name: JellyfinVersion
value: 0.0.0
steps:
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
displayName: Set release version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
- task: Docker@2
displayName: 'Push Unstable Image'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
repository: 'jellyfin/jellyfin-web'
command: buildAndPush
buildContext: '.'
Dockerfile: 'deployment/Dockerfile.docker'
containerRegistry: Docker Hub
tags: |
unstable-$(Build.BuildNumber)
unstable
- task: Docker@2
displayName: 'Push Stable Image'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
inputs:
repository: 'jellyfin/jellyfin-web'
command: buildAndPush
buildContext: '.'
Dockerfile: 'deployment/Dockerfile.docker'
containerRegistry: Docker Hub
tags: |
stable-$(Build.BuildNumber)
$(JellyfinVersion)
- job: CollectArtifacts
displayName: 'Collect Artifacts'
dependsOn:
- BuildPackage
- BuildDocker
condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
pool:
vmImage: 'ubuntu-latest'
steps:
- task: SSH@0
displayName: 'Update Unstable Repository'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable'
- task: SSH@0
displayName: 'Update Stable Repository'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)'

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

@@ -0,0 +1,17 @@
trigger:
batch: true
branches:
include:
- '*'
tags:
include:
- '*'
pr:
branches:
include:
- '*'
jobs:
- template: azure-pipelines-build.yml
- template: azure-pipelines-lint.yml
- template: azure-pipelines-package.yml

1
.copr/Makefile Symbolic link
View File

@@ -0,0 +1 @@
../fedora/Makefile

5
.dependabot/config.yml Normal file
View File

@@ -0,0 +1,5 @@
version: 1
update_configs:
- package_manager: "javascript"
directory: "/"
update_schedule: "weekly"

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

@@ -8,5 +8,5 @@ trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
[*.{json,yaml,yml}]
[json]
indent_size = 2

View File

@@ -1,11 +0,0 @@
{
"ecmaVersion": "es5",
"modules": "false",
"files": "./dist/**/*.js",
"not": [
"./dist/libraries/pdf.worker.js",
"./dist/libraries/worker-bundle.js",
"./dist/libraries/wasm-gen/libarchive.js",
"./dist/serviceworker.js"
]
}

View File

@@ -2,3 +2,4 @@ node_modules
dist
.idea
.vscode
src/libraries

View File

@@ -1,14 +1,9 @@
const restrictedGlobals = require('confusing-browser-globals');
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
'react',
'promise',
'import',
'eslint-comments',
'sonarjs'
'eslint-comments'
],
env: {
node: true,
@@ -16,216 +11,46 @@ module.exports = {
es2017: true,
es2020: true
},
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
impliedStrict: true
}
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
// 'plugin:promise/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:eslint-comments/recommended',
'plugin:compat/recommended',
'plugin:sonarjs/recommended'
'plugin:compat/recommended'
],
rules: {
'array-callback-return': ['error', { 'checkForEach': true }],
'block-spacing': ['error'],
'brace-style': ['error', '1tbs', { 'allowSingleLine': true }],
'brace-style': ['error'],
'comma-dangle': ['error', 'never'],
'comma-spacing': ['error'],
'curly': ['error', 'multi-line', 'consistent'],
'default-case-last': ['error'],
'eol-last': ['error'],
'indent': ['error', 4, { 'SwitchCase': 1 }],
'jsx-quotes': ['error', 'prefer-single'],
'keyword-spacing': ['error'],
'max-statements-per-line': ['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-floating-decimal': ['error'],
'no-lonely-if': ['error'],
'no-multi-spaces': ['error'],
'no-multiple-empty-lines': ['error', { 'max': 1 }],
'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-trailing-spaces': ['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'],
'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': ['fixme', 'hack', 'xxx'] }],
'object-curly-spacing': ['error', 'always'],
'one-var': ['error', 'never'],
'operator-linebreak': ['error', 'before', { overrides: { '?': 'after', ':': 'after', '=': 'after' } }],
'padded-blocks': ['error', 'never'],
'prefer-const': ['error', { 'destructuring': 'all' }],
'prefer-promise-reject-errors': ['warn', { 'allowEmptyReject': true }],
'@typescript-eslint/prefer-for-of': ['error'],
'@typescript-eslint/prefer-optional-chain': ['error'],
'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
'radix': ['error'],
'@typescript-eslint/semi': ['error'],
'semi': ['error'],
'space-before-blocks': ['error'],
'space-infix-ops': 'error',
'yoda': 'error',
'react/jsx-filename-extension': ['error', { 'extensions': ['.jsx', '.tsx'] }],
'react/jsx-no-bind': ['error'],
'react/jsx-no-useless-fragment': ['error'],
'react/jsx-no-constructed-context-values': ['error'],
'react/no-array-index-key': ['error'],
'sonarjs/no-inverted-boolean-check': ['error'],
// TODO: Enable the following rules and fix issues
'sonarjs/cognitive-complexity': ['off'],
'sonarjs/no-duplicate-string': ['off']
},
settings: {
react: {
version: 'detect'
},
'import/parsers': {
'@typescript-eslint/parser': [ '.ts', '.tsx' ]
},
'import/resolver': {
node: {
extensions: [
'.js',
'.ts',
'.jsx',
'.tsx'
],
moduleDirectory: [
'node_modules',
'src'
]
}
},
polyfills: [
// Native Promises Only
'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',
// Temporary while eslint-compat-plugin is buggy
'document.querySelector'
]
'space-infix-ops': 'error'
},
overrides: [
// Config files and development scripts
{
files: [
'./babel.config.js',
'./.eslintrc.js',
'./postcss.config.js',
'./webpack.*.js',
'./scripts/**/*.js'
]
},
// JavaScript source files
{
files: [
'./src/**/*.{js,jsx,ts,tsx}'
'./src/**/*.js'
],
parserOptions: {
project: ['./tsconfig.json']
},
parser: 'babel-eslint',
env: {
node: false,
amd: true,
@@ -245,56 +70,127 @@ module.exports = {
// Dependency globals
'$': 'readonly',
'jQuery': 'readonly',
'requirejs': 'readonly',
// Jellyfin globals
'ApiClient': 'writable',
'Events': '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',
'dfnshelper': 'writable',
'LibraryMenu': 'writable',
'LinkParser': 'writable',
'LiveTvHelpers': 'writable',
'Loading': 'writable',
'MetadataEditor': 'writable',
'ServerNotifications': 'writable',
'TaskButton': 'writable',
'pageClassOn': 'writable',
'pageIdOn': 'writable',
'PlaylistViewer': 'writable',
'UserParentalControlPage': 'writable',
'Windows': 'readonly',
// Build time definitions
__COMMIT_SHA__: 'readonly',
__JF_BUILD_VERSION__: 'readonly',
__PACKAGE_JSON_NAME__: 'readonly',
__PACKAGE_JSON_VERSION__: 'readonly',
__USE_SYSTEM_FONTS__: 'readonly',
__WEBPACK_SERVE__: 'readonly'
'Windows': 'readonly'
},
rules: {
'@typescript-eslint/prefer-string-starts-ends-with': ['error']
}
},
// TypeScript source files
{
files: [
'./src/**/*.{ts,tsx}'
],
extends: [
'eslint:recommended',
'plugin:import/typescript',
'plugin:@typescript-eslint/recommended',
'plugin:eslint-comments/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended'
],
rules: {
'@typescript-eslint/no-floating-promises': ['error'],
'@typescript-eslint/no-unused-vars': ['error'],
'sonarjs/cognitive-complexity': ['error']
// TODO: Fix warnings and remove these rules
'no-redeclare': ['warn'],
'no-unused-vars': ['warn'],
'no-useless-escape': ['warn'],
// TODO: Remove after ES6 migration is complete
'import/no-unresolved': ['off']
},
settings: {
polyfills: [
// Native Promises Only
'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',
// Temporary while eslint-compat-plugin is buggy
'document.querySelector'
]
}
}
]
};
}

5
.github/CODEOWNERS vendored
View File

@@ -1 +1,4 @@
* @jellyfin/web
.ci @dkanada @EraYaN
.github @jellyfin/core
build.sh @joshuaboniface
deployment @joshuaboniface

View File

@@ -1,22 +0,0 @@
---
name: Playback Issue
about: You have playback issues with some files
labels: playback
---
**Describe The Bug**
<!-- A clear and concise description of what the bug is. -->
**Media Information**
<!-- Please paste any ffprobe or MediaInfo logs. -->
**Screenshots**
<!-- Add screenshots from the Playback Data and Media Info. -->
**System (please complete the following information):**
- Platform: [e.g. Linux, Windows, iPhone, Tizen]
- Browser: [e.g. Firefox, Chrome, Safari]
- Jellyfin Version: [e.g. 10.6.0]
**Additional Context**
<!-- Add any other context about the problem here. -->

View File

@@ -1,13 +0,0 @@
---
name: Technical Discussion
about: You want to discuss technical aspects of changes you intend to make
labels: enhancement
---
<!-- Explain the change and the motivations behind it.
For example, if you plan to rely on a new dependency, explain why and what
it brings to the project.
If you plan to make significant changes, go roughly over the steps you intend
to take and how you would divide the change in PRs of a manageable size. -->

View File

@@ -1,9 +0,0 @@
---
name: Meta Issue
about: You want to track a number of other issues as part of a larger project
labels: meta
---
* [ ] Issue 1 [#123]
* [ ] Issue 2 [#456]
* [ ] ...

View File

@@ -1,20 +1,23 @@
---
name: Bug Report
about: You have noticed a general issue or regression, and would like to report it
name: Bug report
about: Create a bug report
title: ''
labels: bug
assignees: ''
---
**Describe The Bug**
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**Steps To Reproduce**
**To Reproduce**
<!-- Steps to reproduce the behavior: -->
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected Behavior**
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Logs**
@@ -24,9 +27,9 @@ labels: bug
<!-- If applicable, add screenshots to help explain your problem. -->
**System (please complete the following information):**
- Platform: [e.g. Linux, Windows, iPhone, Tizen]
- OS: [e.g. Docker, Debian, Windows]
- Browser: [e.g. Firefox, Chrome, Safari]
- Jellyfin Version: [e.g. 10.6.0]
- Jellyfin Version: [e.g. 10.0.1]
**Additional Context**
**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**

View File

@@ -1,8 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"github>jellyfin/.github//renovate-presets/nodejs",
":semanticCommitsDisabled",
":dependencyDashboard"
]
}

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,23 +0,0 @@
name: Automation
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
on:
push:
branches:
- master
pull_request_target:
jobs:
conflicts:
name: Merge conflict labeling
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
steps:
- uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
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,129 +0,0 @@
name: Build
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master, release* ]
pull_request_target:
branches: [ master, release* ]
workflow_dispatch:
jobs:
run-build-prod:
name: Run production build
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup node environment
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: 20
check-latest: true
cache: npm
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run a production build
env:
JELLYFIN_VERSION: ${{ github.event.pull_request.head.sha || 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@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
with:
name: jellyfin-web__prod
path: dist
publish:
name: Deploy to Cloudflare Pages
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
needs:
- run-build-prod
permissions:
contents: read
deployments: write
steps:
- name: Add comment
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
if: ${{ github.event_name == 'pull_request_target' }}
with:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
message: |
## Cloudflare Pages deployment
| **Latest commit** | <code>${{ github.event.pull_request.head.sha || github.sha }}</code> |
|-------------------|:-:|
| **Status** | 🔄 Deploying... |
| **Preview URL** | Not available |
| **Type** | 🔀 Preview |
pr_number: ${{ github.event.pull_request.number }}
comment_tag: CFPages-deployment
mode: recreate
- name: Download workflow artifact
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
with:
name: jellyfin-web__prod
path: dist
- name: Publish to Cloudflare
id: cf
uses: cloudflare/wrangler-action@f84a562284fc78278ff9052435d9526f9c718361 # v3.7.0
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=jellyfin-web --branch=${{
(github.event_name != 'pull_request_target' || github.event.pull_request.head.repo.full_name == github.repository)
&& (github.event.pull_request.head.ref || github.ref_name)
|| format('{0}/{1}', github.event.pull_request.head.repo.full_name, github.event.pull_request.head.ref)
}} --commit-hash=${{ github.event.pull_request.head.sha || github.sha }}
- name: Update status comment (Success)
if: ${{ github.event_name == 'pull_request_target' && success() }}
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
message: |
## Cloudflare Pages deployment
| **Latest commit** | <code>${{ github.event.pull_request.head.sha || github.sha }}</code> |
|-------------------|:-:|
| **Status** | ✅ Deployed! |
| **Preview URL** | ${{ steps.cf.outputs.deployment-url != '' && steps.cf.outputs.deployment-url || 'Not available' }} |
| **Type** | 🔀 Preview |
pr_number: ${{ github.event.pull_request.number }}
comment_tag: CFPages-deployment
mode: recreate
- name: Update status comment (Failure)
if: ${{ github.event_name == 'pull_request_target' && failure() }}
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
message: |
## Cloudflare Pages deployment
| **Latest commit** | <code>${{ github.event.pull_request.head.sha || github.sha }}</code> |
|-------------------|:-:|
| **Status** | ❌ Failure. Check workflow logs for details |
| **Preview URL** | Not available |
| **Type** | 🔀 Preview |
pr_number: ${{ github.event.pull_request.number }}
comment_tag: CFPages-deployment
mode: recreate

View File

@@ -1,34 +0,0 @@
name: CodeQL
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master, release* ]
pull_request:
branches: [ master, release* ]
schedule:
- cron: '30 7 * * 6'
jobs:
codeql:
name: Run CodeQL
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Initialize CodeQL
uses: github/codeql-action/init@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
with:
languages: javascript
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0

View File

@@ -1,36 +0,0 @@
name: Commands
on:
issue_comment:
types:
- created
- edited
jobs:
rebase:
name: Rebase
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
runs-on: ubuntu-latest
steps:
- name: Notify as seen
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Automatic Rebase
uses: cirrus-actions/rebase@b87d48154a87a85666003575337e27b8cd65f691 # 1.8
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
- name: Comment on failure
if: failure()
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
I'm sorry @${{ github.event.comment.user.login }}, I'm afraid I can't do that.

View File

@@ -1,36 +0,0 @@
name: PR suggestions
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.id || github.run_id }}
cancel-in-progress: true
on:
pull_request_target:
branches: [ master, release* ]
jobs:
run-eslint:
name: Run eslint suggestions
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup node environment
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: 20
check-latest: true
cache: npm
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run eslint
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
uses: CatChen/eslint-suggestion-action@bc82950fa97bb3e46d9cca16a8bf2ad3e3c010fc # v4.1.5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,123 +0,0 @@
name: Quality checks
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master, release* ]
pull_request:
branches: [ master, release* ]
jobs:
run-escheck:
name: Run es-check
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup node environment
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: 20
check-latest: true
cache: npm
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run a production build
run: npm run build:production
- name: Run es-check
run: npm run escheck
run-eslint:
name: Run eslint
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup node environment
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: 20
check-latest: true
cache: npm
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run eslint
run: npx eslint --quiet "."
run-stylelint:
name: Run stylelint
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup node environment
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: 20
check-latest: true
cache: npm
- name: Set up stylelint matcher
uses: xt0rted/stylelint-problem-matcher@34db1b874c0452909f0696aedef70b723870a583 # tag=v1
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run stylelint
run: npm run stylelint
run-tsc:
name: Run TypeScript build check
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup node environment
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: 20
check-latest: true
cache: npm
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run tsc
run: npm run build:check
run-test:
name: Run tests
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup node environment
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: 20
check-latest: true
cache: npm
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run test suite
run: npm run test

View File

@@ -1,51 +0,0 @@
name: Stale Check
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
issues:
name: Check issues
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
operations-per-run: 75
days-before-stale: 120
days-before-pr-stale: -1
days-before-close: 21
days-before-pr-close: -1
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-conflicts:
name: Check PRs with merge conflicts
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
operations-per-run: 75
# The merge conflict action will remove the label when updated
remove-stale-when-updated: false
days-before-stale: -1
days-before-close: 90
days-before-issue-close: -1
stale-pr-label: merge conflict
close-pr-message: |-
This PR has been closed due to having unresolved merge conflicts.

View File

@@ -1,52 +0,0 @@
name: Update the Jellyfin SDK
on:
schedule:
- cron: '0 7 * * *'
workflow_dispatch:
concurrency:
group: unstable-sdk-pr
cancel-in-progress: true
jobs:
update:
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
steps:
- name: Check out Git repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: master
token: ${{ secrets.JF_BOT_TOKEN }}
- name: Set up Node.js
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: 20
check-latest: true
cache: npm
- name: Install latest unstable SDK
run: |
npm i --save @jellyfin/sdk@unstable
VERSION=$(jq -r '.dependencies["@jellyfin/sdk"]' package.json)
echo "JF_SDK_VERSION=${VERSION}" >> $GITHUB_ENV
- name: Open a pull request
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
commit-message: Update @jellyfin/sdk to ${{env.JF_SDK_VERSION}}
committer: jellyfin-bot <team@jellyfin.org>
author: jellyfin-bot <team@jellyfin.org>
branch: update-jf-sdk
delete-branch: true
title: Update @jellyfin/sdk to ${{env.JF_SDK_VERSION}}
body: |
**Changes**
Updates to the latest unstable @jellyfin/sdk build
labels: |
dependencies
npm

13
.gitignore vendored
View File

@@ -3,16 +3,9 @@ dist
web
node_modules
# test coverage
coverage
# config
config.json
# ide
.idea
.vs
# vim
*.sw?
.vscode
# log
yarn-error.log

3
.npmrc
View File

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

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

View File

@@ -1,19 +1,18 @@
{
"plugins": [
"stylelint-no-browser-hacks/lib"
],
"stylelint-no-browser-hacks/lib",
],
"rules": {
"at-rule-empty-line-before": [ "always", {
"except": [
except: [
"blockless-after-same-name-blockless",
"first-nested"
"first-nested",
],
"ignore": ["after-comment"]
ignore: ["after-comment"],
} ],
"at-rule-name-case": "lower",
"at-rule-name-space-after": "always-single-line",
"at-rule-no-unknown": true,
"at-rule-no-vendor-prefix": true,
"at-rule-semicolon-newline-after": "always",
"block-closing-brace-empty-line-before": "never",
"block-closing-brace-newline-after": "always",
@@ -27,27 +26,27 @@
"color-hex-length": "short",
"color-no-invalid-hex": true,
"comment-empty-line-before": [ "always", {
"except": ["first-nested"],
"ignore": ["stylelint-commands"]
except: ["first-nested"],
ignore: ["stylelint-commands"],
} ],
"comment-no-empty": true,
"comment-whitespace-inside": "always",
"custom-property-empty-line-before": [ "always", {
"except": [
except: [
"after-custom-property",
"first-nested"
"first-nested",
],
"ignore": [
ignore: [
"after-comment",
"inside-single-line-block"
]
"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"]
ignore: ["consecutive-duplicates-with-different-values"]
}
],
"declaration-block-no-shorthand-property-overrides": true,
@@ -60,6 +59,7 @@
"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",
@@ -78,7 +78,6 @@
"media-feature-colon-space-before": "never",
"media-feature-name-case": "lower",
"media-feature-name-no-unknown": true,
"media-feature-name-no-vendor-prefix": true,
"media-feature-parentheses-space-inside": "never",
"media-feature-range-operator-space-after": "always",
"media-feature-range-operator-space-before": "always",
@@ -105,10 +104,9 @@
]
}
],
"property-no-vendor-prefix": true,
"rule-empty-line-before": [ "always-multi-line", {
"except": ["first-nested"],
"ignore": ["after-comment"]
except: ["first-nested"],
ignore: ["after-comment"],
} ],
"selector-attribute-brackets-space-inside": "never",
"selector-attribute-operator-space-after": "never",
@@ -119,7 +117,6 @@
"selector-list-comma-newline-after": "always",
"selector-list-comma-space-before": "never",
"selector-max-empty-lines": 0,
"selector-no-vendor-prefix": true,
"selector-pseudo-class-case": "lower",
"selector-pseudo-class-no-unknown": true,
"selector-pseudo-class-parentheses-space-inside": "never",
@@ -138,25 +135,9 @@
"string-no-newline": true,
"unit-case": "lower",
"unit-no-unknown": true,
"value-no-vendor-prefix": 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
},
"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
}
}
]
"value-list-max-empty-lines": 0,
}
}

View File

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

View File

@@ -1,7 +0,0 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.format.enable": true,
"editor.formatOnSave": false
}

View File

@@ -1,162 +1,101 @@
# 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)
- [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)
- [Sarab Singh](https://github.com/sarab97)
- [Andrei Oanca](https://github.com/OancaAndrei)
## 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,8 +44,8 @@ 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 1.22.4](https://classic.yarnpkg.com/en/docs/install)
- Gulp-cli
### Getting Started
@@ -56,48 +59,23 @@ Jellyfin Web is the frontend used for most of the clients available for end user
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.
4. Build the client with sourcemaps.
```sh
npm run build:development
yarn build:development
```
## Directory Structure
You can build a nginx compatible version as well.
```
.
└── src
├── apps
│   ├── dashboard # Admin dashboard app layout and routes
│   ├── experimental # New experimental app layout and routes
│   └── stable # Classic (stable) app layout and routes
├── assets # Static assets
├── components # Higher order visual components and React components
├── controllers # Legacy page views and controllers 🧹
├── elements # Basic webcomponents and React wrappers 🧹
├── hooks # Custom React hooks
├── lib # Reusable libraries
│   ├── globalize # Custom localization library
│   ├── legacy # Polyfills for legacy browsers
│   ├── navdrawer # Navigation drawer library for classic layout
│   └── scroller # Content scrolling library
├── plugins # Client plugins
├── scripts # Random assortment of visual components and utilities 🐉
├── strings # Translation files
├── styles # Common app Sass stylesheets
├── themes # CSS themes
├── types # Common TypeScript interfaces/types
└── utils # Utility functions
```
- 🧹 &mdash; Needs cleanup
- 🐉 &mdash; Serious mess (Here be dragons)
```sh
yarn build:standalone
```

View File

@@ -1,22 +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: [
'@babel/plugin-transform-class-properties',
'@babel/plugin-transform-private-methods',
'babel-plugin-dynamic-import-polyfill'
]
};

110
build.sh Executable file
View File

@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# build.sh - Build Jellyfin binary packages
# Part of the Jellyfin Project
set -o errexit
set -o pipefail
usage() {
echo -e "build.sh - Build Jellyfin binary packages"
echo -e "Usage:"
echo -e " $0 -t/--type <BUILD_TYPE> -p/--platform <PLATFORM> [-k/--keep-artifacts] [-l/--list-platforms]"
echo -e "Notes:"
echo -e " * BUILD_TYPE can be one of: [native, docker] and must be specified"
echo -e " * native: Build using the build script in the host OS"
echo -e " * docker: Build using the build script in a standardized Docker container"
echo -e " * PLATFORM can be any platform shown by -l/--list-platforms and must be specified"
echo -e " * If -k/--keep-artifacts is specified, transient artifacts (e.g. Docker containers) will be"
echo -e " retained after the build is finished; the source directory will still be cleaned"
echo -e " * If -l/--list-platforms is specified, all other arguments are ignored; the script will print"
echo -e " the list of supported platforms and exit"
}
list_platforms() {
declare -a platforms
platforms=(
$( find deployment -maxdepth 1 -mindepth 1 -name "build.*" | awk -F'.' '{ $1=""; printf $2; if ($3 != ""){ printf "." $3; }; if ($4 != ""){ printf "." $4; }; print ""; }' | sort )
)
echo -e "Valid platforms:"
echo
for platform in ${platforms[@]}; do
echo -e "* ${platform} : $( grep '^#=' deployment/build.${platform} | sed 's/^#= //' )"
done
}
do_build_native() {
export IS_DOCKER=NO
deployment/build.${PLATFORM}
}
do_build_docker() {
if ! dpkg --print-architecture | grep -q 'amd64'; then
echo "Docker-based builds only support amd64-based cross-building; use a 'native' build instead."
exit 1
fi
if [[ ! -f deployment/Dockerfile.${PLATFORM} ]]; then
echo "Missing Dockerfile for platform ${PLATFORM}"
exit 1
fi
if [[ ${KEEP_ARTIFACTS} == YES ]]; then
docker_args=""
else
docker_args="--rm"
fi
docker build . -t "jellyfin-builder.${PLATFORM}" -f deployment/Dockerfile.${PLATFORM}
mkdir -p ${ARTIFACT_DIR}
docker run $docker_args -v "${SOURCE_DIR}:/jellyfin" -v "${ARTIFACT_DIR}:/dist" "jellyfin-builder.${PLATFORM}"
}
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-t|--type)
BUILD_TYPE="$2"
shift
shift
;;
-p|--platform)
PLATFORM="$2"
shift
shift
;;
-k|--keep-artifacts)
KEEP_ARTIFACTS=YES
shift
;;
-l|--list-platforms)
list_platforms
exit 0
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option $1"
usage
exit 1
;;
esac
done
if [[ -z ${BUILD_TYPE} || -z ${PLATFORM} ]]; then
usage
exit 1
fi
export SOURCE_DIR="$( pwd )"
export ARTIFACT_DIR="${SOURCE_DIR}/../bin/${PLATFORM}"
# Determine build type
case ${BUILD_TYPE} in
native)
do_build_native
;;
docker)
do_build_docker
;;
esac

9
build.yaml Normal file
View File

@@ -0,0 +1,9 @@
---
# We just wrap `build` so this is really it
name: "jellyfin-web"
version: "10.6.4"
packages:
- debian.all
- fedora.all
- centos.all
- portable

View File

@@ -7,7 +7,7 @@ set -o pipefail
set -o xtrace
usage() {
echo -e "bump_version - increase the shared version"
echo -e "bump_version - increase the shared version and generate changelogs"
echo -e ""
echo -e "Usage:"
echo -e " $ bump_version <new_version>"
@@ -18,12 +18,74 @@ if [[ -z $1 ]]; then
exit 1
fi
new_version="$1"
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
shared_version_file="src/components/apphost.js"
build_file="./build.yaml"
# Bump the NPM version
npm --no-git-tag-version --allow-same-version version v${new_version_sed}
new_version="$1"
# Parse the version from shared version file
old_version="$( grep "appVersion" ${shared_version_file} | head -1 | sed -E "s/var appVersion = '([0-9\.]+)';/\1/" | tr -d '[:space:]' )"
echo "Old version in appHost is: $old_version"
# Set the shared version to the specified new_version
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
sed -i "s/${old_version_sed}/${new_version_sed}/g" ${shared_version_file}
old_version="$( grep "version:" ${build_file} | sed -E 's/version: "([0-9\.]+[-a-z0-9]*)"/\1/' )"
echo "Old version in ${build_file}: ${old_version}"
# Set the build.yaml version to the specified new_version
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
sed -i "s/${old_version_sed}/${new_version}/g" ${build_file}
if [[ ${new_version} == *"-"* ]]; then
new_version_deb="$( sed 's/-/~/g' <<<"${new_version}" )"
else
new_version_deb="${new_version}-1"
fi
# Write out a temporary Debian changelog with our new stuff appended and some templated formatting
debian_changelog_file="debian/changelog"
debian_changelog_temp="$( mktemp )"
# Create new temp file with our changelog
echo -e "jellyfin-web (${new_version_deb}) unstable; urgency=medium
* New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v${new_version}
-- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
" >> ${debian_changelog_temp}
cat ${debian_changelog_file} >> ${debian_changelog_temp}
# Move into place
mv ${debian_changelog_temp} ${debian_changelog_file}
# Write out a temporary Yum changelog with our new stuff prepended and some templated formatting
fedora_spec_file="fedora/jellyfin-web.spec"
fedora_changelog_temp="$( mktemp )"
fedora_spec_temp_dir="$( mktemp -d )"
fedora_spec_temp="${fedora_spec_temp_dir}/jellyfin-web.spec.tmp"
# Make a copy of our spec file for hacking
cp ${fedora_spec_file} ${fedora_spec_temp_dir}/
pushd ${fedora_spec_temp_dir}
# Split out the stuff before and after changelog
csplit jellyfin-web.spec "/^%changelog/" # produces xx00 xx01
# Update the version in xx00
sed -i "s/${old_version_sed}/${new_version_sed}/g" xx00
# Remove the header from xx01
sed -i '/^%changelog/d' xx01
# Create new temp file with our changelog
echo -e "%changelog
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v${new_version}" >> ${fedora_changelog_temp}
cat xx01 >> ${fedora_changelog_temp}
# Reassembble
cat xx00 ${fedora_changelog_temp} > ${fedora_spec_temp}
popd
# Move into place
mv ${fedora_spec_temp} ${fedora_spec_file}
# Clean up
rm -rf ${fedora_changelog_temp} ${fedora_spec_temp_dir}
# Stage the changed files for commit
git add .
git status -v
git add ${shared_version_file} ${build_file} ${debian_changelog_file} ${fedora_spec_file}
git status

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

29
debian/changelog vendored Normal file
View File

@@ -0,0 +1,29 @@
jellyfin-web (10.6.4-1) unstable; urgency=medium
* New upstream version 10.6.4; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.6.4
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 30 Aug 2020 16:45:08 -0400
jellyfin-web (10.6.3-1) unstable; urgency=medium
* New upstream version 10.6.3; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.6.3
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 16 Aug 2020 19:48:03 -0400
jellyfin-web (10.6.2-1) unstable; urgency=medium
* New upstream version 10.6.2; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.6.2
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 02 Aug 2020 20:25:58 -0400
jellyfin-web (10.6.1-1) unstable; urgency=medium
* New upstream version 10.6.1; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.6.1
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 27 Jul 2020 18:29:54 -0400
jellyfin-web (10.6.0-1) unstable; urgency=medium
* New upstream version 10.6.0; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.6.0
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 16 Mar 2020 11:15:00 -0400

1
debian/compat vendored Normal file
View File

@@ -0,0 +1 @@
8

16
debian/control vendored Normal file
View File

@@ -0,0 +1,16 @@
Source: jellyfin-web
Section: misc
Priority: optional
Maintainer: Jellyfin Team <team@jellyfin.org>
Build-Depends: debhelper (>= 9),
npm | nodejs
Standards-Version: 3.9.4
Homepage: https://jellyfin.org/
Vcs-Git: https://github.org/jellyfin/jellyfin-web.git
Vcs-Browser: https://github.org/jellyfin/jellyfin-web
Package: jellyfin-web
Recommends: jellyfin-server
Architecture: all
Description: Jellyfin is the Free Software Media System.
This package provides the Jellyfin web client.

28
debian/copyright vendored Normal file
View File

@@ -0,0 +1,28 @@
Format: http://dep.debian.net/deps/dep5
Upstream-Name: jellyfin-web
Source: https://github.com/jellyfin/jellyfin-web
Files: *
Copyright: 2018-2020 Jellyfin Team
License: GPL-3.0
Files: debian/*
Copyright: 2020 Joshua Boniface <joshua@boniface.me>
License: GPL-3.0
License: GPL-3.0
This package is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
.
This package is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
.
On Debian systems, the complete text of the GNU General
Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".

6
debian/gbp.conf vendored Normal file
View File

@@ -0,0 +1,6 @@
[DEFAULT]
pristine-tar = False
cleaner = fakeroot debian/rules clean
[import-orig]
filter = [ ".git*", ".hg*", ".vs*", ".vscode*" ]

1
debian/install vendored Normal file
View File

@@ -0,0 +1 @@
web usr/share/jellyfin/

1
debian/po/POTFILES.in vendored Normal file
View File

@@ -0,0 +1 @@
[type: gettext/rfc822deb] templates

57
debian/po/templates.pot vendored Normal file
View File

@@ -0,0 +1,57 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: jellyfin-server\n"
"Report-Msgid-Bugs-To: jellyfin-server@packages.debian.org\n"
"POT-Creation-Date: 2015-06-12 20:51-0600\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#. Type: note
#. Description
#: ../templates:1001
msgid "Jellyfin permission info:"
msgstr ""
#. Type: note
#. Description
#: ../templates:1001
msgid ""
"Jellyfin by default runs under a user named \"jellyfin\". Please ensure that the "
"user jellyfin has read and write access to any folders you wish to add to your "
"library. Otherwise please run jellyfin under a different user."
msgstr ""
#. Type: string
#. Description
#: ../templates:2001
msgid "Username to run Jellyfin as:"
msgstr ""
#. Type: string
#. Description
#: ../templates:2001
msgid "The user that jellyfin will run as."
msgstr ""
#. Type: note
#. Description
#: ../templates:3001
msgid "Jellyfin still running"
msgstr ""
#. Type: note
#. Description
#: ../templates:3001
msgid "Jellyfin is currently running. Please close it and try again."
msgstr ""

20
debian/rules vendored Executable file
View File

@@ -0,0 +1,20 @@
#! /usr/bin/make -f
export DH_VERBOSE=1
%:
dh $@
# disable "make check"
override_dh_auto_test:
# disable stripping debugging symbols
override_dh_clistrip:
override_dh_auto_build:
npx yarn install
mv $(CURDIR)/dist $(CURDIR)/web
override_dh_auto_clean:
test -d $(CURDIR)/dist && rm -rf '$(CURDIR)/dist' || true
test -d $(CURDIR)/web && rm -rf '$(CURDIR)/web' || true
test -d $(CURDIR)/node_modules && rm -rf '$(CURDIR)/node_modules' || true

1
debian/source/format vendored Normal file
View File

@@ -0,0 +1 @@
1.0

7
debian/source/options vendored Normal file
View File

@@ -0,0 +1,7 @@
tar-ignore='.git*'
tar-ignore='**/.git'
tar-ignore='**/.hg'
tar-ignore='**/.vs'
tar-ignore='**/.vscode'
tar-ignore='deployment'
tar-ignore='*.deb'

View File

@@ -0,0 +1,29 @@
FROM centos:7
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV IS_DOCKER=YES
# Prepare CentOS environment
RUN yum update -y \
&& yum install -y epel-release \
&& yum install -y @buildsys-build rpmdevtools git yum-plugins-core nodejs-yarn autoconf automake glibc-devel
# Install recent NodeJS and Yarn
RUN curl -fSsLo /etc/yum.repos.d/yarn.repo https://dl.yarnpkg.com/rpm/yarn.repo \
&& rpm -i https://rpm.nodesource.com/pub_10.x/el/7/x86_64/nodesource-release-el7-1.noarch.rpm \
&& yum install -y yarn
# Link to build script
RUN ln -sf ${SOURCE_DIR}/deployment/build.centos /build.sh
VOLUME ${SOURCE_DIR}
VOLUME ${ARTIFACT_DIR}
ENTRYPOINT ["/build.sh"]

View File

@@ -0,0 +1,27 @@
FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update \
&& apt-get install -y debhelper mmv npm git
# Prepare Yarn
RUN npm install -g yarn
# Link to build script
RUN ln -sf ${SOURCE_DIR}/deployment/build.debian /build.sh
VOLUME ${SOURCE_DIR}
VOLUME ${ARTIFACT_DIR}
ENTRYPOINT ["/build.sh"]

View File

@@ -0,0 +1,11 @@
FROM node:alpine
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin-web
RUN apk add autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python
WORKDIR ${SOURCE_DIR}
COPY . .
RUN yarn install && mv dist ${ARTIFACT_DIR}

View File

@@ -0,0 +1,23 @@
FROM fedora:31
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV IS_DOCKER=YES
# Prepare Fedora environment
RUN dnf update -y \
&& dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core nodejs-yarn autoconf automake glibc-devel
# Link to build script
RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora /build.sh
VOLUME ${SOURCE_DIR}
VOLUME ${ARTIFACT_DIR}
ENTRYPOINT ["/build.sh"]

View File

@@ -0,0 +1,26 @@
FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update \
&& apt-get install -y mmv npm git
# Prepare Yarn
RUN npm install -g yarn
# Link to build script
RUN ln -sf ${SOURCE_DIR}/deployment/build.portable /build.sh
VOLUME ${SOURCE_DIR}
VOLUME ${ARTIFACT_DIR}
ENTRYPOINT ["/build.sh"]

41
deployment/build.centos Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
set -o errexit
set -o xtrace
# move to source directory
pushd ${SOURCE_DIR}
cp -a yarn.lock /tmp/yarn.lock
# modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd fedora
PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
sed -i "s/Version:.*/Version: ${BUILD_ID}/" jellyfin-web.spec
sed -i "/%changelog/q" jellyfin-web.spec
cat <<EOF >>jellyfin-web.spec
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
- Jellyfin Web unstable build ${BUILD_ID} for merged PR #${PR_ID}
EOF
popd
fi
# build rpm
make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS
rpmbuild --rebuild -bb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
# move the artifacts
mv /root/rpmbuild/RPMS/noarch/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/
if [[ ${IS_DOCKER} == YES ]]; then
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
fi
rm -f fedora/jellyfin*.tar.gz
cp -a /tmp/yarn.lock yarn.lock
popd

39
deployment/build.debian Executable file
View File

@@ -0,0 +1,39 @@
#!/bin/bash
set -o errexit
set -o xtrace
# move to source directory
pushd ${SOURCE_DIR}
cp -a yarn.lock /tmp/yarn.lock
# modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd debian
PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
cat <<EOF >changelog
jellyfin-web (${BUILD_ID}-unstable) unstable; urgency=medium
* Jellyfin Web unstable build ${BUILD_ID} for merged PR #${PR_ID}
-- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
EOF
popd
fi
# build deb
dpkg-buildpackage -us -uc --pre-clean --post-clean
mkdir -p ${ARTIFACT_DIR}
mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}
cp -a /tmp/yarn.lock yarn.lock
if [[ ${IS_DOCKER} == YES ]]; then
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
fi
popd

41
deployment/build.fedora Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
set -o errexit
set -o xtrace
# move to source directory
pushd ${SOURCE_DIR}
cp -a yarn.lock /tmp/yarn.lock
# modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd fedora
PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
sed -i "s/Version:.*/Version: ${BUILD_ID}/" jellyfin-web.spec
sed -i "/%changelog/q" jellyfin-web.spec
cat <<EOF >>jellyfin-web.spec
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
- Jellyfin Web unstable build ${BUILD_ID} for merged PR #${PR_ID}
EOF
popd
fi
# build rpm
make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS
rpmbuild -rb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
# move the artifacts
mv /root/rpmbuild/RPMS/noarch/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
fi
rm -f fedora/jellyfin*.tar.gz
cp -a /tmp/yarn.lock yarn.lock
popd

30
deployment/build.portable Executable file
View File

@@ -0,0 +1,30 @@
#!/bin/bash
set -o errexit
set -o xtrace
# move to source directory
pushd ${SOURCE_DIR}
# get version
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
version="${BUILD_ID}"
else
version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
fi
# build archives
npx yarn install
mv dist jellyfin-web_${version}
tar -czf jellyfin-web_${version}_portable.tar.gz jellyfin-web_${version}
rm -rf dist
# move the artifacts
mkdir -p ${ARTIFACT_DIR}
mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
fi
popd

21
fedora/Makefile Normal file
View File

@@ -0,0 +1,21 @@
VERSION := $(shell sed -ne '/^Version:/s/.* *//p' fedora/jellyfin-web.spec)
srpm:
cd fedora/; \
SOURCE_DIR=.. \
WORKDIR="$${PWD}"; \
tar \
--transform "s,^\.,jellyfin-web-$(VERSION)," \
--exclude='.git*' \
--exclude='**/.git' \
--exclude='**/.hg' \
--exclude='deployment' \
--exclude='*.deb' \
--exclude='*.rpm' \
--exclude='jellyfin-web-$(VERSION).tar.gz' \
-czf "jellyfin-web-$(VERSION).tar.gz" \
-C $${SOURCE_DIR} ./
cd fedora/; \
rpmbuild -bs jellyfin-web.spec \
--define "_sourcedir $$PWD/" \
--define "_srcrpmdir $(outdir)"

53
fedora/jellyfin-web.spec Normal file
View File

@@ -0,0 +1,53 @@
%global debug_package %{nil}
Name: jellyfin-web
Version: 10.6.4
Release: 1%{?dist}
Summary: The Free Software Media System web client
License: GPLv3
URL: https://jellyfin.org
# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%{version}.tar.gz`
Source0: jellyfin-web-%{version}.tar.gz
%if 0%{?centos}
BuildRequires: yarn
# sadly the yarn RPM at https://dl.yarnpkg.com/rpm/ uses git but doesn't Requires: it
BuildRequires: git
%else
BuildRequires: nodejs-yarn
%endif
BuildArch: noarch
# Disable Automatic Dependency Processing
AutoReqProv: no
%description
Jellyfin is a free software media system that puts you in control of managing and streaming your media.
%prep
%autosetup -n jellyfin-web-%{version} -b 0
%build
%install
yarn install
%{__mkdir} -p %{buildroot}%{_datadir}
mv dist %{buildroot}%{_datadir}/jellyfin-web
%{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/jellyfin/LICENSE
%files
%attr(755,root,root) %{_datadir}/jellyfin-web
%{_datadir}/licenses/jellyfin/LICENSE
%changelog
* Sun Aug 30 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.6.4; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.6.4
* Sun Aug 16 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.6.3; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.6.3
* Sun Aug 02 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.6.2; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.6.2
* Mon Jul 27 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.6.1; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.6.1
* Mon Mar 23 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
- Forthcoming stable release

204
gulpfile.js Normal file
View File

@@ -0,0 +1,204 @@
const { src, dest, series, parallel, watch } = require('gulp');
const browserSync = require('browser-sync').create();
const del = require('del');
const babel = require('gulp-babel');
const concat = require('gulp-concat');
const terser = require('gulp-terser');
const htmlmin = require('gulp-htmlmin');
const imagemin = require('gulp-imagemin');
const sourcemaps = require('gulp-sourcemaps');
const mode = require('gulp-mode')({
modes: ['development', 'production'],
default: 'development',
verbose: false
});
const stream = require('webpack-stream');
const inject = require('gulp-inject');
const postcss = require('gulp-postcss');
const sass = require('gulp-sass');
const gulpif = require('gulp-if');
const lazypipe = require('lazypipe');
sass.compiler = require('node-sass');
let config;
if (mode.production()) {
config = require('./webpack.prod.js');
} else {
config = require('./webpack.dev.js');
}
const options = {
javascript: {
query: ['src/**/*.js', '!src/bundle.js', '!src/standalone.js', '!src/scripts/apploader.js']
},
apploader: {
query: ['src/standalone.js', 'src/scripts/apploader.js']
},
css: {
query: ['src/**/*.css', 'src/**/*.scss']
},
html: {
query: ['src/**/*.html', '!src/index.html']
},
images: {
query: ['src/**/*.png', 'src/**/*.jpg', 'src/**/*.gif', 'src/**/*.svg']
},
copy: {
query: ['src/**/*.json', 'src/**/*.ico', 'src/**/*.mp3']
},
injectBundle: {
query: 'src/index.html'
}
};
function serve() {
browserSync.init({
server: {
baseDir: './dist'
},
port: 8080
});
const events = ['add', 'change'];
watch(options.javascript.query).on('all', function (event, path) {
if (events.includes(event)) {
javascript(path);
}
});
watch(options.apploader.query, apploader(true));
watch('src/bundle.js', webpack);
watch(options.css.query).on('all', function (event, path) {
if (events.includes(event)) {
css(path);
}
});
watch(options.html.query).on('all', function (event, path) {
if (events.includes(event)) {
html(path);
}
});
watch(options.images.query).on('all', function (event, path) {
if (events.includes(event)) {
images(path);
}
});
watch(options.copy.query).on('all', function (event, path) {
if (events.includes(event)) {
copy(path);
}
});
watch(options.injectBundle.query, injectBundle);
}
function clean() {
return del(['dist/']);
}
const pipelineJavascript = lazypipe()
.pipe(function () {
return mode.development(sourcemaps.init({ loadMaps: true }));
})
.pipe(function () {
return babel({
presets: [
['@babel/preset-env']
]
});
})
.pipe(function () {
return terser({
keep_fnames: true,
mangle: false
});
})
.pipe(function () {
return mode.development(sourcemaps.write('.'));
});
function javascript(query) {
return src(typeof query !== 'function' ? query : options.javascript.query, { base: './src/' })
.pipe(pipelineJavascript())
.pipe(dest('dist/'))
.pipe(browserSync.stream());
}
function apploader(standalone) {
function task() {
return src(options.apploader.query, { base: './src/' })
.pipe(gulpif(standalone, concat('scripts/apploader.js')))
.pipe(pipelineJavascript())
.pipe(dest('dist/'))
.pipe(browserSync.stream());
}
task.displayName = 'apploader';
return task;
}
function webpack() {
return stream(config)
.pipe(dest('dist/'))
.pipe(browserSync.stream());
}
function css(query) {
return src(typeof query !== 'function' ? query : options.css.query, { base: './src/' })
.pipe(mode.development(sourcemaps.init({ loadMaps: true })))
.pipe(sass().on('error', sass.logError))
.pipe(postcss())
.pipe(mode.development(sourcemaps.write('.')))
.pipe(dest('dist/'))
.pipe(browserSync.stream());
}
function html(query) {
return src(typeof query !== 'function' ? query : options.html.query, { base: './src/' })
.pipe(mode.production(htmlmin({ collapseWhitespace: true })))
.pipe(dest('dist/'))
.pipe(browserSync.stream());
}
function images(query) {
return src(typeof query !== 'function' ? query : options.images.query, { base: './src/' })
.pipe(mode.production(imagemin()))
.pipe(dest('dist/'))
.pipe(browserSync.stream());
}
function copy(query) {
return src(typeof query !== 'function' ? query : options.copy.query, { base: './src/' })
.pipe(dest('dist/'))
.pipe(browserSync.stream());
}
function injectBundle() {
return src(options.injectBundle.query, { base: './src/' })
.pipe(inject(
src(['src/scripts/apploader.js'], { read: false }, { base: './src/' }), {
relative: true,
transform: function (filepath) {
return `<script src="${filepath}" defer></script>`;
}
}
))
.pipe(dest('dist/'))
.pipe(browserSync.stream());
}
function build(standalone) {
return series(clean, parallel(javascript, apploader(standalone), webpack, css, html, images, copy));
}
exports.default = series(build(false), injectBundle);
exports.standalone = series(build(true), injectBundle);
exports.serve = series(exports.standalone, serve);

41598
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,130 +1,150 @@
{
"name": "jellyfin-web",
"version": "10.10.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.24.9",
"@babel/plugin-transform-class-properties": "7.24.7",
"@babel/plugin-transform-modules-umd": "7.24.7",
"@babel/plugin-transform-private-methods": "7.24.7",
"@babel/preset-env": "7.24.8",
"@babel/preset-react": "7.24.7",
"@types/dompurify": "3.0.5",
"@types/escape-html": "1.0.4",
"@types/loadable__component": "5.13.9",
"@types/lodash-es": "4.17.12",
"@types/markdown-it": "14.1.2",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/sortablejs": "1.15.8",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"@uupaa/dynamic-import-polyfill": "1.0.2",
"@vitest/coverage-v8": "2.0.5",
"autoprefixer": "10.4.19",
"babel-loader": "9.1.3",
"babel-plugin-dynamic-import-polyfill": "1.0.0",
"clean-webpack-plugin": "4.0.0",
"confusing-browser-globals": "1.0.11",
"copy-webpack-plugin": "12.0.2",
"cross-env": "7.0.3",
"css-loader": "7.1.2",
"cssnano": "7.0.4",
"es-check": "7.2.1",
"eslint": "8.57.0",
"eslint-plugin-compat": "4.2.0",
"eslint-plugin-eslint-comments": "3.2.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jsx-a11y": "6.9.0",
"eslint-plugin-react": "7.35.0",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-sonarjs": "0.25.1",
"expose-loader": "5.0.0",
"fork-ts-checker-webpack-plugin": "9.0.2",
"html-loader": "5.1.0",
"html-webpack-plugin": "5.6.0",
"jsdom": "24.1.1",
"mini-css-extract-plugin": "2.9.0",
"postcss": "8.4.40",
"postcss-loader": "8.1.1",
"postcss-preset-env": "9.6.0",
"postcss-scss": "4.0.9",
"sass": "1.77.8",
"sass-loader": "15.0.0",
"source-map-loader": "5.0.0",
"speed-measure-webpack-plugin": "1.5.0",
"style-loader": "4.0.0",
"stylelint": "15.11.0",
"stylelint-config-rational-order": "0.1.2",
"stylelint-no-browser-hacks": "1.3.0",
"stylelint-order": "6.0.4",
"stylelint-scss": "5.3.2",
"ts-loader": "9.5.1",
"typescript": "5.5.4",
"vitest": "2.0.5",
"webpack": "5.93.0",
"webpack-bundle-analyzer": "4.10.2",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.0.4",
"webpack-merge": "6.0.1",
"worker-loader": "3.0.8"
"@babel/core": "^7.10.3",
"@babel/plugin-proposal-class-properties": "^7.10.1",
"@babel/plugin-proposal-private-methods": "^7.10.1",
"@babel/plugin-transform-modules-amd": "^7.9.6",
"@babel/polyfill": "^7.8.7",
"@babel/preset-env": "^7.10.3",
"autoprefixer": "^9.8.5",
"babel-eslint": "^11.0.0-beta.2",
"babel-loader": "^8.0.6",
"browser-sync": "^2.26.7",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.6.0",
"cssnano": "^4.1.10",
"del": "^5.1.0",
"eslint": "^6.8.0",
"eslint-plugin-compat": "^3.5.1",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-import": "^2.21.2",
"eslint-plugin-promise": "^4.2.1",
"file-loader": "^6.0.0",
"gulp": "^4.0.2",
"gulp-babel": "^8.0.0",
"gulp-cli": "^2.3.0",
"gulp-concat": "^2.6.1",
"gulp-htmlmin": "^5.0.1",
"gulp-if": "^3.0.0",
"gulp-imagemin": "^7.1.0",
"gulp-inject": "^5.0.5",
"gulp-mode": "^1.0.2",
"gulp-postcss": "^8.0.0",
"gulp-sass": "^4.0.2",
"gulp-sourcemaps": "^2.6.5",
"gulp-terser": "^1.2.0",
"html-webpack-plugin": "^4.3.0",
"lazypipe": "^1.0.2",
"node-sass": "^4.13.1",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"style-loader": "^1.1.3",
"stylelint": "^13.6.1",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-no-browser-hacks": "^1.2.1",
"stylelint-order": "^4.1.0",
"webpack": "^4.41.5",
"webpack-merge": "^4.2.2",
"webpack-stream": "^5.2.1"
},
"dependencies": {
"@emotion/react": "11.13.0",
"@emotion/styled": "11.13.0",
"@fontsource/noto-sans": "5.0.22",
"@fontsource/noto-sans-hk": "5.0.20",
"@fontsource/noto-sans-jp": "5.0.19",
"@fontsource/noto-sans-kr": "5.0.19",
"@fontsource/noto-sans-sc": "5.0.20",
"@fontsource/noto-sans-tc": "5.0.20",
"@jellyfin/libass-wasm": "4.2.2",
"@jellyfin/sdk": "0.0.0-unstable.202408050429",
"@mui/icons-material": "5.15.19",
"@mui/material": "5.15.19",
"@mui/x-data-grid": "7.6.1",
"@react-hook/resize-observer": "2.0.1",
"@tanstack/react-query": "5.51.11",
"@tanstack/react-query-devtools": "5.51.11",
"@types/react-lazy-load-image-component": "1.6.4",
"abortcontroller-polyfill": "1.7.5",
"blurhash": "2.0.5",
"alameda": "^1.4.0",
"blurhash": "^1.1.3",
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
"classnames": "2.5.1",
"core-js": "3.37.1",
"date-fns": "2.30.0",
"dompurify": "3.0.1",
"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.5.13",
"intersection-observer": "0.12.2",
"jellyfin-apiclient": "1.11.0",
"jquery": "3.7.1",
"jstree": "3.3.16",
"libarchive.js": "1.3.0",
"lodash-es": "4.17.21",
"markdown-it": "14.1.0",
"material-design-icons-iconfont": "6.7.0",
"native-promise-only": "0.8.1",
"pdfjs-dist": "3.11.174",
"react": "18.3.1",
"react-blurhash": "0.3.0",
"react-dom": "18.3.1",
"react-lazy-load-image-component": "1.6.2",
"react-router-dom": "6.25.1",
"resize-observer-polyfill": "1.5.1",
"screenfull": "6.0.2",
"sortablejs": "1.15.2",
"swiper": "11.1.7",
"usehooks-ts": "3.1.0",
"webcomponents.js": "0.7.24",
"whatwg-fetch": "3.6.20"
"core-js": "^3.6.5",
"date-fns": "^2.14.0",
"epubjs": "^0.3.85",
"fast-text-encoding": "^1.0.3",
"flv.js": "^1.5.0",
"headroom.js": "^0.11.0",
"hls.js": "^0.14.0",
"howler": "^2.2.0",
"intersection-observer": "^0.11.0",
"jellyfin-apiclient": "^1.4.1",
"jellyfin-noto": "https://github.com/jellyfin/jellyfin-noto",
"jquery": "^3.5.1",
"jstree": "^3.3.10",
"libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv",
"material-design-icons-iconfont": "^5.0.1",
"native-promise-only": "^0.8.0-a",
"page": "^1.11.6",
"query-string": "^6.13.1",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.0.2",
"shaka-player": "^2.5.13",
"sortablejs": "^1.10.2",
"swiper": "^5.4.5",
"webcomponents.js": "^0.7.24",
"whatwg-fetch": "^3.2.0"
},
"babel": {
"presets": [
"@babel/preset-env"
],
"overrides": [
{
"test": [
"src/components/accessSchedule/accessSchedule.js",
"src/components/actionSheet/actionSheet.js",
"src/components/autoFocuser.js",
"src/components/cardbuilder/cardBuilder.js",
"src/components/cardbuilder/chaptercardbuilder.js",
"src/components/cardbuilder/peoplecardbuilder.js",
"src/components/images/imageLoader.js",
"src/components/indicators/indicators.js",
"src/components/lazyLoader/lazyLoaderIntersectionObserver.js",
"src/components/playback/brightnessosd.js",
"src/components/playback/mediasession.js",
"src/components/playback/nowplayinghelper.js",
"src/components/playback/playbackorientation.js",
"src/components/playback/playerSelectionMenu.js",
"src/components/playback/playersettingsmenu.js",
"src/components/playback/playmethodhelper.js",
"src/components/playback/remotecontrolautoplay.js",
"src/components/playback/volumeosd.js",
"src/components/playmenu.js",
"src/components/sanatizefilename.js",
"src/components/scrollManager.js",
"src/components/syncPlay/groupSelectionMenu.js",
"src/components/syncPlay/playbackPermissionManager.js",
"src/components/syncPlay/syncPlayManager.js",
"src/components/syncPlay/timeSyncManager.js",
"src/controllers/dashboard/logs.js",
"src/controllers/dashboard/plugins/repositories.js",
"src/controllers/user/display.js",
"src/controllers/user/home.js",
"src/controllers/user/playback.js",
"src/controllers/user/subtitles.js",
"src/plugins/bookPlayer/plugin.js",
"src/plugins/bookPlayer/tableOfContents.js",
"src/plugins/photoPlayer/plugin.js",
"src/scripts/deleteHelper.js",
"src/scripts/dfnshelper.js",
"src/scripts/dom.js",
"src/scripts/fileDownloader.js",
"src/scripts/filesystem.js",
"src/scripts/imagehelper.js",
"src/scripts/inputManager.js",
"src/plugins/backdropScreensaver/plugin.js",
"src/components/filterdialog/filterdialog.js",
"src/components/fetchhelper.js",
"src/scripts/keyboardNavigation.js",
"src/scripts/settings/appSettings.js",
"src/scripts/settings/userSettings.js",
"src/scripts/settings/webSettings.js"
],
"plugins": [
"@babel/plugin-transform-modules-amd",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-private-methods"
]
}
]
},
"browserslist": [
"last 2 Firefox versions",
@@ -143,21 +163,12 @@
"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",
"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",
"yarn": "YARN NO LONGER USED - use npm instead."
"serve": "gulp serve --development",
"prepare": "gulp --production",
"build:development": "gulp --development",
"build:production": "gulp --production",
"build:standalone": "gulp standalone --development",
"lint": "eslint \".\"",
"stylelint": "stylelint \"src/**/*.css\""
}
}

View File

@@ -7,8 +7,8 @@ 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 }),
postcssPresetEnv({browsers: packageConfig.browserslist}),
autoprefixer({overrideBrowserslist: packageConfig.browserslist}),
cssnano()
]
});

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

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

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,22 +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 { WebConfigProvider } from 'hooks/useWebConfig';
import { queryClient } from 'utils/query/queryClient';
import RootAppRouter from 'RootAppRouter';
const RootApp = () => (
<QueryClientProvider client={queryClient}>
<ApiProvider>
<WebConfigProvider>
<RootAppRouter />
</WebConfigProvider>
</ApiProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
export default RootApp;

View File

@@ -1,54 +0,0 @@
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 AppHeader from 'components/AppHeader';
import Backdrop from 'components/Backdrop';
import { createRouterHistory } from 'components/router/routerHistory';
import UserThemeProvider from 'themes/UserThemeProvider';
const layoutMode = localStorage.getItem('layout');
const isExperimentalLayout = layoutMode === 'experimental';
const router = createHashRouter([
{
element: <RootAppLayout />,
children: [
...(isExperimentalLayout ? EXPERIMENTAL_APP_ROUTES : STABLE_APP_ROUTES),
...DASHBOARD_APP_ROUTES
]
}
]);
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 (
<UserThemeProvider>
<Backdrop />
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
<Outlet />
</UserThemeProvider>
);
}

49
src/addplugin.html Normal file
View File

@@ -0,0 +1,49 @@
<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" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://docs.jellyfin.org/general/server/plugins/index.html">${Help}</a>
</div>
<p id="overview" style="font-style: italic;"></p>
<p id="description"></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>
</form>
</div>
</div>
<br />
<div class="readOnlyContent">
<div is="emby-collapse" title="${HeaderDeveloperInfo}">
<div class="collapseContent">
<p id="developer"></p>
</div>
</div>
<div is="emby-collapse" title="${HeaderRevisionHistory}">
<div class="collapseContent">
<div id="revisionHistory"></div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -8,7 +8,7 @@
</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>

361
src/apiclient.d.ts vendored
View File

@@ -1,361 +0,0 @@
// TODO: Move to jellyfin-apiclient
/* 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 { ConnectionState } from './utils/jellyfin-apiclient/ConnectionState';
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>;
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;
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 */

View File

@@ -1,9 +1,9 @@
<div id="apiKeysPage" data-role="page" class="page type-interior advancedConfigurationPage fullWidthContent" data-title="${HeaderApiKeys}">
<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="${Add}">
<button is="emby-button" type="button" class="fab btnNewKey submit" style="margin-left:1em;" title="${ButtonAdd}">
<span class="material-icons add" aria-hidden="true"></span>
</button>
</div>

View File

@@ -1,94 +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 React, { FC, useCallback, useEffect, useState } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import AppBody from 'components/AppBody';
import AppToolbar from 'components/toolbar/AppToolbar';
import ElevationScroll from 'components/ElevationScroll';
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
import { useApi } from 'hooks/useApi';
import AppTabs from './components/AppTabs';
import AppDrawer from './components/drawer/AppDrawer';
import { DASHBOARD_APP_PATHS } from './routes/routes';
import './AppOverrides.scss';
const DRAWERLESS_PATHS = [ DASHBOARD_APP_PATHS.MetadataManager ];
export const Component: FC = () => {
const [ isDrawerActive, setIsDrawerActive ] = useState(false);
const location = useLocation();
const { user } = useApi();
const isMediumScreen = useMediaQuery((t: Theme) => t.breakpoints.up('md'));
const isDrawerAvailable = Boolean(user)
&& !DRAWERLESS_PATHS.some(path => location.pathname.startsWith(`/${path}`));
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 (
<Box sx={{ display: 'flex' }}>
<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
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onToggleDrawer}
>
<AppTabs isDrawerOpen={isDrawerOpen} />
</AppToolbar>
</AppBar>
</ElevationScroll>
{
isDrawerAvailable && (
<AppDrawer
open={isDrawerOpen}
onClose={onToggleDrawer}
onOpen={onToggleDrawer}
/>
)
}
<Box
component='main'
sx={{
width: '100%',
flexGrow: 1
}}
>
<AppBody>
<Outlet />
</AppBody>
</Box>
</Box>
);
};

View File

@@ -1,34 +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 {
@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;
}
}
}

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 'types/eventType';
import Events, { type Event } from 'utils/events';
interface AppTabsParams {
isDrawerOpen: boolean
}
interface TabDefinition {
href: string
name: string
}
const handleResize = debounce(() => window.dispatchEvent(new Event('resize')), 100);
const AppTabs: FC<AppTabsParams> = ({
isDrawerOpen
}) => {
const documentRef = useRef<Document>(document);
const [ activeIndex, setActiveIndex ] = useState(0);
const [ tabs, setTabs ] = useState<TabDefinition[]>();
const isBigScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
const onTabsUpdate = useCallback((
_e: Event,
_newView?: string,
newIndex: number | undefined = 0,
newTabs?: TabDefinition[]
) => {
setActiveIndex(newIndex);
if (!isEqual(tabs, newTabs)) {
setTabs(newTabs);
}
}, [ tabs ]);
useEffect(() => {
const doc = documentRef.current;
if (doc) Events.on(doc, EventType.SET_TABS, onTabsUpdate);
return () => {
if (doc) Events.off(doc, EventType.SET_TABS, onTabsUpdate);
};
}, [ onTabsUpdate ]);
// HACK: Force resizing to workaround upstream bug with tab resizing
// https://github.com/mui/material-ui/issues/24011
useEffect(() => {
handleResize();
}, [ isDrawerOpen ]);
if (!tabs?.length) return null;
return (
<Tabs
value={activeIndex}
sx={{
width: '100%',
flexShrink: {
xs: 0,
lg: 'unset'
},
order: {
xs: 100,
lg: 'unset'
}
}}
variant={isBigScreen ? 'standard' : 'scrollable'}
centered={isBigScreen}
>
{
tabs.map(({ href, name }, index) => (
<Tab
key={`tab-${name}`}
label={name}
data-tab-index={`${index}`}
component={Link}
to={href}
/>
))
}
</Tabs>
);
};
export default AppTabs;

View File

@@ -1,34 +0,0 @@
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
import Chip from '@mui/material/Chip';
import React from 'react';
import globalize from 'lib/globalize';
const LogLevelChip = ({ level }: { level: LogLevel }) => {
let color: 'info' | 'warning' | 'error' | undefined;
switch (level) {
case LogLevel.Information:
color = 'info';
break;
case LogLevel.Warning:
color = 'warning';
break;
case LogLevel.Error:
case LogLevel.Critical:
color = 'error';
break;
}
const levelText = globalize.translate(`LogLevel.${level}`);
return (
<Chip
size='small'
color={color}
label={levelText}
title={levelText}
/>
);
};
export default LogLevelChip;

View File

@@ -1,64 +0,0 @@
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
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, { FC, useCallback, useState } from 'react';
const OverviewCell: FC<ActivityLogEntry> = ({ Overview, ShortOverview }) => {
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,17 +0,0 @@
import React, { type RefAttributes } from 'react';
import { Link } from 'react-router-dom';
import { GridActionsCellItem, type GridActionsCellItemProps } from '@mui/x-data-grid';
type GridActionsCellLinkProps = { to: string } & GridActionsCellItemProps & RefAttributes<HTMLButtonElement>;
/**
* Link component to use in mui's data-grid action column due to a current bug with passing props to custom link components.
* @see https://github.com/mui/mui-x/issues/4654
*/
const GridActionsCellLink = ({ to, ...props }: GridActionsCellLinkProps) => (
<Link to={to}>
<GridActionsCellItem {...props} />
</Link>
);
export default GridActionsCellLink;

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,61 +0,0 @@
import Article from '@mui/icons-material/Article';
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/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,42 +0,0 @@
import { Devices, Analytics } from '@mui/icons-material';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import React 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,42 +0,0 @@
import { Dvr, LiveTv } from '@mui/icons-material';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import React 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/recordings'>
<ListItemIcon>
<Dvr />
</ListItemIcon>
<ListItemText primary={globalize.translate('HeaderDVR')} />
</ListItemLink>
</ListItem>
</List>
);
};
export default LiveTvDrawerSection;

View File

@@ -1,71 +0,0 @@
import Extension from '@mui/icons-material/Extension';
import Folder from '@mui/icons-material/Folder';
import Public from '@mui/icons-material/Public';
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' ]}
excludePaths={pagesInfo?.map(p => `/${Dashboard.getPluginUrl(p.Name)}`)}
>
<ListItemIcon>
<Extension />
</ListItemIcon>
<ListItemText primary={globalize.translate('TabMyPlugins')} />
</ListItemLink>
<ListItemLink
to='/dashboard/plugins/catalog'
includePaths={[ '/dashboard/plugins/repositories' ]}
>
<ListItemIcon>
<Public />
</ListItemIcon>
<ListItemText primary={globalize.translate('TabCatalog')} />
</ListItemLink>
{pagesInfo?.map(pageInfo => (
<ListItemLink
key={pageInfo.PluginId}
to={`/${Dashboard.getPluginUrl(pageInfo.Name)}`}
>
<ListItemIcon>
{/* TODO: Support different icons? */}
<Folder />
</ListItemIcon>
<ListItemText primary={pageInfo.DisplayName} />
</ListItemLink>
))}
</List>
);
};
export default PluginDrawerSection;

View File

@@ -1,121 +0,0 @@
import { Dashboard, ExpandLess, ExpandMore, LibraryAdd, People, PlayCircle, Settings } from '@mui/icons-material';
import Collapse from '@mui/material/Collapse';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import React from 'react';
import { useLocation } from 'react-router-dom';
import ListItemLink from 'components/ListItemLink';
import globalize from 'lib/globalize';
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 = LIBRARY_PATHS.includes(location.pathname);
const isPlaybackSectionOpen = PLAYBACK_PATHS.includes(location.pathname);
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>
<ListItem disablePadding>
<ListItemLink to='/dashboard/users'>
<ListItemIcon>
<People />
</ListItemIcon>
<ListItemText primary={globalize.translate('HeaderUsers')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/libraries' selected={false}>
<ListItemIcon>
<LibraryAdd />
</ListItemIcon>
<ListItemText primary={globalize.translate('HeaderLibraries')} />
{isLibrarySectionOpen ? <ExpandLess /> : <ExpandMore />}
</ListItemLink>
</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('Metadata')} />
</ListItemLink>
<ListItemLink to='/dashboard/libraries/nfo' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('TabNfoSettings')} />
</ListItemLink>
</List>
</Collapse>
<ListItem disablePadding>
<ListItemLink to='/dashboard/playback/transcoding' selected={false}>
<ListItemIcon>
<PlayCircle />
</ListItemIcon>
<ListItemText primary={globalize.translate('TitlePlayback')} />
{isPlaybackSectionOpen ? <ExpandLess /> : <ExpandMore />}
</ListItemLink>
</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,21 +0,0 @@
import type { ConfigurationPageInfo } from '@jellyfin/sdk/lib/generated-client/models/configuration-page-info';
export const findBestConfigurationPage = (
configurationPages: ConfigurationPageInfo[],
pluginId: string
) => {
// Find candidates matching the plugin id
const candidates = configurationPages.filter(c => c.PluginId === pluginId);
// If none are found, return undefined
if (candidates.length === 0) return;
// If only one is found, return it
if (candidates.length === 1) return candidates[0];
// Prefer the first candidate with the EnableInMainMenu flag for consistency
const menuCandidate = candidates.find(c => !!c.EnableInMainMenu);
if (menuCandidate) return menuCandidate;
// Fallback to the first match
return candidates[0];
};

View File

@@ -1,25 +0,0 @@
import type { PluginInfo } from '@jellyfin/sdk/lib/generated-client/models/plugin-info';
import { PluginStatus } from '@jellyfin/sdk/lib/generated-client/models/plugin-status';
/**
* HACK: The Plugins API is returning garbage data in some cases,
* so we need to try to find the "best" match if multiple exist.
*/
export const findBestPluginInfo = (
pluginId: string,
plugins?: PluginInfo[]
) => {
if (!plugins) return;
// Find all plugin entries with a matching ID
const matches = plugins.filter(p => p.Id === pluginId);
// Get the first match (or undefined if none)
const firstMatch = matches?.[0];
if (matches.length > 1) {
return matches.find(p => p.Status === PluginStatus.Disabled) // Disabled entries take priority
|| matches.find(p => p.Status === PluginStatus.Restart) // Then entries specifying restart is needed
|| firstMatch; // Fallback to the first match
}
return firstMatch;
};

View File

@@ -1,5 +0,0 @@
export enum QueryKey {
ConfigurationPages = 'ConfigurationPages',
PackageInfo = 'PackageInfo',
Plugins = 'Plugins'
}

View File

@@ -1,40 +0,0 @@
import type { Api } from '@jellyfin/sdk';
import type { DashboardApiGetConfigurationPagesRequest } from '@jellyfin/sdk/lib/generated-client/api/dashboard-api';
import { getDashboardApi } from '@jellyfin/sdk/lib/utils/api/dashboard-api';
import { queryOptions, useQuery } from '@tanstack/react-query';
import type { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
import { QueryKey } from './queryKey';
const fetchConfigurationPages = async (
api?: Api,
params?: DashboardApiGetConfigurationPagesRequest,
options?: AxiosRequestConfig
) => {
if (!api) {
console.warn('[fetchConfigurationPages] No API instance available');
return [];
}
const response = await getDashboardApi(api)
.getConfigurationPages(params, options);
return response.data;
};
const getConfigurationPagesQuery = (
api?: Api,
params?: DashboardApiGetConfigurationPagesRequest
) => queryOptions({
queryKey: [ QueryKey.ConfigurationPages, params?.enableInMainMenu ],
queryFn: ({ signal }) => fetchConfigurationPages(api, params, { signal }),
enabled: !!api
});
export const useConfigurationPages = (
params?: DashboardApiGetConfigurationPagesRequest
) => {
const { api } = useApi();
return useQuery(getConfigurationPagesQuery(api, params));
};

View File

@@ -1,24 +0,0 @@
import type { PluginsApiDisablePluginRequest } from '@jellyfin/sdk/lib/generated-client/api/plugins-api';
import { getPluginsApi } from '@jellyfin/sdk/lib/utils/api/plugins-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QueryKey } from './queryKey';
export const useDisablePlugin = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: PluginsApiDisablePluginRequest) => (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getPluginsApi(api!)
.disablePlugin(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QueryKey.Plugins ]
});
}
});
};

View File

@@ -1,24 +0,0 @@
import type { PluginsApiEnablePluginRequest } from '@jellyfin/sdk/lib/generated-client/api/plugins-api';
import { getPluginsApi } from '@jellyfin/sdk/lib/utils/api/plugins-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QueryKey } from './queryKey';
export const useEnablePlugin = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: PluginsApiEnablePluginRequest) => (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getPluginsApi(api!)
.enablePlugin(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QueryKey.Plugins ]
});
}
});
};

View File

@@ -1,27 +0,0 @@
import type { PackageApiInstallPackageRequest } from '@jellyfin/sdk/lib/generated-client/api/package-api';
import { getPackageApi } from '@jellyfin/sdk/lib/utils/api/package-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QueryKey } from './queryKey';
export const useInstallPackage = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: PackageApiInstallPackageRequest) => (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getPackageApi(api!)
.installPackage(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QueryKey.ConfigurationPages ]
});
void queryClient.invalidateQueries({
queryKey: [ QueryKey.Plugins ]
});
}
});
};

View File

@@ -1,47 +0,0 @@
import { queryOptions, useQuery } from '@tanstack/react-query';
import type { Api } from '@jellyfin/sdk';
import type { PackageApiGetPackageInfoRequest } from '@jellyfin/sdk/lib/generated-client/api/package-api';
import { getPackageApi } from '@jellyfin/sdk/lib/utils/api/package-api';
import type { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
import { QueryKey } from './queryKey';
const fetchPackageInfo = async (
api?: Api,
params?: PackageApiGetPackageInfoRequest,
options?: AxiosRequestConfig
) => {
if (!api) {
console.warn('[fetchPackageInfo] No API instance available');
return;
}
if (!params) {
console.warn('[fetchPackageInfo] Missing request params');
return;
}
const response = await getPackageApi(api)
.getPackageInfo(params, options);
return response.data;
};
const getPackageInfoQuery = (
api?: Api,
params?: PackageApiGetPackageInfoRequest
) => queryOptions({
// Don't retry since requests for plugins not available in repos fail
retry: false,
queryKey: [ QueryKey.PackageInfo, params?.name, params?.assemblyGuid ],
queryFn: ({ signal }) => fetchPackageInfo(api, params, { signal }),
enabled: !!api && !!params?.name
});
export const usePackageInfo = (
params?: PackageApiGetPackageInfoRequest
) => {
const { api } = useApi();
return useQuery(getPackageInfoQuery(api, params));
};

View File

@@ -1,36 +0,0 @@
import type { Api } from '@jellyfin/sdk';
import { getPluginsApi } from '@jellyfin/sdk/lib/utils/api/plugins-api';
import { queryOptions, useQuery } from '@tanstack/react-query';
import type { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
import { QueryKey } from './queryKey';
const fetchPlugins = async (
api?: Api,
options?: AxiosRequestConfig
) => {
if (!api) {
console.warn('[fetchPlugins] No API instance available');
return [];
}
const response = await getPluginsApi(api)
.getPlugins(options);
return response.data;
};
const getPluginsQuery = (
api?: Api
) => queryOptions({
queryKey: [ QueryKey.Plugins ],
queryFn: ({ signal }) => fetchPlugins(api, { signal }),
enabled: !!api
});
export const usePlugins = () => {
const { api } = useApi();
return useQuery(getPluginsQuery(api));
};

View File

@@ -1,27 +0,0 @@
import type { PluginsApiUninstallPluginByVersionRequest } from '@jellyfin/sdk/lib/generated-client/api/plugins-api';
import { getPluginsApi } from '@jellyfin/sdk/lib/utils/api/plugins-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QueryKey } from './queryKey';
export const useUninstallPlugin = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: PluginsApiUninstallPluginByVersionRequest) => (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getPluginsApi(api!)
.uninstallPluginByVersion(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QueryKey.Plugins ]
});
void queryClient.invalidateQueries({
queryKey: [ QueryKey.ConfigurationPages ]
});
}
});
};

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