Compare commits
2 Commits
renovate/m
...
sortmedias
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8c2956cc2 | ||
|
|
38dd4c5f43 |
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
|
||||
4
.eslintignore
Normal file
4
.eslintignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
.idea
|
||||
.vscode
|
||||
300
.eslintrc.js
Normal file
300
.eslintrc.js
Normal file
@@ -0,0 +1,300 @@
|
||||
const restrictedGlobals = require('confusing-browser-globals');
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
'react',
|
||||
'import',
|
||||
'eslint-comments',
|
||||
'sonarjs'
|
||||
],
|
||||
env: {
|
||||
node: true,
|
||||
es6: true,
|
||||
es2017: true,
|
||||
es2020: true
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:import/errors',
|
||||
'plugin:eslint-comments/recommended',
|
||||
'plugin:compat/recommended',
|
||||
'plugin:sonarjs/recommended'
|
||||
],
|
||||
rules: {
|
||||
'array-callback-return': ['error', { 'checkForEach': true }],
|
||||
'block-spacing': ['error'],
|
||||
'brace-style': ['error', '1tbs', { 'allowSingleLine': true }],
|
||||
'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'],
|
||||
'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'
|
||||
]
|
||||
},
|
||||
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}'
|
||||
],
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json']
|
||||
},
|
||||
env: {
|
||||
node: false,
|
||||
amd: true,
|
||||
browser: true,
|
||||
es6: true,
|
||||
es2017: true,
|
||||
es2020: true
|
||||
},
|
||||
globals: {
|
||||
// Browser globals
|
||||
'MediaMetadata': 'readonly',
|
||||
// Tizen globals
|
||||
'tizen': 'readonly',
|
||||
'webapis': 'readonly',
|
||||
// WebOS globals
|
||||
'webOS': 'readonly',
|
||||
// Dependency globals
|
||||
'$': 'readonly',
|
||||
'jQuery': 'readonly',
|
||||
// Jellyfin globals
|
||||
'ApiClient': 'writable',
|
||||
'Events': 'writable',
|
||||
'chrome': 'writable',
|
||||
'DlnaProfilePage': 'writable',
|
||||
'DashboardPage': 'writable',
|
||||
'Emby': 'readonly',
|
||||
'Globalize': 'writable',
|
||||
'Hls': 'writable',
|
||||
'LibraryMenu': 'writable',
|
||||
'LinkParser': 'writable',
|
||||
'LiveTvHelpers': 'writable',
|
||||
'Loading': 'writable',
|
||||
'MetadataEditor': 'writable',
|
||||
'ServerNotifications': 'writable',
|
||||
'TaskButton': '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'
|
||||
},
|
||||
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']
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -1,5 +1 @@
|
||||
* @jellyfin/web
|
||||
# Joshua must review all changes to bump_version
|
||||
bump_version @joshuaboniface
|
||||
# Core must approve all changes within the repo config
|
||||
.github/ @jellyfin/core
|
||||
|
||||
32
.github/ISSUE_TEMPLATE/1-bug-report.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/1-bug-report.md
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: You have noticed a general issue or regression, and would like to report it
|
||||
labels: bug
|
||||
---
|
||||
|
||||
**Describe The Bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**Steps To Reproduce**
|
||||
<!-- Steps to reproduce the behavior: -->
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected Behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Logs**
|
||||
<!-- Please paste any log errors. -->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**System (please complete the following information):**
|
||||
- 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. -->
|
||||
122
.github/ISSUE_TEMPLATE/1-bug-report.yml
vendored
122
.github/ISSUE_TEMPLATE/1-bug-report.yml
vendored
@@ -1,122 +0,0 @@
|
||||
name: Bug Report
|
||||
description: You have noticed a general issue or regression, and would like to report it
|
||||
labels:
|
||||
- bug
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: before-posting
|
||||
attributes:
|
||||
label: "This issue respects the following points:"
|
||||
description: All conditions are **required**.
|
||||
options:
|
||||
- label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin-web/issues?q=is%3Aissue) _(I've searched it)_.
|
||||
required: true
|
||||
- label: I agree to follow Jellyfin's [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct).
|
||||
required: true
|
||||
- label: This report addresses only a single issue; If you encounter multiple issues, kindly create separate reports for each one.
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Bug information
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: |
|
||||
A clear and concise description of the bug.
|
||||
You can also attach screenshots or screen recordings here to help explain your issue.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
description: |
|
||||
Steps to reproduce the behavior:
|
||||
placeholder: |
|
||||
1. Go to …
|
||||
2. Click on …
|
||||
3. Scroll down to …
|
||||
4. See error / the app crashes
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: behaviour
|
||||
attributes:
|
||||
label: Expected/Actual behaviour
|
||||
description: |
|
||||
Describe the behavior you were expecting versus what actually occurred.
|
||||
placeholder: |
|
||||
I expected the app to... However, the actual behavior was that...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: |
|
||||
Please paste any log errors.
|
||||
placeholder: Paste logs…
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Environment
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Server
|
||||
You will find these values in your Admin Dashboard
|
||||
- type: input
|
||||
id: server-version
|
||||
attributes:
|
||||
label: Server version
|
||||
placeholder: 10.10.2
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: web-version
|
||||
attributes:
|
||||
label: Web version
|
||||
placeholder: 10.10.2
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: build-version
|
||||
attributes:
|
||||
label: Build version
|
||||
placeholder: 10.10.2
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Client
|
||||
Information about the device you are seeing the issue on
|
||||
- type: input
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Specify the operating system or device where the issue occurs. If relevant, include details like version or model.
|
||||
placeholder: e.g. Linux, Windows, iPhone, Tizen
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: browser
|
||||
attributes:
|
||||
label: Browser
|
||||
description: Indicate which browser you're using when encountering the issue. If possible, mention the browser version as well.
|
||||
placeholder: e.g. Firefox, Chrome, Safari
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Additional
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Include any relevant details, resources, or screenshots that might help in understanding or implementing the request.
|
||||
placeholder: Add any additional context here.
|
||||
validations:
|
||||
required: false
|
||||
22
.github/ISSUE_TEMPLATE/2-playback-issue.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/2-playback-issue.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
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. -->
|
||||
145
.github/ISSUE_TEMPLATE/2-playback-issue.yml
vendored
145
.github/ISSUE_TEMPLATE/2-playback-issue.yml
vendored
@@ -1,145 +0,0 @@
|
||||
name: Playback Issue
|
||||
description: Create a bug report related to media playback
|
||||
labels:
|
||||
- bug
|
||||
- playback
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: before-posting
|
||||
attributes:
|
||||
label: "This issue respects the following points:"
|
||||
description: All conditions are **required**.
|
||||
options:
|
||||
- label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin-web/issues?q=is%3Aissue) _(I've searched it)_.
|
||||
required: true
|
||||
- label: I agree to follow Jellyfin's [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct).
|
||||
required: true
|
||||
- label: This report addresses only a single issue; If you encounter multiple issues, kindly create separate reports for each one.
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Bug information
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: |
|
||||
A clear and concise description of the bug.
|
||||
You can also attach screenshots or screen recordings here to help explain your issue.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
description: |
|
||||
Steps to reproduce the behavior:
|
||||
placeholder: |
|
||||
1. Go to …
|
||||
2. Click on …
|
||||
3. Scroll down to …
|
||||
4. See error / the app crashes
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: behaviour
|
||||
attributes:
|
||||
label: Expected/Actual behaviour
|
||||
description: |
|
||||
Describe the behavior you were expecting versus what actually occurred.
|
||||
placeholder: |
|
||||
I expected the app to... However, the actual behavior was that...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: mediainfo
|
||||
attributes:
|
||||
label: Media info of the file
|
||||
description: |
|
||||
Please share the media information for the file causing issues. You can use a variety of tools to retrieve this information.
|
||||
- Use ffprobe (`ffprobe ./file.mp4`)
|
||||
- Copy the media info from the web interface
|
||||
placeholder: Paste media info…
|
||||
render: shell
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Logs
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: |
|
||||
Please paste your logs here if applicable.
|
||||
placeholder: Paste logs…
|
||||
- type: textarea
|
||||
id: logs-ffmpeg
|
||||
attributes:
|
||||
label: FFmpeg logs
|
||||
description: |
|
||||
Please paste your FFmpeg logs here if available. You can find these in your servers dashboard under "logs".
|
||||
placeholder: Paste logs…
|
||||
render: shell
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Environment
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Server
|
||||
You will find these values in your Admin Dashboard
|
||||
- type: input
|
||||
id: server-version
|
||||
attributes:
|
||||
label: Server version
|
||||
placeholder: 10.10.2
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: web-version
|
||||
attributes:
|
||||
label: Web version
|
||||
placeholder: 10.10.2
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: build-version
|
||||
attributes:
|
||||
label: Build version
|
||||
placeholder: 10.10.2
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Client
|
||||
Information about the device you are seeing the issue on
|
||||
- type: input
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Specify the operating system or device where the issue occurs. If relevant, include details like version or model.
|
||||
placeholder: e.g. Linux, Windows, iPhone, Tizen
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: browser
|
||||
attributes:
|
||||
label: Browser
|
||||
description: Indicate which browser you're using when encountering the issue. If possible, mention the browser version as well.
|
||||
placeholder: e.g. Firefox, Chrome, Safari
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Additional
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Include any relevant details, resources, or screenshots that might help in understanding or implementing the request.
|
||||
placeholder: Add any additional context here.
|
||||
validations:
|
||||
required: false
|
||||
13
.github/ISSUE_TEMPLATE/3-technical-discussion.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/3-technical-discussion.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
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. -->
|
||||
9
.github/ISSUE_TEMPLATE/4-meta-issue.md
vendored
Normal file
9
.github/ISSUE_TEMPLATE/4-meta-issue.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
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]
|
||||
* [ ] ...
|
||||
22
.github/renovate.json
vendored
22
.github/renovate.json
vendored
@@ -2,27 +2,7 @@
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"github>jellyfin/.github//renovate-presets/nodejs",
|
||||
":semanticCommitsDisabled",
|
||||
":dependencyDashboard"
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": false
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackageNames": [ "@jellyfin/sdk" ],
|
||||
"followTag": "unstable",
|
||||
"minimumReleaseAge": null,
|
||||
"prPriority": 5,
|
||||
"schedule": [ "after 7:00 am" ]
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [ "dompurify" ],
|
||||
"matchUpdateTypes": [ "major" ],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [ "hls.js" ],
|
||||
"prPriority": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
15
.github/workflows/__automation.yml
vendored
15
.github/workflows/__automation.yml
vendored
@@ -1,15 +0,0 @@
|
||||
name: Automation 🎛️
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
conflicts:
|
||||
name: Merge conflict labeling 🏷️
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
|
||||
with:
|
||||
dirtyLabel: 'merge conflict'
|
||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
||||
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
||||
40
.github/workflows/__codeql.yml
vendored
40
.github/workflows/__codeql.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: GitHub CodeQL 🔬
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
commit:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze ${{ matrix.language }} 🔬
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language:
|
||||
- javascript-typescript
|
||||
|
||||
steps:
|
||||
- name: Checkout repository ⬇️
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
ref: ${{ inputs.commit }}
|
||||
show-progress: false
|
||||
|
||||
- name: Initialize CodeQL 🛠️
|
||||
uses: github/codeql-action/init@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3.31.4
|
||||
with:
|
||||
queries: security-and-quality
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild 📦
|
||||
uses: github/codeql-action/autobuild@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3.31.4
|
||||
|
||||
- name: Perform CodeQL Analysis 🧪
|
||||
uses: github/codeql-action/analyze@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3.31.4
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
59
.github/workflows/__deploy.yml
vendored
59
.github/workflows/__deploy.yml
vendored
@@ -1,59 +0,0 @@
|
||||
name: Deploy 🏗️
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
branch:
|
||||
required: true
|
||||
type: string
|
||||
commit:
|
||||
required: false
|
||||
type: string
|
||||
comment:
|
||||
required: false
|
||||
type: boolean
|
||||
artifact_name:
|
||||
required: false
|
||||
type: string
|
||||
default: frontend
|
||||
|
||||
jobs:
|
||||
cf-pages:
|
||||
name: CloudFlare Pages 📃
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: ${{ inputs.branch == 'master' && 'Production' || 'Preview' }}
|
||||
url: ${{ steps.cf.outputs.deployment-url }}
|
||||
outputs:
|
||||
url: ${{ steps.cf.outputs.deployment-url }}
|
||||
|
||||
steps:
|
||||
- name: Download workflow artifact ⬇️
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}
|
||||
path: dist
|
||||
|
||||
- name: Publish to Cloudflare Pages 📃
|
||||
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
|
||||
id: cf
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy dist --project-name=jellyfin-web --branch=${{ inputs.branch }}
|
||||
|
||||
compose-comment:
|
||||
name: Compose and push comment 📝
|
||||
# Always run so the comment is composed for the workflow summary
|
||||
if: ${{ always() }}
|
||||
uses: ./.github/workflows/__job_messages.yml
|
||||
secrets: inherit
|
||||
needs:
|
||||
- cf-pages
|
||||
|
||||
with:
|
||||
branch: ${{ inputs.branch }}
|
||||
commit: ${{ inputs.commit }}
|
||||
preview_url: ${{ needs.cf-pages.outputs.url }}
|
||||
in_progress: false
|
||||
comment: ${{ inputs.comment }}
|
||||
65
.github/workflows/__job_messages.yml
vendored
65
.github/workflows/__job_messages.yml
vendored
@@ -1,65 +0,0 @@
|
||||
name: Job messages ⚙️
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
branch:
|
||||
required: false
|
||||
type: string
|
||||
commit:
|
||||
required: true
|
||||
type: string
|
||||
preview_url:
|
||||
required: false
|
||||
type: string
|
||||
in_progress:
|
||||
required: true
|
||||
type: boolean
|
||||
comment:
|
||||
required: false
|
||||
type: boolean
|
||||
marker:
|
||||
description: Hidden marker to detect PR comments composed by the bot
|
||||
required: false
|
||||
type: string
|
||||
default: "CFPages-deployment"
|
||||
|
||||
|
||||
jobs:
|
||||
cf_pages_msg:
|
||||
name: CloudFlare Pages deployment 📃🚀
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Compose message 📃
|
||||
if: ${{ always() }}
|
||||
id: compose
|
||||
env:
|
||||
COMMIT: ${{ inputs.commit }}
|
||||
PREVIEW_URL: ${{ inputs.preview_url != '' && (inputs.branch != 'master' && inputs.preview_url || format('https://jellyfin-web.pages.dev ({0})', inputs.preview_url)) || 'Not available' }}
|
||||
DEPLOY_STATUS: ${{ inputs.in_progress && '🔄 Deploying...' || (inputs.preview_url != '' && '✅ Deployed!' || '❌ Failure. Check workflow logs for details') }}
|
||||
DEPLOYMENT_TYPE: ${{ inputs.branch != 'master' && '🔀 Preview' || '⚙️ Production' }}
|
||||
WORKFLOW_RUN: ${{ !inputs.in_progress && format('**[View build logs](https://github.com/{0}/actions/runs/{1})**', github.repository, github.run_id) || '' }}
|
||||
# EOF is needed for multiline environment variables in a GitHub Actions context
|
||||
run: |
|
||||
echo "## Cloudflare Pages deployment" > $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Latest commit** | <code>${COMMIT::7}</code> |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------------------------- |:----------------------------: |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Status** | $DEPLOY_STATUS |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Preview URL** | $PREVIEW_URL |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Type** | $DEPLOYMENT_TYPE |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "$WORKFLOW_RUN" >> $GITHUB_STEP_SUMMARY
|
||||
COMPOSED_MSG=$(cat $GITHUB_STEP_SUMMARY)
|
||||
echo "msg<<EOF" >> $GITHUB_ENV
|
||||
echo "$COMPOSED_MSG" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Push comment to Pull Request 🔼
|
||||
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1
|
||||
if: ${{ inputs.comment && steps.compose.conclusion == 'success' }}
|
||||
with:
|
||||
github-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
message: ${{ env.msg }}
|
||||
comment-tag: ${{ inputs.marker }}
|
||||
45
.github/workflows/__package.yml
vendored
45
.github/workflows/__package.yml
vendored
@@ -1,45 +0,0 @@
|
||||
name: Packaging 📦
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
commit:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
run-build-prod:
|
||||
name: Run production build 🏗️
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
ref: ${{ inputs.commit || github.sha }}
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
check-latest: true
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run a production build
|
||||
env:
|
||||
JELLYFIN_VERSION: ${{ inputs.commit || github.sha }}
|
||||
run: npm run build:production
|
||||
|
||||
- name: Update config.json for testing
|
||||
run: |
|
||||
jq '.multiserver=true | .servers=["https://demo.jellyfin.org/unstable"]' dist/config.json > dist/config.tmp.json
|
||||
mv dist/config.tmp.json dist/config.json
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: frontend
|
||||
path: dist
|
||||
61
.github/workflows/__quality_checks.yml
vendored
61
.github/workflows/__quality_checks.yml
vendored
@@ -1,61 +0,0 @@
|
||||
name: Quality checks 👌🧪
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
commit:
|
||||
required: true
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
name: Vulnerable dependencies 🔎
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
ref: ${{ inputs.commit }}
|
||||
show-progress: false
|
||||
|
||||
- name: Scan
|
||||
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
|
||||
with:
|
||||
## Workaround from https://github.com/actions/dependency-review-action/issues/456
|
||||
## TODO: Remove when necessary
|
||||
base-ref: ${{ github.event.pull_request.base.sha || 'master' }}
|
||||
head-ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
quality:
|
||||
name: Run ${{ matrix.command }} 🕵️♂️
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
command:
|
||||
- build:es-check
|
||||
- lint
|
||||
- stylelint
|
||||
- build:check
|
||||
- test
|
||||
|
||||
steps:
|
||||
- name: Checkout ⬇️
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
ref: ${{ inputs.commit }}
|
||||
show-progress: false
|
||||
|
||||
- name: Setup node environment ⚙️
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
check-latest: true
|
||||
|
||||
- name: Install dependencies 📦
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run ${{ matrix.command }} ⚙️
|
||||
run: npm run ${{ matrix.command }}
|
||||
23
.github/workflows/automation.yml
vendored
Normal file
23
.github/workflows/automation.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
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 }}
|
||||
129
.github/workflows/build.yml
vendored
Normal file
129
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
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
|
||||
34
.github/workflows/codeql.yml
vendored
Normal file
34
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
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
|
||||
36
.github/workflows/commands.yml
vendored
Normal file
36
.github/workflows/commands.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
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.
|
||||
36
.github/workflows/pr-suggestions.yml
vendored
Normal file
36
.github/workflows/pr-suggestions.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
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 }}
|
||||
100
.github/workflows/pull_request.yml
vendored
100
.github/workflows/pull_request.yml
vendored
@@ -1,100 +0,0 @@
|
||||
name: Pull Request 📥
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- master
|
||||
- release*
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
push-comment:
|
||||
name: Create comments ✍️
|
||||
if: ${{ always() && !cancelled() && github.repository == 'jellyfin/jellyfin-web' }}
|
||||
uses: ./.github/workflows/__job_messages.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha }}
|
||||
in_progress: true
|
||||
comment: true
|
||||
|
||||
build:
|
||||
name: Build 🏗️
|
||||
if: ${{ always() && !cancelled() }}
|
||||
uses: ./.github/workflows/__package.yml
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
automation:
|
||||
name: Automation 🎛️
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
uses: ./.github/workflows/__automation.yml
|
||||
secrets: inherit
|
||||
|
||||
quality_checks:
|
||||
name: Quality checks 👌🧪
|
||||
if: ${{ always() && !cancelled() }}
|
||||
uses: ./.github/workflows/__quality_checks.yml
|
||||
permissions: {}
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
codeql:
|
||||
name: GitHub CodeQL 🔬
|
||||
if: ${{ always() && !cancelled() }}
|
||||
uses: ./.github/workflows/__codeql.yml
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
deploy:
|
||||
name: Deploy 🚀
|
||||
uses: ./.github/workflows/__deploy.yml
|
||||
if: ${{ always() && !cancelled() && needs.build.result == 'success' && github.repository == 'jellyfin/jellyfin-web' }}
|
||||
needs:
|
||||
- push-comment
|
||||
- build
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
secrets: inherit
|
||||
with:
|
||||
# If the PR is from the master branch of a fork, append the fork's name to the branch name
|
||||
branch: ${{ github.event.pull_request.head.repo.full_name != github.repository && github.event.pull_request.head.ref == 'master' && format('{0}/{1}', github.event.pull_request.head.repo.full_name, github.event.pull_request.head.ref) || github.event.pull_request.head.ref }}
|
||||
comment: true
|
||||
commit: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
run-eslint:
|
||||
name: Run eslint suggestions
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
check-latest: true
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run eslint
|
||||
uses: CatChen/eslint-suggestion-action@4ee415529307a8ca0260b4a3775484802523e5af # v4.1.19
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
58
.github/workflows/push.yml
vendored
58
.github/workflows/push.yml
vendored
@@ -1,58 +0,0 @@
|
||||
name: Push & Release 🌍
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- release*
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
|
||||
jobs:
|
||||
automation:
|
||||
name: Automation 🎛️
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
uses: ./.github/workflows/__automation.yml
|
||||
secrets: inherit
|
||||
|
||||
main:
|
||||
name: 'Unstable release 🚀⚠️'
|
||||
uses: ./.github/workflows/__package.yml
|
||||
with:
|
||||
commit: ${{ github.sha }}
|
||||
|
||||
quality_checks:
|
||||
name: Quality checks 👌🧪
|
||||
if: ${{ always() && !cancelled() }}
|
||||
uses: ./.github/workflows/__quality_checks.yml
|
||||
permissions: {}
|
||||
with:
|
||||
commit: ${{ github.sha }}
|
||||
|
||||
codeql:
|
||||
name: GitHub CodeQL 🔬
|
||||
uses: ./.github/workflows/__codeql.yml
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
with:
|
||||
commit: ${{ github.sha }}
|
||||
|
||||
deploy:
|
||||
name: Deploy 🚀
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
uses: ./.github/workflows/__deploy.yml
|
||||
needs:
|
||||
- main
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
secrets: inherit
|
||||
with:
|
||||
branch: ${{ github.ref_name }}
|
||||
comment: false
|
||||
123
.github/workflows/quality.yml
vendored
Normal file
123
.github/workflows/quality.yml
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
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
|
||||
@@ -1,26 +1,28 @@
|
||||
name: Scheduled tasks 🕑
|
||||
name: Stale Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
issues:
|
||||
name: Check stale issues and PRs
|
||||
name: Check issues
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
|
||||
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
operations-per-run: 75
|
||||
# Issues receive a stale warning after 120 days and close after an additional 21 days
|
||||
days-before-stale: 120
|
||||
days-before-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: |-
|
||||
@@ -29,10 +31,21 @@ jobs:
|
||||
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
|
||||
|
||||
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://jellyfin.org/contact).
|
||||
# PRs are closed after having unresolved merge conflicts for 90 days
|
||||
days-before-pr-stale: 0
|
||||
days-before-pr-close: 90
|
||||
only-pr-labels: merge conflict
|
||||
stale-pr-label: stale
|
||||
|
||||
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.
|
||||
52
.github/workflows/update-sdk.yml
vendored
Normal file
52
.github/workflows/update-sdk.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: Update the Jellyfin SDK
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 7 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: unstable-sdk-pr
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@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
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -16,8 +16,3 @@ config.json
|
||||
# vim
|
||||
*.sw?
|
||||
|
||||
# direnv
|
||||
.direnv/
|
||||
|
||||
# environment related
|
||||
.envrc
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@stylistic/stylelint-plugin",
|
||||
"stylelint-no-browser-hacks/lib"
|
||||
],
|
||||
"rules": {
|
||||
@@ -11,20 +10,20 @@
|
||||
],
|
||||
"ignore": ["after-comment"]
|
||||
} ],
|
||||
"@stylistic/at-rule-name-case": "lower",
|
||||
"@stylistic/at-rule-name-space-after": "always-single-line",
|
||||
"at-rule-name-case": "lower",
|
||||
"at-rule-name-space-after": "always-single-line",
|
||||
"at-rule-no-unknown": true,
|
||||
"at-rule-no-vendor-prefix": true,
|
||||
"@stylistic/at-rule-semicolon-newline-after": "always",
|
||||
"@stylistic/block-closing-brace-empty-line-before": "never",
|
||||
"@stylistic/block-closing-brace-newline-after": "always",
|
||||
"@stylistic/block-closing-brace-newline-before": "always-multi-line",
|
||||
"@stylistic/block-closing-brace-space-before": "always-single-line",
|
||||
"at-rule-semicolon-newline-after": "always",
|
||||
"block-closing-brace-empty-line-before": "never",
|
||||
"block-closing-brace-newline-after": "always",
|
||||
"block-closing-brace-newline-before": "always-multi-line",
|
||||
"block-closing-brace-space-before": "always-single-line",
|
||||
"block-no-empty": true,
|
||||
"@stylistic/block-opening-brace-newline-after": "always-multi-line",
|
||||
"@stylistic/block-opening-brace-space-after": "always-single-line",
|
||||
"@stylistic/block-opening-brace-space-before": "always",
|
||||
"@stylistic/color-hex-case": "lower",
|
||||
"block-opening-brace-newline-after": "always-multi-line",
|
||||
"block-opening-brace-space-after": "always-single-line",
|
||||
"block-opening-brace-space-before": "always",
|
||||
"color-hex-case": "lower",
|
||||
"color-hex-length": "short",
|
||||
"color-no-invalid-hex": true,
|
||||
"comment-empty-line-before": [ "always", {
|
||||
@@ -43,8 +42,8 @@
|
||||
"inside-single-line-block"
|
||||
]
|
||||
} ],
|
||||
"@stylistic/declaration-bang-space-after": "never",
|
||||
"@stylistic/declaration-bang-space-before": "always",
|
||||
"declaration-bang-space-after": "never",
|
||||
"declaration-bang-space-before": "always",
|
||||
"declaration-block-no-duplicate-properties": [
|
||||
true,
|
||||
{
|
||||
@@ -52,52 +51,52 @@
|
||||
}
|
||||
],
|
||||
"declaration-block-no-shorthand-property-overrides": true,
|
||||
"@stylistic/declaration-block-semicolon-newline-after": "always-multi-line",
|
||||
"@stylistic/declaration-block-semicolon-space-after": "always-single-line",
|
||||
"@stylistic/declaration-block-semicolon-space-before": "never",
|
||||
"declaration-block-semicolon-newline-after": "always-multi-line",
|
||||
"declaration-block-semicolon-space-after": "always-single-line",
|
||||
"declaration-block-semicolon-space-before": "never",
|
||||
"declaration-block-single-line-max-declarations": 1,
|
||||
"@stylistic/declaration-block-trailing-semicolon": "always",
|
||||
"@stylistic/declaration-colon-newline-after": "always-multi-line",
|
||||
"@stylistic/declaration-colon-space-after": "always-single-line",
|
||||
"@stylistic/declaration-colon-space-before": "never",
|
||||
"declaration-block-trailing-semicolon": "always",
|
||||
"declaration-colon-newline-after": "always-multi-line",
|
||||
"declaration-colon-space-after": "always-single-line",
|
||||
"declaration-colon-space-before": "never",
|
||||
"font-family-no-duplicate-names": true,
|
||||
"function-calc-no-unspaced-operator": true,
|
||||
"@stylistic/function-comma-newline-after": "always-multi-line",
|
||||
"@stylistic/function-comma-space-after": "always-single-line",
|
||||
"@stylistic/function-comma-space-before": "never",
|
||||
"function-comma-newline-after": "always-multi-line",
|
||||
"function-comma-space-after": "always-single-line",
|
||||
"function-comma-space-before": "never",
|
||||
"function-linear-gradient-no-nonstandard-direction": true,
|
||||
"@stylistic/function-max-empty-lines": 0,
|
||||
"function-max-empty-lines": 0,
|
||||
"function-name-case": "lower",
|
||||
"@stylistic/function-parentheses-newline-inside": "always-multi-line",
|
||||
"@stylistic/function-parentheses-space-inside": "never-single-line",
|
||||
"@stylistic/function-whitespace-after": "always",
|
||||
"@stylistic/indentation": 4,
|
||||
"function-parentheses-newline-inside": "always-multi-line",
|
||||
"function-parentheses-space-inside": "never-single-line",
|
||||
"function-whitespace-after": "always",
|
||||
"indentation": 4,
|
||||
"keyframe-declaration-no-important": true,
|
||||
"length-zero-no-unit": true,
|
||||
"@stylistic/max-empty-lines": 1,
|
||||
"@stylistic/media-feature-colon-space-after": "always",
|
||||
"@stylistic/media-feature-colon-space-before": "never",
|
||||
"@stylistic/media-feature-name-case": "lower",
|
||||
"max-empty-lines": 1,
|
||||
"media-feature-colon-space-after": "always",
|
||||
"media-feature-colon-space-before": "never",
|
||||
"media-feature-name-case": "lower",
|
||||
"media-feature-name-no-unknown": true,
|
||||
"media-feature-name-no-vendor-prefix": true,
|
||||
"@stylistic/media-feature-parentheses-space-inside": "never",
|
||||
"@stylistic/media-feature-range-operator-space-after": "always",
|
||||
"@stylistic/media-feature-range-operator-space-before": "always",
|
||||
"@stylistic/media-query-list-comma-newline-after": "always-multi-line",
|
||||
"@stylistic/media-query-list-comma-space-after": "always-single-line",
|
||||
"@stylistic/media-query-list-comma-space-before": "never",
|
||||
"media-feature-parentheses-space-inside": "never",
|
||||
"media-feature-range-operator-space-after": "always",
|
||||
"media-feature-range-operator-space-before": "always",
|
||||
"media-query-list-comma-newline-after": "always-multi-line",
|
||||
"media-query-list-comma-space-after": "always-single-line",
|
||||
"media-query-list-comma-space-before": "never",
|
||||
"no-descending-specificity": true,
|
||||
"no-duplicate-at-import-rules": true,
|
||||
"no-duplicate-selectors": true,
|
||||
"no-empty-source": true,
|
||||
"@stylistic/no-eol-whitespace": true,
|
||||
"@stylistic/no-extra-semicolons": true,
|
||||
"no-eol-whitespace": true,
|
||||
"no-extra-semicolons": true,
|
||||
"no-invalid-double-slash-comments": true,
|
||||
"@stylistic/no-missing-end-of-source-newline": true,
|
||||
"@stylistic/number-leading-zero": "always",
|
||||
"@stylistic/number-no-trailing-zeros": true,
|
||||
"no-missing-end-of-source-newline": true,
|
||||
"number-leading-zero": "always",
|
||||
"number-no-trailing-zeros": true,
|
||||
"plugin/no-browser-hacks": true,
|
||||
"@stylistic/property-case": "lower",
|
||||
"property-case": "lower",
|
||||
"property-no-unknown": [
|
||||
true,
|
||||
{
|
||||
@@ -111,20 +110,20 @@
|
||||
"except": ["first-nested"],
|
||||
"ignore": ["after-comment"]
|
||||
} ],
|
||||
"@stylistic/selector-attribute-brackets-space-inside": "never",
|
||||
"@stylistic/selector-attribute-operator-space-after": "never",
|
||||
"@stylistic/selector-attribute-operator-space-before": "never",
|
||||
"@stylistic/selector-combinator-space-after": "always",
|
||||
"@stylistic/selector-combinator-space-before": "always",
|
||||
"@stylistic/selector-descendant-combinator-no-non-space": true,
|
||||
"@stylistic/selector-list-comma-newline-after": "always",
|
||||
"@stylistic/selector-list-comma-space-before": "never",
|
||||
"@stylistic/selector-max-empty-lines": 0,
|
||||
"selector-attribute-brackets-space-inside": "never",
|
||||
"selector-attribute-operator-space-after": "never",
|
||||
"selector-attribute-operator-space-before": "never",
|
||||
"selector-combinator-space-after": "always",
|
||||
"selector-combinator-space-before": "always",
|
||||
"selector-descendant-combinator-no-non-space": true,
|
||||
"selector-list-comma-newline-after": "always",
|
||||
"selector-list-comma-space-before": "never",
|
||||
"selector-max-empty-lines": 0,
|
||||
"selector-no-vendor-prefix": true,
|
||||
"@stylistic/selector-pseudo-class-case": "lower",
|
||||
"selector-pseudo-class-case": "lower",
|
||||
"selector-pseudo-class-no-unknown": true,
|
||||
"@stylistic/selector-pseudo-class-parentheses-space-inside": "never",
|
||||
"@stylistic/selector-pseudo-element-case": "lower",
|
||||
"selector-pseudo-class-parentheses-space-inside": "never",
|
||||
"selector-pseudo-element-case": "lower",
|
||||
"selector-pseudo-element-colon-notation": "double",
|
||||
"selector-pseudo-element-no-unknown": [
|
||||
true,
|
||||
@@ -137,13 +136,13 @@
|
||||
"selector-type-case": "lower",
|
||||
"selector-type-no-unknown": true,
|
||||
"string-no-newline": true,
|
||||
"@stylistic/unit-case": "lower",
|
||||
"unit-case": "lower",
|
||||
"unit-no-unknown": true,
|
||||
"value-no-vendor-prefix": true,
|
||||
"@stylistic/value-list-comma-newline-after": "always-multi-line",
|
||||
"@stylistic/value-list-comma-space-after": "always-single-line",
|
||||
"@stylistic/value-list-comma-space-before": "never",
|
||||
"@stylistic/value-list-max-empty-lines": 0
|
||||
"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": [
|
||||
{
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,7 +1,4 @@
|
||||
{
|
||||
"[json][typescript][typescriptreact][javascript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
|
||||
@@ -92,17 +92,6 @@
|
||||
- [Venkat Karasani](https://github.com/venkat-karasani)
|
||||
- [Connor Smith](https://github.com/ConnorS1110)
|
||||
- [iFraan](https://github.com/iFraan)
|
||||
- [Ali](https://github.com/bu3alwa)
|
||||
- [K. Kyle Puchkov](https://github.com/kepper104)
|
||||
- [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode)
|
||||
- [Jxiced](https://github.com/Jxiced)
|
||||
- [Derek Huber](https://github.com/Derek4aty1)
|
||||
- [StableCrimson](https://github.com/StableCrimson)
|
||||
- [diegoeche](https://github.com/diegoeche)
|
||||
- [Free O'Toole](https://github.com/freeotoole)
|
||||
- [TheBosZ](https://github.com/thebosz)
|
||||
- [qm3jp](https://github.com/qm3jp)
|
||||
- [johnnyg](https://github.com/johnnyg)
|
||||
|
||||
## Emby Contributors
|
||||
|
||||
|
||||
48
README.md
48
README.md
@@ -73,39 +73,31 @@ Jellyfin Web is the frontend used for most of the clients available for end user
|
||||
|
||||
## Directory Structure
|
||||
|
||||
> [!NOTE]
|
||||
> We are in the process of refactoring to a [new structure](https://forum.jellyfin.org/t-proposed-update-to-the-structure-of-jellyfin-web) based on [Bulletproof React](https://github.com/alan2207/bulletproof-react/blob/master/docs/project-structure.md) architecture guidelines.
|
||||
> Most new code should be organized under the appropriate app directory unless it is common/shared.
|
||||
|
||||
```
|
||||
.
|
||||
└── src
|
||||
├── apps
|
||||
│ ├── dashboard # Admin dashboard app
|
||||
│ ├── experimental # New experimental app
|
||||
│ ├── stable # Classic (stable) app
|
||||
│ └── wizard # Startup wizard app
|
||||
├── assets # Static assets
|
||||
├── components # Higher order visual components and React components
|
||||
├── constants # Common constant values
|
||||
├── controllers # Legacy page views and controllers 🧹 ❌
|
||||
├── elements # Basic webcomponents and React equivalents 🧹
|
||||
├── hooks # Custom React hooks
|
||||
├── lib # Reusable libraries
|
||||
│ ├── globalize # Custom localization library
|
||||
│ ├── jellyfin-apiclient # Supporting code for the deprecated apiclient package
|
||||
│ ├── legacy # Polyfills for legacy browsers
|
||||
│ ├── navdrawer # Navigation drawer library for classic layout
|
||||
│ └── scroller # Content scrolling library
|
||||
├── plugins # Client plugins (features dynamically loaded at runtime)
|
||||
├── scripts # Random assortment of visual components and utilities 🐉 ❌
|
||||
├── strings # Translation files (only commit changes to en-us.json)
|
||||
├── styles # Common app Sass stylesheets
|
||||
├── themes # Sass and MUI themes
|
||||
├── types # Common TypeScript interfaces/types
|
||||
└── utils # Utility functions
|
||||
│ ├── 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
|
||||
```
|
||||
|
||||
- ❌ — Deprecated, do **not** create new files here
|
||||
- 🧹 — Needs cleanup
|
||||
- 🐉 — Serious mess (Here be dragons)
|
||||
|
||||
@@ -15,5 +15,8 @@ module.exports = {
|
||||
'@babel/preset-react'
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-transform-class-properties',
|
||||
'@babel/plugin-transform-private-methods',
|
||||
'babel-plugin-dynamic-import-polyfill'
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,417 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import eslint from '@eslint/js';
|
||||
import comments from '@eslint-community/eslint-plugin-eslint-comments/configs';
|
||||
import compat from 'eslint-plugin-compat';
|
||||
import globals from 'globals';
|
||||
// @ts-expect-error Missing type definition
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import restrictedGlobals from 'confusing-browser-globals';
|
||||
import sonarjs from 'eslint-plugin-sonarjs';
|
||||
import stylistic from '@stylistic/eslint-plugin';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
// @ts-expect-error Harmless type mismatch in dependency
|
||||
comments.recommended,
|
||||
compat.configs['flat/recommended'],
|
||||
importPlugin.flatConfigs.errors,
|
||||
sonarjs.configs.recommended,
|
||||
|
||||
reactPlugin.configs.flat.recommended,
|
||||
{
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
}
|
||||
},
|
||||
jsxA11y.flatConfigs.recommended,
|
||||
|
||||
// Global ignores
|
||||
{
|
||||
ignores: [
|
||||
'node_modules',
|
||||
'coverage',
|
||||
'dist',
|
||||
'.idea',
|
||||
'.vscode'
|
||||
]
|
||||
},
|
||||
|
||||
// Global style rules
|
||||
{
|
||||
plugins: {
|
||||
'@stylistic': stylistic
|
||||
},
|
||||
extends: [ importPlugin.flatConfigs.typescript ],
|
||||
rules: {
|
||||
'array-callback-return': ['error', { 'checkForEach': true }],
|
||||
'curly': ['error', 'multi-line', 'consistent'],
|
||||
'default-case-last': 'error',
|
||||
'max-params': ['error', 7],
|
||||
'new-cap': [
|
||||
'error',
|
||||
{
|
||||
'capIsNewExceptions': ['jQuery.Deferred'],
|
||||
'newIsCapExceptionPattern': '\\.default$'
|
||||
}
|
||||
],
|
||||
'no-duplicate-imports': 'error',
|
||||
'no-empty-function': 'error',
|
||||
'no-extend-native': 'error',
|
||||
'no-lonely-if': 'error',
|
||||
'no-nested-ternary': 'error',
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': ['error', { builtinGlobals: false }],
|
||||
'no-restricted-globals': ['error'].concat(restrictedGlobals),
|
||||
'no-return-assign': 'error',
|
||||
'no-return-await': 'error',
|
||||
'no-sequences': ['error', { 'allowInParentheses': false }],
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': 'error',
|
||||
'no-throw-literal': 'error',
|
||||
'no-undef-init': 'error',
|
||||
'no-unneeded-ternary': 'error',
|
||||
'no-unused-expressions': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
|
||||
'no-unused-private-class-members': 'error',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'no-useless-rename': 'error',
|
||||
'no-useless-constructor': 'off',
|
||||
'@typescript-eslint/no-useless-constructor': 'error',
|
||||
'no-var': 'error',
|
||||
'no-void': ['error', { 'allowAsStatement': true }],
|
||||
'no-warning-comments': ['warn', { 'terms': ['hack', 'xxx'] }],
|
||||
'one-var': ['error', 'never'],
|
||||
'prefer-const': ['error', { 'destructuring': 'all' }],
|
||||
'prefer-promise-reject-errors': ['warn', { 'allowEmptyReject': true }],
|
||||
'@typescript-eslint/prefer-for-of': 'error',
|
||||
'radix': 'error',
|
||||
'yoda': 'error',
|
||||
|
||||
'sonarjs/fixme-tag': 'warn',
|
||||
'sonarjs/todo-tag': 'off',
|
||||
'sonarjs/deprecation': 'off',
|
||||
'sonarjs/no-alphabetical-sort': 'warn',
|
||||
'sonarjs/no-inverted-boolean-check': 'error',
|
||||
'sonarjs/no-selector-parameter': 'off',
|
||||
'sonarjs/pseudo-random': 'warn',
|
||||
// TODO: Enable the following sonarjs rules and fix issues
|
||||
'sonarjs/no-duplicate-string': 'off',
|
||||
'sonarjs/no-nested-functions': 'warn',
|
||||
|
||||
// TODO: Replace with stylistic.configs.customize()
|
||||
'@stylistic/block-spacing': 'error',
|
||||
'@stylistic/brace-style': ['error', '1tbs', { 'allowSingleLine': true }],
|
||||
'@stylistic/comma-dangle': ['error', 'never'],
|
||||
'@stylistic/comma-spacing': 'error',
|
||||
'@stylistic/eol-last': 'error',
|
||||
'@stylistic/indent': ['error', 4, { 'SwitchCase': 1 }],
|
||||
'@stylistic/jsx-quotes': ['error', 'prefer-single'],
|
||||
'@stylistic/keyword-spacing': 'error',
|
||||
'@stylistic/max-statements-per-line': 'error',
|
||||
'@stylistic/no-floating-decimal': 'error',
|
||||
'@stylistic/no-mixed-spaces-and-tabs': 'error',
|
||||
'@stylistic/no-multi-spaces': 'error',
|
||||
'@stylistic/no-multiple-empty-lines': ['error', { 'max': 1 }],
|
||||
'@stylistic/no-trailing-spaces': 'error',
|
||||
'@stylistic/object-curly-spacing': ['error', 'always'],
|
||||
'@stylistic/operator-linebreak': ['error', 'before', { overrides: { '?': 'after', ':': 'after', '=': 'after' } }],
|
||||
'@stylistic/padded-blocks': ['error', 'never'],
|
||||
'@stylistic/quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
|
||||
'@stylistic/semi': 'error',
|
||||
'@stylistic/space-before-blocks': 'error',
|
||||
'@stylistic/space-infix-ops': 'error',
|
||||
|
||||
'@typescript-eslint/no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: '@jellyfin/sdk/lib/generated-client',
|
||||
message: 'Use direct file imports for tree-shaking',
|
||||
allowTypeImports: true
|
||||
},
|
||||
{
|
||||
name: '@jellyfin/sdk/lib/generated-client/api',
|
||||
message: 'Use direct file imports for tree-shaking',
|
||||
allowTypeImports: true
|
||||
},
|
||||
{
|
||||
name: '@jellyfin/sdk/lib/generated-client/models',
|
||||
message: 'Use direct file imports for tree-shaking',
|
||||
allowTypeImports: true
|
||||
},
|
||||
{
|
||||
name: '@mui/icons-material',
|
||||
message: 'Use direct file imports for tree-shaking',
|
||||
allowTypeImports: true
|
||||
},
|
||||
{
|
||||
name: '@mui/material',
|
||||
message: 'Use direct file imports for tree-shaking',
|
||||
allowTypeImports: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Config files use node globals
|
||||
{
|
||||
ignores: [ 'src' ],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Config files are commonjs by default
|
||||
{
|
||||
files: [ '**/*.{cjs,js}' ],
|
||||
ignores: [ 'src' ],
|
||||
languageOptions: {
|
||||
sourceType: 'commonjs'
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-require-imports': 'off'
|
||||
}
|
||||
},
|
||||
|
||||
// App files
|
||||
{
|
||||
files: [
|
||||
'src/**/*.{js,jsx,ts,tsx}'
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
// Tizen globals
|
||||
'tizen': false,
|
||||
'webapis': false,
|
||||
// WebOS globals
|
||||
'webOS': false,
|
||||
// Dependency globals
|
||||
'$': false,
|
||||
'jQuery': false,
|
||||
// Jellyfin globals
|
||||
'ApiClient': true,
|
||||
'Events': true,
|
||||
'chrome': true,
|
||||
'Emby': false,
|
||||
'Hls': true,
|
||||
'LibraryMenu': true,
|
||||
'Windows': false,
|
||||
// Build time definitions
|
||||
__COMMIT_SHA__: false,
|
||||
__JF_BUILD_VERSION__: false,
|
||||
__PACKAGE_JSON_NAME__: false,
|
||||
__PACKAGE_JSON_VERSION__: false,
|
||||
__USE_SYSTEM_FONTS__: false,
|
||||
__WEBPACK_SERVE__: false
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
node: {
|
||||
extensions: [
|
||||
'.js',
|
||||
'.ts',
|
||||
'.jsx',
|
||||
'.tsx'
|
||||
],
|
||||
moduleDirectory: [
|
||||
'node_modules',
|
||||
'src'
|
||||
]
|
||||
}
|
||||
},
|
||||
polyfills: [
|
||||
'Promise',
|
||||
// whatwg-fetch
|
||||
'fetch',
|
||||
// document-register-element
|
||||
'document.registerElement',
|
||||
// resize-observer-polyfill
|
||||
'ResizeObserver',
|
||||
// fast-text-encoding
|
||||
'TextEncoder',
|
||||
// intersection-observer
|
||||
'IntersectionObserver',
|
||||
// Core-js
|
||||
'Object.assign',
|
||||
'Object.is',
|
||||
'Object.setPrototypeOf',
|
||||
'Object.toString',
|
||||
'Object.freeze',
|
||||
'Object.seal',
|
||||
'Object.preventExtensions',
|
||||
'Object.isFrozen',
|
||||
'Object.isSealed',
|
||||
'Object.isExtensible',
|
||||
'Object.getOwnPropertyDescriptor',
|
||||
'Object.getPrototypeOf',
|
||||
'Object.keys',
|
||||
'Object.entries',
|
||||
'Object.getOwnPropertyNames',
|
||||
'Function.name',
|
||||
'Function.hasInstance',
|
||||
'Array.from',
|
||||
'Array.arrayOf',
|
||||
'Array.copyWithin',
|
||||
'Array.fill',
|
||||
'Array.find',
|
||||
'Array.findIndex',
|
||||
'Array.iterator',
|
||||
'String.fromCodePoint',
|
||||
'String.raw',
|
||||
'String.iterator',
|
||||
'String.codePointAt',
|
||||
'String.endsWith',
|
||||
'String.includes',
|
||||
'String.repeat',
|
||||
'String.startsWith',
|
||||
'String.trim',
|
||||
'String.anchor',
|
||||
'String.big',
|
||||
'String.blink',
|
||||
'String.bold',
|
||||
'String.fixed',
|
||||
'String.fontcolor',
|
||||
'String.fontsize',
|
||||
'String.italics',
|
||||
'String.link',
|
||||
'String.small',
|
||||
'String.strike',
|
||||
'String.sub',
|
||||
'String.sup',
|
||||
'RegExp',
|
||||
'Number',
|
||||
'Math',
|
||||
'Date',
|
||||
'async',
|
||||
'Symbol',
|
||||
'Map',
|
||||
'Set',
|
||||
'WeakMap',
|
||||
'WeakSet',
|
||||
'ArrayBuffer',
|
||||
'DataView',
|
||||
'Int8Array',
|
||||
'Uint8Array',
|
||||
'Uint8ClampedArray',
|
||||
'Int16Array',
|
||||
'Uint16Array',
|
||||
'Int32Array',
|
||||
'Uint32Array',
|
||||
'Float32Array',
|
||||
'Float64Array',
|
||||
'Reflect'
|
||||
]
|
||||
},
|
||||
rules: {
|
||||
// TODO: Add typescript recommended typed rules
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
{
|
||||
selector: 'default',
|
||||
format: [ 'camelCase', 'PascalCase' ],
|
||||
leadingUnderscore: 'allow'
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
format: [ 'camelCase', 'PascalCase', 'UPPER_CASE' ],
|
||||
leadingUnderscore: 'allowSingleOrDouble',
|
||||
trailingUnderscore: 'allowSingleOrDouble'
|
||||
},
|
||||
{
|
||||
selector: 'typeLike',
|
||||
format: [ 'PascalCase' ]
|
||||
},
|
||||
{
|
||||
selector: 'enumMember',
|
||||
format: [ 'PascalCase', 'UPPER_CASE' ]
|
||||
},
|
||||
{
|
||||
selector: [ 'objectLiteralProperty', 'typeProperty' ],
|
||||
format: [ 'camelCase', 'PascalCase' ],
|
||||
leadingUnderscore: 'allowSingleOrDouble',
|
||||
trailingUnderscore: 'allowSingleOrDouble'
|
||||
},
|
||||
// Ignore numbers, locale strings (en-us), aria/data attributes and CSS selectors
|
||||
{
|
||||
selector: [ 'objectLiteralProperty', 'typeProperty' ],
|
||||
format: null,
|
||||
filter: {
|
||||
regex: '[ &\\-]|^([0-9]+)$',
|
||||
match: true
|
||||
}
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/no-deprecated': 'warn',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/prefer-string-starts-ends-with': 'error'
|
||||
}
|
||||
},
|
||||
|
||||
// React files
|
||||
{
|
||||
files: [ 'src/**/*.{jsx,tsx}' ],
|
||||
plugins: {
|
||||
'react-hooks': reactHooks
|
||||
},
|
||||
rules: {
|
||||
'react/jsx-filename-extension': ['error', { 'extensions': ['.jsx', '.tsx'] }],
|
||||
'react/jsx-no-bind': 'error',
|
||||
'react/jsx-no-useless-fragment': 'error',
|
||||
'react/no-array-index-key': 'error',
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn'
|
||||
}
|
||||
},
|
||||
|
||||
// Service worker
|
||||
{
|
||||
files: [ 'src/serviceworker.js' ],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.serviceworker
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Legacy JS (less strict)
|
||||
{
|
||||
files: [ 'src/**/*.{js,jsx}' ],
|
||||
rules: {
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'@typescript-eslint/no-this-alias': 'off',
|
||||
|
||||
'sonarjs/public-static-readonly': 'off',
|
||||
|
||||
// TODO: Enable the following rules and fix issues
|
||||
'sonarjs/cognitive-complexity': 'off',
|
||||
'sonarjs/constructor-for-side-effects': 'off',
|
||||
'sonarjs/function-return-type': 'off',
|
||||
'sonarjs/no-async-constructor': 'off',
|
||||
'sonarjs/no-duplicate-string': 'off',
|
||||
'sonarjs/no-ignored-exceptions': 'off',
|
||||
'sonarjs/no-invariant-returns': 'warn',
|
||||
'sonarjs/no-nested-functions': 'off',
|
||||
'sonarjs/void-use': 'off'
|
||||
}
|
||||
}
|
||||
);
|
||||
60
flake.lock
generated
60
flake.lock
generated
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1739874174,
|
||||
"narHash": "sha256-XGxSVtojlwjYRYGvGXex0Cw+/363EVJlbY9TPX9bARk=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d2ab2691c798f6b633be91d74b1626980ddaff30",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
34
flake.nix
34
flake.nix
@@ -1,34 +0,0 @@
|
||||
{
|
||||
description = "jellyfin-web nix flake";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
in {
|
||||
devShell = with pkgs;
|
||||
mkShell rec {
|
||||
buildInputs = [
|
||||
nodejs_20
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
# Also see: https://github.com/sass/embedded-host-node/issues/334
|
||||
echo "Removing sass-embedded from node-modules as its broken on NixOS."
|
||||
rm -rf node_modules/sass-embedded*
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
23076
package-lock.json
generated
23076
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
166
package.json
166
package.json
@@ -1,142 +1,131 @@
|
||||
{
|
||||
"name": "jellyfin-web",
|
||||
"version": "10.12.0",
|
||||
"version": "10.10.0",
|
||||
"description": "Web interface for Jellyfin",
|
||||
"repository": "https://github.com/jellyfin/jellyfin-web",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.27.4",
|
||||
"@babel/plugin-transform-modules-umd": "7.27.1",
|
||||
"@babel/preset-env": "7.27.2",
|
||||
"@babel/preset-react": "7.27.1",
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "4.5.0",
|
||||
"@eslint/js": "9.30.1",
|
||||
"@stylistic/eslint-plugin": "4.4.1",
|
||||
"@stylistic/stylelint-plugin": "3.1.3",
|
||||
"@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": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/react-lazy-load-image-component": "1.6.4",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@typescript-eslint/parser": "8.35.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.62.0",
|
||||
"@typescript-eslint/parser": "5.62.0",
|
||||
"@uupaa/dynamic-import-polyfill": "1.0.2",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"autoprefixer": "10.4.21",
|
||||
"babel-loader": "10.0.0",
|
||||
"@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": "13.0.0",
|
||||
"copy-webpack-plugin": "12.0.2",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "7.1.2",
|
||||
"cssnano": "7.0.7",
|
||||
"es-check": "9.1.4",
|
||||
"eslint": "9.30.1",
|
||||
"eslint-plugin-compat": "6.0.2",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-sonarjs": "3.0.4",
|
||||
"expose-loader": "5.0.1",
|
||||
"fast-glob": "3.3.3",
|
||||
"fork-ts-checker-webpack-plugin": "9.1.0",
|
||||
"globals": "16.2.0",
|
||||
"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.3",
|
||||
"jsdom": "26.1.0",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"postcss": "8.5.6",
|
||||
"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": "10.2.3",
|
||||
"postcss-preset-env": "9.6.0",
|
||||
"postcss-scss": "4.0.9",
|
||||
"sass": "1.89.2",
|
||||
"sass-loader": "16.0.5",
|
||||
"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": "16.21.0",
|
||||
"stylelint": "15.11.0",
|
||||
"stylelint-config-rational-order": "0.1.2",
|
||||
"stylelint-no-browser-hacks": "2.0.0",
|
||||
"stylelint-order": "7.0.0",
|
||||
"stylelint-scss": "6.12.1",
|
||||
"ts-loader": "9.5.2",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.35.1",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.2.4",
|
||||
"webpack": "5.99.9",
|
||||
"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": "6.0.1",
|
||||
"webpack-dev-server": "5.2.2",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.0.4",
|
||||
"webpack-merge": "6.0.1",
|
||||
"worker-loader": "3.0.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.14.0",
|
||||
"@emotion/styled": "11.14.0",
|
||||
"@fontsource/noto-sans": "5.2.7",
|
||||
"@fontsource/noto-sans-hk": "5.2.6",
|
||||
"@fontsource/noto-sans-jp": "5.2.6",
|
||||
"@fontsource/noto-sans-kr": "5.2.6",
|
||||
"@fontsource/noto-sans-sc": "5.2.6",
|
||||
"@fontsource/noto-sans-tc": "5.2.6",
|
||||
"@jellyfin/libass-wasm": "4.2.3",
|
||||
"@jellyfin/sdk": "0.0.0-unstable.202512091852",
|
||||
"@jellyfin/ux-web": "1.0.0",
|
||||
"@mui/icons-material": "6.4.12",
|
||||
"@mui/material": "6.4.12",
|
||||
"@mui/x-date-pickers": "7.29.4",
|
||||
"@react-hook/resize-observer": "2.0.2",
|
||||
"@tanstack/react-query": "5.80.10",
|
||||
"@tanstack/react-query-devtools": "5.80.10",
|
||||
"abortcontroller-polyfill": "1.7.8",
|
||||
"@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",
|
||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.43.0",
|
||||
"core-js": "3.37.1",
|
||||
"date-fns": "2.30.0",
|
||||
"dompurify": "2.5.8",
|
||||
"element-closest-polyfill": "1.0.7",
|
||||
"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.6.13",
|
||||
"hls.js": "1.5.13",
|
||||
"intersection-observer": "0.12.2",
|
||||
"jellyfin-apiclient": "1.11.0",
|
||||
"jquery": "3.7.1",
|
||||
"jstree": "3.3.17",
|
||||
"libarchive.js": "2.0.2",
|
||||
"libpgs": "0.8.1",
|
||||
"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",
|
||||
"material-react-table": "3.2.1",
|
||||
"native-promise-only": "0.8.1",
|
||||
"pdfjs-dist": "3.11.174",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
"react": "19.2.3",
|
||||
"react": "18.3.1",
|
||||
"react-blurhash": "0.3.0",
|
||||
"react-dom": "19.2.3",
|
||||
"react-lazy-load-image-component": "1.6.3",
|
||||
"react-router-dom": "7.11.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.6",
|
||||
"swiper": "11.2.8",
|
||||
"usehooks-ts": "3.1.1",
|
||||
"sortablejs": "1.15.2",
|
||||
"swiper": "11.1.7",
|
||||
"usehooks-ts": "3.1.0",
|
||||
"webcomponents.js": "0.7.24",
|
||||
"whatwg-fetch": "3.6.20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sass-embedded": "1.89.2"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 Firefox versions",
|
||||
"last 2 Chrome versions",
|
||||
@@ -160,16 +149,15 @@
|
||||
"build:development": "webpack --config webpack.dev.js",
|
||||
"build:production": "cross-env NODE_ENV=\"production\" webpack --config webpack.prod.js",
|
||||
"build:check": "tsc --noEmit",
|
||||
"build:es-check": "npm run build:production && npm run escheck",
|
||||
"escheck": "es-check",
|
||||
"lint": "eslint",
|
||||
"lint": "eslint \"./\"",
|
||||
"test": "vitest --watch=false --config vite.config.ts",
|
||||
"test:watch": "vitest --config vite.config.ts",
|
||||
"stylelint": "stylelint \"src/**/*.{css,scss}\""
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"npm": ">=9.6.4 <11.0.0",
|
||||
"npm": ">=9.6.4",
|
||||
"yarn": "YARN NO LONGER USED - use npm instead."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,28 +3,19 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import React from 'react';
|
||||
|
||||
import { ApiProvider } from 'hooks/useApi';
|
||||
import { UserSettingsProvider } from 'hooks/useUserSettings';
|
||||
import { WebConfigProvider } from 'hooks/useWebConfig';
|
||||
import browser from 'scripts/browser';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
import RootAppRouter from 'RootAppRouter';
|
||||
|
||||
const useReactQueryDevtools = window.Proxy // '@tanstack/query-devtools' requires 'Proxy', which cannot be polyfilled for legacy browsers
|
||||
&& !browser.tv; // Don't use devtools on the TV as the navigation is weird
|
||||
|
||||
const RootApp = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ApiProvider>
|
||||
<UserSettingsProvider>
|
||||
<WebConfigProvider>
|
||||
<RootAppRouter />
|
||||
</WebConfigProvider>
|
||||
</UserSettingsProvider>
|
||||
<WebConfigProvider>
|
||||
<RootAppRouter />
|
||||
</WebConfigProvider>
|
||||
</ApiProvider>
|
||||
{useReactQueryDevtools && (
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
)}
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
RouterProvider,
|
||||
@@ -10,31 +10,20 @@ import {
|
||||
import { DASHBOARD_APP_PATHS, DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
||||
import { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/routes/routes';
|
||||
import { STABLE_APP_ROUTES } from 'apps/stable/routes/routes';
|
||||
import { WIZARD_APP_ROUTES } from 'apps/wizard/routes/routes';
|
||||
import AppHeader from 'components/AppHeader';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
import { SETTING_KEY as LAYOUT_SETTING_KEY } from 'components/layoutManager';
|
||||
import BangRedirect from 'components/router/BangRedirect';
|
||||
import { createRouterHistory } from 'components/router/routerHistory';
|
||||
import { LayoutMode } from 'constants/layoutMode';
|
||||
import browser from 'scripts/browser';
|
||||
import appTheme from 'themes';
|
||||
import { ThemeStorageManager } from 'themes/themeStorageManager';
|
||||
import UserThemeProvider from 'themes/UserThemeProvider';
|
||||
|
||||
const layoutMode = browser.tv ? LayoutMode.Tv : localStorage.getItem(LAYOUT_SETTING_KEY);
|
||||
const isExperimentalLayout = !layoutMode || layoutMode === LayoutMode.Experimental;
|
||||
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,
|
||||
...WIZARD_APP_ROUTES,
|
||||
{
|
||||
path: '!/*',
|
||||
Component: BangRedirect
|
||||
}
|
||||
...DASHBOARD_APP_ROUTES
|
||||
]
|
||||
}
|
||||
]);
|
||||
@@ -55,15 +44,11 @@ function RootAppLayout() {
|
||||
.some(path => location.pathname.startsWith(`/${path}`));
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
theme={appTheme}
|
||||
defaultMode='dark'
|
||||
storageManager={ThemeStorageManager}
|
||||
>
|
||||
<UserThemeProvider>
|
||||
<Backdrop />
|
||||
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
|
||||
|
||||
<Outlet />
|
||||
</ThemeProvider>
|
||||
</UserThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
11
src/apiclient.d.ts
vendored
11
src/apiclient.d.ts
vendored
@@ -1,3 +1,4 @@
|
||||
// TODO: Move to jellyfin-apiclient
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare module 'jellyfin-apiclient' {
|
||||
import type {
|
||||
@@ -67,7 +68,7 @@ declare module 'jellyfin-apiclient' {
|
||||
UtcTimeResponse,
|
||||
VirtualFolderInfo
|
||||
} from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { ConnectionState } from 'lib/jellyfin-apiclient';
|
||||
import { ConnectionState } from './utils/jellyfin-apiclient/ConnectionState';
|
||||
|
||||
class ApiClient {
|
||||
constructor(serverAddress: string, appName: string, appVersion: string, deviceName: string, deviceId: string);
|
||||
@@ -136,7 +137,6 @@ declare module 'jellyfin-apiclient' {
|
||||
getInstantMixFromItem(itemId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
getIntros(itemId: string): Promise<BaseItemDtoQueryResult>;
|
||||
getItemCounts(userId?: string): Promise<ItemCounts>;
|
||||
/** @deprecated This function returns a URL with a legacy auth parameter.*/
|
||||
getItemDownloadUrl(itemId: string): string;
|
||||
getItemImageInfos(itemId: string): Promise<ImageInfo[]>;
|
||||
getItems(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
|
||||
@@ -182,7 +182,7 @@ declare module 'jellyfin-apiclient' {
|
||||
getPluginConfiguration(id: string): Promise<any>;
|
||||
getPublicSystemInfo(): Promise<PublicSystemInfo>;
|
||||
getPublicUsers(): Promise<UserDto[]>;
|
||||
getQuickConnect(verb: string): Promise<void | boolean | number | QuickConnectResult | QuickConnectState>;
|
||||
getQuickConnect(verb: string): Promise<void|boolean|number|QuickConnectResult|QuickConnectState>;
|
||||
getReadySyncItems(deviceId: string): Promise<any>;
|
||||
getRecordingFolders(userId: string): Promise<BaseItemDtoQueryResult>;
|
||||
getRegistrationInfo(feature: string): Promise<any>;
|
||||
@@ -308,7 +308,7 @@ declare module 'jellyfin-apiclient' {
|
||||
class AppStore {
|
||||
constructor();
|
||||
|
||||
getItem(name: string): string | null;
|
||||
getItem(name: string): string|null;
|
||||
removeItem(name: string): void;
|
||||
setItem(name: string, value: string): void;
|
||||
}
|
||||
@@ -329,7 +329,7 @@ declare module 'jellyfin-apiclient' {
|
||||
connectToServer(server: any, options?: any): Promise<any>;
|
||||
connectToServers(servers: any[], options?: any): Promise<any>;
|
||||
deleteServer(serverId: string): Promise<void>;
|
||||
getApiClient(item: BaseItemDto | string): ApiClient;
|
||||
getApiClient(item: BaseItemDto|string): ApiClient;
|
||||
getApiClients(): ApiClient[];
|
||||
getAvailableServers(): any[];
|
||||
getOrCreateApiClient(serverId: string): ApiClient;
|
||||
@@ -337,7 +337,6 @@ declare module 'jellyfin-apiclient' {
|
||||
handleMessageReceived(msg: any): void;
|
||||
logout(): Promise<void>;
|
||||
minServerVersion(val?: string): string;
|
||||
updateSavedServerId(server: any): Promise<void>;
|
||||
user(apiClient: ApiClient): Promise<any>;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,37 +2,31 @@ import AppBar from '@mui/material/AppBar';
|
||||
import Box from '@mui/material/Box';
|
||||
import { type Theme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
||||
import React, { FC, StrictMode, useCallback, useEffect, useState } from 'react';
|
||||
import 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 ServerButton from 'components/toolbar/ServerButton';
|
||||
import ElevationScroll from 'components/ElevationScroll';
|
||||
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import ThemeCss from 'components/ThemeCss';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { useLocale } from 'hooks/useLocale';
|
||||
|
||||
import AppTabs from './components/AppTabs';
|
||||
import AppDrawer from './components/drawer/AppDrawer';
|
||||
import HelpButton from './components/toolbar/HelpButton';
|
||||
import { DASHBOARD_APP_PATHS } from './routes/routes';
|
||||
|
||||
import './AppOverrides.scss';
|
||||
|
||||
const DRAWERLESS_PATHS = [ DASHBOARD_APP_PATHS.MetadataManager ];
|
||||
|
||||
export const Component: FC = () => {
|
||||
const [ isDrawerActive, setIsDrawerActive ] = useState(false);
|
||||
const location = useLocation();
|
||||
const { user } = useApi();
|
||||
const { dateFnsLocale } = useLocale();
|
||||
|
||||
const isMediumScreen = useMediaQuery((t: Theme) => t.breakpoints.up('md'));
|
||||
const isMetadataManager = location.pathname.startsWith(`/${DASHBOARD_APP_PATHS.MetadataManager}`);
|
||||
const isDrawerAvailable = Boolean(user) && !isMetadataManager;
|
||||
const isDrawerAvailable = Boolean(user)
|
||||
&& !DRAWERLESS_PATHS.some(path => location.pathname.startsWith(`/${path}`));
|
||||
const isDrawerOpen = isDrawerActive && isDrawerAvailable;
|
||||
|
||||
const onToggleDrawer = useCallback(() => {
|
||||
@@ -49,65 +43,52 @@ export const Component: FC = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={dateFnsLocale}>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<StrictMode>
|
||||
<ElevationScroll elevate={false}>
|
||||
<AppBar
|
||||
position='fixed'
|
||||
sx={{
|
||||
width: {
|
||||
xs: '100%',
|
||||
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
|
||||
},
|
||||
ml: {
|
||||
xs: 0,
|
||||
md: isDrawerAvailable ? DRAWER_WIDTH : 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AppToolbar
|
||||
isBackButtonAvailable={appRouter.canGoBack()}
|
||||
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
|
||||
isDrawerOpen={isDrawerOpen}
|
||||
onDrawerButtonClick={onToggleDrawer}
|
||||
buttons={
|
||||
<HelpButton />
|
||||
}
|
||||
>
|
||||
{isMetadataManager && (
|
||||
<ServerButton />
|
||||
)}
|
||||
|
||||
<AppTabs isDrawerOpen={isDrawerOpen} />
|
||||
</AppToolbar>
|
||||
</AppBar>
|
||||
</ElevationScroll>
|
||||
|
||||
{
|
||||
isDrawerAvailable && (
|
||||
<AppDrawer
|
||||
open={isDrawerOpen}
|
||||
onClose={onToggleDrawer}
|
||||
onOpen={onToggleDrawer}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</StrictMode>
|
||||
|
||||
<Box
|
||||
component='main'
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<ElevationScroll elevate={false}>
|
||||
<AppBar
|
||||
position='fixed'
|
||||
sx={{
|
||||
width: '100%',
|
||||
flexGrow: 1
|
||||
width: {
|
||||
xs: '100%',
|
||||
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
|
||||
},
|
||||
ml: {
|
||||
xs: 0,
|
||||
md: isDrawerAvailable ? DRAWER_WIDTH : 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AppBody>
|
||||
<Outlet />
|
||||
</AppBody>
|
||||
</Box>
|
||||
<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>
|
||||
<ThemeCss dashboard />
|
||||
</LocalizationProvider>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ $drawer-width: 240px;
|
||||
|
||||
// Fix dashboard pages layout to work with drawer
|
||||
.dashboardDocument {
|
||||
.mainAnimatedPage:not(.metadataEditorPage) {
|
||||
.mainAnimatedPage {
|
||||
@media all and (min-width: $mui-bp-md) {
|
||||
left: $drawer-width;
|
||||
}
|
||||
@@ -31,8 +31,4 @@ $drawer-width: 240px;
|
||||
padding-top: 3.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.metadataEditorPage {
|
||||
padding-top: 3.25rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import isEqual from 'lodash-es/isEqual';
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { EventType } from 'constants/eventType';
|
||||
import { EventType } from 'types/eventType';
|
||||
import Events, { type Event } from 'utils/events';
|
||||
|
||||
interface AppTabsParams {
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardMedia from '@mui/material/CardMedia';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
|
||||
import CardActionArea from '@mui/material/CardActionArea';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { Link, To } from 'react-router-dom';
|
||||
|
||||
interface BaseCardProps {
|
||||
title?: string;
|
||||
text?: string;
|
||||
image?: string | null;
|
||||
icon?: React.ReactNode;
|
||||
to?: To;
|
||||
onClick?: () => void;
|
||||
action?: boolean;
|
||||
actionRef?: React.MutableRefObject<HTMLButtonElement | null>;
|
||||
onActionClick?: () => void;
|
||||
height?: number;
|
||||
width?: number;
|
||||
};
|
||||
|
||||
const BaseCard = ({
|
||||
title,
|
||||
text,
|
||||
image,
|
||||
icon,
|
||||
to,
|
||||
onClick,
|
||||
action,
|
||||
actionRef,
|
||||
onActionClick,
|
||||
height,
|
||||
width
|
||||
}: BaseCardProps) => {
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: height || 240,
|
||||
width: width
|
||||
}}
|
||||
>
|
||||
<CardActionArea
|
||||
{...(to && {
|
||||
component: Link,
|
||||
to: to
|
||||
})}
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
alignItems: 'stretch'
|
||||
}}
|
||||
>
|
||||
{image ? (
|
||||
<CardMedia
|
||||
sx={{ flexGrow: 1 }}
|
||||
image={image}
|
||||
title={title}
|
||||
/>
|
||||
) : (
|
||||
<Box className={getDefaultBackgroundClass(title)} sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
{icon}
|
||||
</Box>
|
||||
)}
|
||||
</CardActionArea>
|
||||
<CardContent
|
||||
sx={{
|
||||
minHeight: 50,
|
||||
'&:last-child': {
|
||||
paddingBottom: 2,
|
||||
paddingRight: 1
|
||||
}
|
||||
}}>
|
||||
<Stack flexGrow={1} direction='row'>
|
||||
<Stack flexGrow={1}>
|
||||
<Typography gutterBottom sx={{
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis'
|
||||
}}>
|
||||
{title}
|
||||
</Typography>
|
||||
{text && (
|
||||
<Typography
|
||||
variant='body2'
|
||||
color='text.secondary'
|
||||
sx={{
|
||||
lineBreak: 'anywhere'
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
<Box>
|
||||
{action ? (
|
||||
<IconButton ref={actionRef} onClick={onActionClick}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</Box>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseCard;
|
||||
@@ -1,70 +0,0 @@
|
||||
import Search from '@mui/icons-material/Search';
|
||||
import InputBase, { type InputBaseProps } from '@mui/material/InputBase';
|
||||
import { alpha, styled } from '@mui/material/styles';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
const SearchContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: alpha(theme.palette.common.white, 0.15),
|
||||
'&:hover': {
|
||||
backgroundColor: alpha(theme.palette.common.white, 0.25)
|
||||
},
|
||||
width: '100%',
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: 'auto'
|
||||
}
|
||||
}));
|
||||
|
||||
const SearchIconWrapper = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(0, 2),
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}));
|
||||
|
||||
const StyledInputBase = styled(InputBase)(({ theme }) => ({
|
||||
color: 'inherit',
|
||||
flexGrow: 1,
|
||||
'& .MuiInputBase-input': {
|
||||
padding: theme.spacing(1, 1, 1, 0),
|
||||
// vertical padding + font size from searchIcon
|
||||
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
|
||||
transition: theme.transitions.create('width'),
|
||||
width: '100%',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
width: '20ch'
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
interface SearchInputProps extends InputBaseProps {
|
||||
label?: string
|
||||
}
|
||||
|
||||
const SearchInput: FC<SearchInputProps> = ({
|
||||
label,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<SearchContainer>
|
||||
<SearchIconWrapper>
|
||||
<Search />
|
||||
</SearchIconWrapper>
|
||||
<StyledInputBase
|
||||
placeholder={label}
|
||||
inputProps={{
|
||||
'aria-label': label,
|
||||
...props.inputProps
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</SearchContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchInput;
|
||||
@@ -1,30 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import Snackbar, { SnackbarProps } from '@mui/material/Snackbar';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
const Toast = (props: SnackbarProps) => {
|
||||
const onCloseClick = useCallback((e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
props.onClose?.(e, 'clickaway');
|
||||
}, [ props ]);
|
||||
|
||||
const action = (
|
||||
<IconButton
|
||||
size='small'
|
||||
color='inherit'
|
||||
onClick={onCloseClick}
|
||||
>
|
||||
<CloseIcon fontSize='small' />
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
autoHideDuration={3300}
|
||||
action={action}
|
||||
{ ...props }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toast;
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
|
||||
import type { SxProps, Theme } from '@mui/material';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import React, { type FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import UserAvatar from 'components/UserAvatar';
|
||||
|
||||
interface UserAvatarButtonProps {
|
||||
user?: UserDto
|
||||
sx?: SxProps<Theme>
|
||||
}
|
||||
|
||||
const UserAvatarButton: FC<UserAvatarButtonProps> = ({
|
||||
user,
|
||||
sx
|
||||
}) => (
|
||||
user?.Id ? (
|
||||
<IconButton
|
||||
size='large'
|
||||
color='inherit'
|
||||
sx={{
|
||||
padding: 0,
|
||||
...sx
|
||||
}}
|
||||
title={user.Name || undefined}
|
||||
component={Link}
|
||||
to={`/dashboard/users/profile?userId=${user.Id}`}
|
||||
>
|
||||
<UserAvatar user={user} />
|
||||
</IconButton>
|
||||
) : undefined
|
||||
);
|
||||
|
||||
export default UserAvatarButton;
|
||||
34
src/apps/dashboard/components/activityTable/LogLevelChip.tsx
Normal file
34
src/apps/dashboard/components/activityTable/LogLevelChip.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
@@ -1,14 +1,12 @@
|
||||
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, { type FC, useCallback, useState } from 'react';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
|
||||
import type { ActivityLogEntryCell } from '../types/ActivityLogEntryCell';
|
||||
|
||||
const OverviewCell: FC<ActivityLogEntryCell> = ({ row }) => {
|
||||
const { ShortOverview, Overview } = row.original;
|
||||
const OverviewCell: FC<ActivityLogEntry> = ({ Overview, ShortOverview }) => {
|
||||
const displayValue = ShortOverview ?? Overview;
|
||||
const [ open, setOpen ] = useState(false);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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;
|
||||
@@ -1,5 +1,4 @@
|
||||
import Article from '@mui/icons-material/Article';
|
||||
import Backup from '@mui/icons-material/Backup';
|
||||
import Lan from '@mui/icons-material/Lan';
|
||||
import Schedule from '@mui/icons-material/Schedule';
|
||||
import VpnKey from '@mui/icons-material/VpnKey';
|
||||
@@ -39,14 +38,6 @@ const AdvancedDrawerSection = () => {
|
||||
<ListItemText primary={globalize.translate('HeaderApiKeys')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/backups'>
|
||||
<ListItemIcon>
|
||||
<Backup />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderBackups')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/logs'>
|
||||
<ListItemIcon>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Analytics from '@mui/icons-material/Analytics';
|
||||
import Devices from '@mui/icons-material/Devices';
|
||||
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';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Dvr from '@mui/icons-material/Dvr';
|
||||
import LiveTv from '@mui/icons-material/LiveTv';
|
||||
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';
|
||||
@@ -29,7 +28,7 @@ const LiveTvDrawerSection = () => {
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/livetv/recordings'>
|
||||
<ListItemLink to='/dashboard/recordings'>
|
||||
<ListItemIcon>
|
||||
<Dvr />
|
||||
</ListItemIcon>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
@@ -32,16 +33,23 @@ const PluginDrawerSection = () => {
|
||||
>
|
||||
<ListItemLink
|
||||
to='/dashboard/plugins'
|
||||
includePaths={[
|
||||
'/configurationpage',
|
||||
'/dashboard/plugins/repositories'
|
||||
]}
|
||||
includePaths={[ '/configurationpage' ]}
|
||||
excludePaths={pagesInfo?.map(p => `/${Dashboard.getPluginUrl(p.Name)}`)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Extension />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabPlugins')} />
|
||||
<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 => (
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import Dashboard from '@mui/icons-material/Dashboard';
|
||||
import ExpandLess from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
import LibraryAdd from '@mui/icons-material/LibraryAdd';
|
||||
import Palette from '@mui/icons-material/Palette';
|
||||
import People from '@mui/icons-material/People';
|
||||
import PlayCircle from '@mui/icons-material/PlayCircle';
|
||||
import Settings from '@mui/icons-material/Settings';
|
||||
import { 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 ListItemButton from '@mui/material/ListItemButton/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React, { type MouseEvent, useCallback, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
@@ -36,20 +28,8 @@ const PLAYBACK_PATHS = [
|
||||
const ServerDrawerSection = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const [ isLibrarySectionOpen, setIsLibrarySectionOpen ] = useState(LIBRARY_PATHS.includes(location.pathname));
|
||||
const [ isPlaybackSectionOpen, setIsPlaybackSectionOpen ] = useState(PLAYBACK_PATHS.includes(location.pathname));
|
||||
|
||||
const onLibrarySectionClick = useCallback((e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsLibrarySectionOpen(isOpen => !isOpen);
|
||||
}, []);
|
||||
|
||||
const onPlaybackSectionClick = useCallback((e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsPlaybackSectionOpen(isOpen => !isOpen);
|
||||
}, []);
|
||||
const isLibrarySectionOpen = LIBRARY_PATHS.includes(location.pathname);
|
||||
const isPlaybackSectionOpen = PLAYBACK_PATHS.includes(location.pathname);
|
||||
|
||||
return (
|
||||
<List
|
||||
@@ -76,12 +56,6 @@ const ServerDrawerSection = () => {
|
||||
<ListItemText primary={globalize.translate('General')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItemLink to='/dashboard/branding'>
|
||||
<ListItemIcon>
|
||||
<Palette />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderBranding')} />
|
||||
</ListItemLink>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/users'>
|
||||
<ListItemIcon>
|
||||
@@ -91,13 +65,13 @@ const ServerDrawerSection = () => {
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton onClick={onLibrarySectionClick}>
|
||||
<ListItemLink to='/dashboard/libraries' selected={false}>
|
||||
<ListItemIcon>
|
||||
<LibraryAdd />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderLibraries')} />
|
||||
{isLibrarySectionOpen ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemButton>
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<Collapse in={isLibrarySectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
@@ -108,7 +82,7 @@ const ServerDrawerSection = () => {
|
||||
<ListItemText inset primary={globalize.translate('Display')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dashboard/libraries/metadata' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('LabelMetadata')} />
|
||||
<ListItemText inset primary={globalize.translate('Metadata')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dashboard/libraries/nfo' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabNfoSettings')} />
|
||||
@@ -116,13 +90,13 @@ const ServerDrawerSection = () => {
|
||||
</List>
|
||||
</Collapse>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton onClick={onPlaybackSectionClick}>
|
||||
<ListItemLink to='/dashboard/playback/transcoding' selected={false}>
|
||||
<ListItemIcon>
|
||||
<PlayCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TitlePlayback')} />
|
||||
{isPlaybackSectionOpen ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemButton>
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<Collapse in={isPlaybackSectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import format from 'date-fns/format';
|
||||
import type { MRT_Cell, MRT_RowData } from 'material-react-table';
|
||||
import { FC } from 'react';
|
||||
|
||||
import { useLocale } from 'hooks/useLocale';
|
||||
|
||||
interface CellProps {
|
||||
cell: MRT_Cell<MRT_RowData>
|
||||
}
|
||||
|
||||
const DateTimeCell: FC<CellProps> = ({ cell }) => {
|
||||
const { dateFnsLocale } = useLocale();
|
||||
|
||||
return format(cell.getValue<Date>(), 'Pp', { locale: dateFnsLocale });
|
||||
};
|
||||
|
||||
export default DateTimeCell;
|
||||
@@ -1,73 +0,0 @@
|
||||
import Box from '@mui/material/Box/Box';
|
||||
import Stack from '@mui/material/Stack/Stack';
|
||||
import type {} from '@mui/material/themeCssVarsAugmentation';
|
||||
import Typography from '@mui/material/Typography/Typography';
|
||||
import { type MRT_RowData, type MRT_TableInstance, type MRT_TableOptions, MaterialReactTable } from 'material-react-table';
|
||||
import React from 'react';
|
||||
|
||||
import Page, { type PageProps } from 'components/Page';
|
||||
|
||||
interface TablePageProps<T extends MRT_RowData> extends PageProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
table: MRT_TableInstance<T>
|
||||
}
|
||||
|
||||
export const DEFAULT_TABLE_OPTIONS: Partial<MRT_TableOptions<MRT_RowData>> = {
|
||||
// Enable custom features
|
||||
enableColumnPinning: true,
|
||||
enableColumnResizing: true,
|
||||
|
||||
// Sticky header/footer
|
||||
enableStickyFooter: true,
|
||||
enableStickyHeader: true,
|
||||
muiTableContainerProps: {
|
||||
sx: {
|
||||
maxHeight: 'calc(100% - 7rem)' // 2 x 3.5rem for header and footer
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const TablePage = <T extends MRT_RowData>({
|
||||
title,
|
||||
subtitle,
|
||||
table,
|
||||
children,
|
||||
...pageProps
|
||||
}: TablePageProps<T>) => {
|
||||
return (
|
||||
<Page
|
||||
title={title}
|
||||
{...pageProps}
|
||||
>
|
||||
<Box
|
||||
className='content-primary'
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
spacing={2}
|
||||
sx={{
|
||||
marginBottom: 1
|
||||
}}
|
||||
>
|
||||
<Typography variant='h1'>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle && (
|
||||
<Typography>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
<MaterialReactTable table={table} />
|
||||
</Box>
|
||||
{children}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default TablePage;
|
||||
@@ -1,36 +0,0 @@
|
||||
import HelpOutline from '@mui/icons-material/HelpOutline';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip/Tooltip';
|
||||
import React from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { HelpLinks } from 'apps/dashboard/constants/helpLinks';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
const HelpButton = () => (
|
||||
<Routes>
|
||||
{
|
||||
HelpLinks.map(({ paths, url }) => paths.map(path => (
|
||||
<Route
|
||||
key={[url, path].join('-')}
|
||||
path={path}
|
||||
element={
|
||||
<Tooltip title={globalize.translate('Help')}>
|
||||
<IconButton
|
||||
href={url}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
size='large'
|
||||
color='inherit'
|
||||
>
|
||||
<HelpOutline />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
))).flat()
|
||||
}
|
||||
</Routes>
|
||||
);
|
||||
|
||||
export default HelpButton;
|
||||
@@ -1,51 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import globalize from 'lib/globalize';
|
||||
import Widget from './Widget';
|
||||
import List from '@mui/material/List';
|
||||
import ActivityListItem from 'apps/dashboard/features/activity/components/ActivityListItem';
|
||||
import { useLogEntries } from 'apps/dashboard/features/activity/api/useLogEntries';
|
||||
import subSeconds from 'date-fns/subSeconds';
|
||||
import Skeleton from '@mui/material/Skeleton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
||||
const ActivityLogWidget = () => {
|
||||
const dayBefore = useMemo(() => (
|
||||
subSeconds(new Date(), 24 * 60 * 60).toISOString()
|
||||
), []);
|
||||
|
||||
const { data: logs, isPending } = useLogEntries({
|
||||
startIndex: 0,
|
||||
limit: 7,
|
||||
minDate: dayBefore,
|
||||
hasUserId: true
|
||||
});
|
||||
|
||||
return (
|
||||
<Widget
|
||||
title={globalize.translate('HeaderActivity')}
|
||||
href='/dashboard/activity?useractivity=true'
|
||||
>
|
||||
{isPending ? (
|
||||
<Stack spacing={2}>
|
||||
<Skeleton variant='rounded' height={60} />
|
||||
<Skeleton variant='rounded' height={60} />
|
||||
<Skeleton variant='rounded' height={60} />
|
||||
<Skeleton variant='rounded' height={60} />
|
||||
</Stack>
|
||||
) : (
|
||||
<List sx={{ bgcolor: 'background.paper' }}>
|
||||
{logs?.Items?.map(entry => (
|
||||
<ActivityListItem
|
||||
key={entry.Id}
|
||||
item={entry}
|
||||
displayShortOverview={true}
|
||||
to='/dashboard/activity?useractivity=true'
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityLogWidget;
|
||||
@@ -1,42 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import globalize from 'lib/globalize';
|
||||
import Widget from './Widget';
|
||||
import List from '@mui/material/List';
|
||||
import ActivityListItem from 'apps/dashboard/features/activity/components/ActivityListItem';
|
||||
import subSeconds from 'date-fns/subSeconds';
|
||||
import { useLogEntries } from 'apps/dashboard/features/activity/api/useLogEntries';
|
||||
|
||||
const AlertsLogWidget = () => {
|
||||
const weekBefore = useMemo(() => (
|
||||
subSeconds(new Date(), 7 * 24 * 60 * 60).toISOString()
|
||||
), []);
|
||||
|
||||
const { data: alerts, isPending } = useLogEntries({
|
||||
startIndex: 0,
|
||||
limit: 4,
|
||||
minDate: weekBefore,
|
||||
hasUserId: false
|
||||
});
|
||||
|
||||
if (isPending || alerts?.Items?.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Widget
|
||||
title={globalize.translate('Alerts')}
|
||||
href='/dashboard/activity?useractivity=false'
|
||||
>
|
||||
<List sx={{ bgcolor: 'background.paper' }}>
|
||||
{alerts?.Items?.map(entry => (
|
||||
<ActivityListItem
|
||||
key={entry.Id}
|
||||
item={entry}
|
||||
displayShortOverview={false}
|
||||
to='/dashboard/activity?useractivity=false'
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertsLogWidget;
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
import globalize from 'lib/globalize';
|
||||
import Widget from './Widget';
|
||||
import DeviceCard from 'apps/dashboard/features/devices/components/DeviceCard';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import useLiveSessions from 'apps/dashboard/features/sessions/hooks/useLiveSessions';
|
||||
|
||||
const DevicesWidget = () => {
|
||||
const { data: devices } = useLiveSessions();
|
||||
|
||||
return (
|
||||
<Widget
|
||||
title={globalize.translate('HeaderDevices')}
|
||||
href='/dashboard/devices'
|
||||
>
|
||||
<Stack direction='row' flexWrap='wrap' gap={2}>
|
||||
{devices?.map(device => (
|
||||
<DeviceCard
|
||||
key={device.Id}
|
||||
device={device}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export default DevicesWidget;
|
||||
@@ -1,103 +0,0 @@
|
||||
import type { ItemCounts } from '@jellyfin/sdk/lib/generated-client/models/item-counts';
|
||||
import Book from '@mui/icons-material/Book';
|
||||
import Movie from '@mui/icons-material/Movie';
|
||||
import MusicNote from '@mui/icons-material/MusicNote';
|
||||
import MusicVideo from '@mui/icons-material/MusicVideo';
|
||||
import Tv from '@mui/icons-material/Tv';
|
||||
import VideoLibrary from '@mui/icons-material/VideoLibrary';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import SvgIcon from '@mui/material/SvgIcon';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { useItemCounts } from 'apps/dashboard/features/metrics/api/useItemCounts';
|
||||
import MetricCard, { type MetricCardProps } from 'apps/dashboard/features/metrics/components/MetricCard';
|
||||
import globalize from 'lib/globalize';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
interface MetricDefinition {
|
||||
key: keyof ItemCounts
|
||||
i18n: string
|
||||
}
|
||||
|
||||
interface CardDefinition {
|
||||
Icon: typeof SvgIcon
|
||||
metrics: MetricDefinition[]
|
||||
}
|
||||
|
||||
const CARD_DEFINITIONS: CardDefinition[] = [
|
||||
{
|
||||
Icon: Movie,
|
||||
metrics: [{ key: 'MovieCount', i18n: 'Movies' }]
|
||||
}, {
|
||||
Icon: Tv,
|
||||
metrics: [
|
||||
{ key: 'SeriesCount', i18n: 'Series' },
|
||||
{ key: 'EpisodeCount', i18n: 'Episodes' }
|
||||
]
|
||||
}, {
|
||||
Icon: MusicNote,
|
||||
metrics: [
|
||||
{ key: 'AlbumCount', i18n: 'Albums' },
|
||||
{ key: 'SongCount', i18n: 'Songs' }
|
||||
]
|
||||
}, {
|
||||
Icon: MusicVideo,
|
||||
metrics: [{ key: 'MusicVideoCount', i18n: 'MusicVideos' }]
|
||||
}, {
|
||||
Icon: Book,
|
||||
metrics: [{ key: 'BookCount', i18n: 'Books' }]
|
||||
}, {
|
||||
Icon: VideoLibrary,
|
||||
metrics: [{ key: 'BoxSetCount', i18n: 'Collections' }]
|
||||
}
|
||||
];
|
||||
|
||||
const ItemCountsWidget = () => {
|
||||
const {
|
||||
data: counts,
|
||||
isPending
|
||||
} = useItemCounts();
|
||||
|
||||
const cards: MetricCardProps[] = useMemo(() => {
|
||||
return CARD_DEFINITIONS
|
||||
.filter(def => (
|
||||
// Include all cards while the request is pending
|
||||
isPending
|
||||
// Check if the metrics are present in counts
|
||||
|| def.metrics.some(({ key }) => counts?.[key])
|
||||
))
|
||||
.map(({ Icon, metrics }) => ({
|
||||
Icon,
|
||||
metrics: metrics.map(({ i18n, key }) => ({
|
||||
label: globalize.translate(i18n),
|
||||
value: counts?.[key]
|
||||
}))
|
||||
}));
|
||||
}, [ counts, isPending ]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Grid
|
||||
container
|
||||
spacing={2}
|
||||
sx={{
|
||||
alignItems: 'stretch'
|
||||
}}
|
||||
>
|
||||
{cards.map(card => (
|
||||
<Grid
|
||||
key={card.metrics.map(metric => metric.label).join('-')}
|
||||
item
|
||||
xs={12}
|
||||
sm={6}
|
||||
lg={4}
|
||||
>
|
||||
<MetricCard {...card} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemCountsWidget;
|
||||
@@ -1,42 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import globalize from 'lib/globalize';
|
||||
import Widget from './Widget';
|
||||
import type { TaskInfo } from '@jellyfin/sdk/lib/generated-client/models/task-info';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import { TaskState } from '@jellyfin/sdk/lib/generated-client/models/task-state';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import TaskProgress from 'apps/dashboard/features/tasks/components/TaskProgress';
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
||||
type RunningTasksWidgetProps = {
|
||||
tasks?: TaskInfo[];
|
||||
};
|
||||
|
||||
const RunningTasksWidget = ({ tasks }: RunningTasksWidgetProps) => {
|
||||
const runningTasks = useMemo(() => {
|
||||
return tasks?.filter(v => v.State == TaskState.Running) || [];
|
||||
}, [ tasks ]);
|
||||
|
||||
if (runningTasks.length == 0) return null;
|
||||
|
||||
return (
|
||||
<Widget
|
||||
title={globalize.translate('HeaderRunningTasks')}
|
||||
href='/dashboard/tasks'
|
||||
>
|
||||
<Paper sx={{ padding: 2 }}>
|
||||
<Stack spacing={2} maxWidth={'330px'}>
|
||||
{runningTasks.map((task => (
|
||||
<Box key={task.Id}>
|
||||
<Typography>{task.Name}</Typography>
|
||||
<TaskProgress task={task} />
|
||||
</Box>
|
||||
)))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export default RunningTasksWidget;
|
||||
@@ -1,104 +0,0 @@
|
||||
import React from 'react';
|
||||
import globalize from 'lib/globalize';
|
||||
import Widget from './Widget';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Button from '@mui/material/Button';
|
||||
import Skeleton from '@mui/material/Skeleton';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
import { useSystemInfo } from 'hooks/useSystemInfo';
|
||||
|
||||
type ServerInfoWidgetProps = {
|
||||
onScanLibrariesClick?: () => void;
|
||||
onRestartClick?: () => void;
|
||||
onShutdownClick?: () => void;
|
||||
isScanning?: boolean;
|
||||
};
|
||||
|
||||
const ServerInfoWidget = ({
|
||||
onScanLibrariesClick,
|
||||
onRestartClick,
|
||||
onShutdownClick,
|
||||
isScanning
|
||||
}: ServerInfoWidgetProps) => {
|
||||
const { data: systemInfo, isPending } = useSystemInfo();
|
||||
|
||||
return (
|
||||
<Widget
|
||||
title={globalize.translate('TabServer')}
|
||||
href='/dashboard/settings'
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<Paper sx={{
|
||||
padding: 2
|
||||
}}>
|
||||
<Stack direction='row'>
|
||||
<Stack flexGrow={1} spacing={1}>
|
||||
<Typography fontWeight='bold'>{globalize.translate('LabelServerName')}</Typography>
|
||||
<Typography fontWeight='bold'>{globalize.translate('LabelServerVersion')}</Typography>
|
||||
<Typography fontWeight='bold'>{globalize.translate('LabelWebVersion')}</Typography>
|
||||
<Typography fontWeight='bold'>{globalize.translate('LabelBuildVersion')}</Typography>
|
||||
</Stack>
|
||||
<Stack flexGrow={5} spacing={1}>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Skeleton />
|
||||
<Skeleton />
|
||||
<Skeleton />
|
||||
<Skeleton />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography>{systemInfo?.ServerName}</Typography>
|
||||
<Typography>{systemInfo?.Version}</Typography>
|
||||
<Typography>{__PACKAGE_JSON_VERSION__}</Typography>
|
||||
<Typography>{__JF_BUILD_VERSION__}</Typography>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Stack direction='row' spacing={1.5}>
|
||||
<Button
|
||||
onClick={onScanLibrariesClick}
|
||||
startIcon={<RefreshIcon />}
|
||||
sx={{
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
disabled={isScanning}
|
||||
>
|
||||
{globalize.translate('ButtonScanAllLibraries')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={onRestartClick}
|
||||
startIcon={<RestartAltIcon />}
|
||||
color='error'
|
||||
sx={{
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{globalize.translate('Restart')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={onShutdownClick}
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
color='error'
|
||||
sx={{
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{globalize.translate('ButtonShutdown')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerInfoWidget;
|
||||
@@ -1,50 +0,0 @@
|
||||
import List from '@mui/material/List';
|
||||
import React from 'react';
|
||||
import StorageListItem from 'apps/dashboard/features/storage/components/StorageListItem';
|
||||
import globalize from 'lib/globalize';
|
||||
import Widget from './Widget';
|
||||
import { useSystemStorage } from 'apps/dashboard/features/storage/api/useSystemStorage';
|
||||
|
||||
const ServerPathWidget = () => {
|
||||
const { data: systemStorage } = useSystemStorage();
|
||||
|
||||
return (
|
||||
<Widget
|
||||
title={globalize.translate('HeaderPaths')}
|
||||
href='/dashboard/settings'
|
||||
>
|
||||
<List sx={{ bgcolor: 'background.paper' }}>
|
||||
<StorageListItem
|
||||
label={globalize.translate('LabelCache')}
|
||||
folder={systemStorage?.CacheFolder}
|
||||
/>
|
||||
<StorageListItem
|
||||
label={globalize.translate('LabelImageCache')}
|
||||
folder={systemStorage?.ImageCacheFolder}
|
||||
/>
|
||||
<StorageListItem
|
||||
label={globalize.translate('LabelProgramData')}
|
||||
folder={systemStorage?.ProgramDataFolder}
|
||||
/>
|
||||
<StorageListItem
|
||||
label={globalize.translate('LabelLogs')}
|
||||
folder={systemStorage?.LogFolder}
|
||||
/>
|
||||
<StorageListItem
|
||||
label={globalize.translate('LabelMetadata')}
|
||||
folder={systemStorage?.InternalMetadataFolder}
|
||||
/>
|
||||
<StorageListItem
|
||||
label={globalize.translate('LabelTranscodes')}
|
||||
folder={systemStorage?.TranscodingTempFolder}
|
||||
/>
|
||||
<StorageListItem
|
||||
label={globalize.translate('LabelWeb')}
|
||||
folder={systemStorage?.WebFolder}
|
||||
/>
|
||||
</List>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerPathWidget;
|
||||
@@ -1,38 +0,0 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import ChevronRight from '@mui/icons-material/ChevronRight';
|
||||
import React from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
|
||||
type WidgetProps = {
|
||||
title: string;
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const Widget = ({ title, href, children }: WidgetProps) => {
|
||||
return (
|
||||
<Box>
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to={href}
|
||||
variant='text'
|
||||
color='inherit'
|
||||
endIcon={<ChevronRight />}
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
marginBottom: 1
|
||||
}}
|
||||
>
|
||||
<Typography variant='h3' component='span'>
|
||||
{title}
|
||||
</Typography>
|
||||
</Button>
|
||||
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Widget;
|
||||
@@ -1,54 +0,0 @@
|
||||
export const HelpLinks = [
|
||||
{
|
||||
paths: ['/dashboard/devices'],
|
||||
url: 'https://jellyfin.org/docs/general/server/devices'
|
||||
}, {
|
||||
paths: ['/dashboard/libraries'],
|
||||
url: 'https://jellyfin.org/docs/general/server/libraries'
|
||||
}, {
|
||||
paths: [
|
||||
'/dashboard/livetv',
|
||||
'/dashboard/livetv/tuner',
|
||||
'/dashboard/recordings'
|
||||
],
|
||||
url: 'https://jellyfin.org/docs/general/server/live-tv/'
|
||||
}, {
|
||||
paths: ['/dashboard/livetv/guide'],
|
||||
url: 'https://jellyfin.org/docs/general/server/live-tv/setup-guide#adding-guide-data'
|
||||
}, {
|
||||
paths: ['/dashboard/networking'],
|
||||
url: 'https://jellyfin.org/docs/general/networking/'
|
||||
}, {
|
||||
paths: ['/dashboard/playback/transcoding'],
|
||||
url: 'https://jellyfin.org/docs/general/server/transcoding'
|
||||
}, {
|
||||
paths: ['/dashboard/plugins'],
|
||||
url: 'https://jellyfin.org/docs/general/server/plugins/'
|
||||
}, {
|
||||
paths: ['/dashboard/plugins/repositories'],
|
||||
url: 'https://jellyfin.org/docs/general/server/plugins/#repositories'
|
||||
}, {
|
||||
paths: [
|
||||
'/dashboard/branding',
|
||||
'/dashboard/settings'
|
||||
],
|
||||
url: 'https://jellyfin.org/docs/general/server/settings'
|
||||
}, {
|
||||
paths: ['/dashboard/tasks'],
|
||||
url: 'https://jellyfin.org/docs/general/server/tasks'
|
||||
}, {
|
||||
paths: ['/dashboard/users'],
|
||||
url: 'https://jellyfin.org/docs/general/server/users/adding-managing-users'
|
||||
}, {
|
||||
paths: [
|
||||
'/dashboard/users/access',
|
||||
'/dashboard/users/parentalcontrol',
|
||||
'/dashboard/users/password',
|
||||
'/dashboard/users/profile'
|
||||
],
|
||||
url: 'https://jellyfin.org/docs/general/server/users/'
|
||||
}, {
|
||||
paths: ['/dashboard/backups'],
|
||||
url: 'https://jellyfin.org/docs/general/administration/backup-and-restore/'
|
||||
}
|
||||
];
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { ActivityLogApiGetLogEntriesRequest } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import type { Api } from '@jellyfin/sdk';
|
||||
import { getActivityLogApi } from '@jellyfin/sdk/lib/utils/api/activity-log-api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
const fetchLogEntries = async (
|
||||
api: Api,
|
||||
requestParams?: ActivityLogApiGetLogEntriesRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
const response = await getActivityLogApi(api).getLogEntries(requestParams, {
|
||||
signal: options?.signal
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const useLogEntries = (
|
||||
requestParams: ActivityLogApiGetLogEntriesRequest
|
||||
) => {
|
||||
const { api } = useApi();
|
||||
return useQuery({
|
||||
queryKey: ['ActivityLogEntries', requestParams],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchLogEntries(api!, requestParams, { signal }),
|
||||
enabled: !!api,
|
||||
refetchOnMount: false
|
||||
});
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import PermMedia from '@mui/icons-material/PermMedia';
|
||||
import React, { type FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ActivityLogEntryCell } from 'apps/dashboard/features/activity/types/ActivityLogEntryCell';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
const ActionsCell: FC<ActivityLogEntryCell> = ({ row }) => (
|
||||
row.original.ItemId ? (
|
||||
<IconButton
|
||||
size='large'
|
||||
title={globalize.translate('LabelMediaDetails')}
|
||||
component={Link}
|
||||
to={`/details?id=${row.original.ItemId}`}
|
||||
>
|
||||
<PermMedia fontSize='inherit' />
|
||||
</IconButton>
|
||||
) : undefined
|
||||
);
|
||||
|
||||
export default ActionsCell;
|
||||
@@ -1,75 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
|
||||
import Notifications from '@mui/icons-material/Notifications';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemAvatar from '@mui/material/ListItemAvatar';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import formatRelative from 'date-fns/formatRelative';
|
||||
import { getLocale } from 'utils/dateFnsLocale';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import getLogLevelColor from '../utils/getLogLevelColor';
|
||||
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
|
||||
type ActivityListItemProps = {
|
||||
item: ActivityLogEntry;
|
||||
displayShortOverview: boolean;
|
||||
to: string;
|
||||
};
|
||||
|
||||
const ActivityListItem = ({ item, displayShortOverview, to }: ActivityListItemProps) => {
|
||||
const relativeDate = useMemo(() => {
|
||||
if (item.Date) {
|
||||
return formatRelative(Date.parse(item.Date), Date.now(), { locale: getLocale() });
|
||||
} else {
|
||||
return 'N/A';
|
||||
}
|
||||
}, [ item ]);
|
||||
|
||||
return (
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to={to}>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: getLogLevelColor(item.Severity || LogLevel.Information) + '.main' }}>
|
||||
<Notifications sx={{ color: '#fff' }} />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
|
||||
<ListItemText
|
||||
primary={<Typography sx={{ whiteSpace: 'pre-wrap' }}>{item.Name}</Typography>}
|
||||
secondary={(
|
||||
<Stack>
|
||||
<Typography
|
||||
sx={{
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
variant='body1'
|
||||
color='text.secondary'
|
||||
>
|
||||
{relativeDate}
|
||||
</Typography>
|
||||
{displayShortOverview && (
|
||||
<Typography
|
||||
sx={{
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
variant='body1'
|
||||
color='text.secondary'
|
||||
>
|
||||
{item.ShortOverview}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
disableTypography
|
||||
/>
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityListItem;
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { ActivityLogEntryCell } from '../types/ActivityLogEntryCell';
|
||||
import LogLevelChip from './LogLevelChip';
|
||||
|
||||
const LogLevelCell: FC<ActivityLogEntryCell> = ({ cell }) => {
|
||||
const level = cell.getValue<LogLevel | undefined>();
|
||||
return level ? (
|
||||
<LogLevelChip level={level} />
|
||||
) : undefined;
|
||||
};
|
||||
|
||||
export default LogLevelCell;
|
||||
@@ -1,21 +0,0 @@
|
||||
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import getLogLevelColor from '../utils/getLogLevelColor';
|
||||
|
||||
const LogLevelChip = ({ level }: { level: LogLevel }) => {
|
||||
const levelText = useMemo(() => globalize.translate(`LogLevel.${level}`), [level]);
|
||||
|
||||
return (
|
||||
<Chip
|
||||
size='small'
|
||||
color={getLogLevelColor(level)}
|
||||
label={levelText}
|
||||
title={levelText}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogLevelChip;
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
|
||||
import type { MRT_Cell, MRT_Row } from 'material-react-table';
|
||||
|
||||
export interface ActivityLogEntryCell {
|
||||
cell: MRT_Cell<ActivityLogEntry>
|
||||
row: MRT_Row<ActivityLogEntry>
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
||||
|
||||
const getLogLevelColor = (level: LogLevel) => {
|
||||
switch (level) {
|
||||
case LogLevel.Information:
|
||||
return 'info';
|
||||
case LogLevel.Warning:
|
||||
return 'warning';
|
||||
case LogLevel.Error:
|
||||
case LogLevel.Critical:
|
||||
return 'error';
|
||||
}
|
||||
};
|
||||
|
||||
export default getLogLevelColor;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { BackupApi } from '@jellyfin/sdk/lib/generated-client/api/backup-api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
export const QUERY_KEY = 'Backups';
|
||||
|
||||
const fetchBackups = async (api: Api, options?: AxiosRequestConfig) => {
|
||||
// FIXME: Replace with getBackupApi when available in SDK
|
||||
const backupApi = new BackupApi(api.configuration, undefined, api.axiosInstance);
|
||||
|
||||
const response = await backupApi.listBackups(options);
|
||||
|
||||
const backups = response.data;
|
||||
|
||||
backups.sort((a, b) => {
|
||||
if (a.DateCreated && b.DateCreated) {
|
||||
return new Date(b.DateCreated).getTime() - new Date(a.DateCreated).getTime();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return backups;
|
||||
};
|
||||
|
||||
export const useBackups = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [ QUERY_KEY ],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchBackups(api!, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { BackupOptionsDto } from '@jellyfin/sdk/lib/generated-client/models/backup-options-dto';
|
||||
import { BackupApi } from '@jellyfin/sdk/lib/generated-client/api/backup-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { QUERY_KEY } from './useBackups';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
export const useCreateBackup = () => {
|
||||
const { api } = useApi();
|
||||
// FIXME: Replace with getBackupApi when available in SDK
|
||||
const backupApi = new BackupApi(api?.configuration, undefined, api?.axiosInstance);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (backupOptions: BackupOptionsDto) => (
|
||||
backupApi.createBackup({
|
||||
backupOptionsDto: backupOptions
|
||||
})
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QUERY_KEY ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import { BackupApi } from '@jellyfin/sdk/lib/generated-client/api/backup-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
export const useRestoreBackup = () => {
|
||||
const { api } = useApi();
|
||||
// FIXME: Replace with getBackupApi when available in SDK
|
||||
const backupApi = new BackupApi(api?.configuration, undefined, api?.axiosInstance);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (fileName: string) => (
|
||||
backupApi.startRestoreBackup({
|
||||
backupRestoreRequestDto: {
|
||||
ArchiveFileName: fileName
|
||||
}
|
||||
})
|
||||
)
|
||||
});
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
import React, { FunctionComponent, useCallback, useState } from 'react';
|
||||
import type { BackupManifestDto } from '@jellyfin/sdk/lib/generated-client/models/backup-manifest-dto';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import Restore from '@mui/icons-material/Restore';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import globalize from 'lib/globalize';
|
||||
import BackupInfoDialog from './BackupInfoDialog';
|
||||
|
||||
type BackupProps = {
|
||||
backup: BackupManifestDto;
|
||||
onRestore: (backup: BackupManifestDto) => void;
|
||||
};
|
||||
|
||||
const Backup: FunctionComponent<BackupProps> = ({ backup, onRestore }) => {
|
||||
const [ isInfoDialogOpen, setIsInfoDialogOpen ] = useState(false);
|
||||
|
||||
const onDialogClose = useCallback(() => {
|
||||
setIsInfoDialogOpen(false);
|
||||
}, []);
|
||||
|
||||
const openDialog = useCallback(() => {
|
||||
setIsInfoDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const restore = useCallback(() => {
|
||||
onRestore(backup);
|
||||
}, [ backup, onRestore ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackupInfoDialog
|
||||
backup={backup}
|
||||
onClose={onDialogClose}
|
||||
open={isInfoDialogOpen}
|
||||
/>
|
||||
<ListItem
|
||||
disablePadding
|
||||
secondaryAction={
|
||||
<Tooltip disableInteractive title={globalize.translate('LabelRestore')}>
|
||||
<IconButton onClick={restore}>
|
||||
<Restore />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<ListItemButton onClick={openDialog}>
|
||||
<ListItemText
|
||||
primary={backup.DateCreated}
|
||||
secondary={backup.Path}
|
||||
slotProps={{
|
||||
primary: {
|
||||
variant: 'h3'
|
||||
},
|
||||
secondary: {
|
||||
variant: 'body1'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Backup;
|
||||
@@ -1,146 +0,0 @@
|
||||
import type { BackupManifestDto } from '@jellyfin/sdk/lib/generated-client/models/backup-manifest-dto';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import Box from '@mui/material/Box';
|
||||
import globalize from 'lib/globalize';
|
||||
import React, { FunctionComponent, useCallback, useState } from 'react';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import ContentCopy from '@mui/icons-material/ContentCopy';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { copy } from 'scripts/clipboard';
|
||||
import Toast from 'apps/dashboard/components/Toast';
|
||||
|
||||
type IProps = {
|
||||
backup: BackupManifestDto;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const BackupInfoDialog: FunctionComponent<IProps> = ({ backup, open, onClose }: IProps) => {
|
||||
const [ isCopiedToastOpen, setIsCopiedToastOpen ] = useState(false);
|
||||
|
||||
const handleToastClose = useCallback(() => {
|
||||
setIsCopiedToastOpen(false);
|
||||
}, []);
|
||||
|
||||
const copyPath = useCallback(async () => {
|
||||
if (backup.Path) {
|
||||
await copy(backup.Path);
|
||||
setIsCopiedToastOpen(true);
|
||||
}
|
||||
}, [ backup.Path ]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
maxWidth={'sm'}
|
||||
fullWidth
|
||||
>
|
||||
<Toast
|
||||
open={isCopiedToastOpen}
|
||||
onClose={handleToastClose}
|
||||
message={globalize.translate('Copied')}
|
||||
/>
|
||||
<DialogTitle>
|
||||
{backup.DateCreated}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Stack spacing={2}>
|
||||
<Box>
|
||||
<Stack
|
||||
direction='row'
|
||||
spacing={2}
|
||||
>
|
||||
<Typography fontWeight='bold'>{globalize.translate('LabelPath')}</Typography>
|
||||
<Stack direction='row'>
|
||||
<Typography color='text.secondary'>{backup.Path}</Typography>
|
||||
<IconButton size='small' onClick={copyPath}>
|
||||
<ContentCopy fontSize='small' />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction='row'
|
||||
spacing={2}
|
||||
>
|
||||
<Typography fontWeight='bold'>{globalize.translate('LabelVersion')}</Typography>
|
||||
<Typography color='text.secondary'>{backup.ServerVersion}</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<FormGroup>
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Database'
|
||||
defaultChecked={true}
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('LabelDatabase')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Metadata'
|
||||
defaultChecked={backup.Options?.Metadata}
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('LabelMetadata')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Subtitles'
|
||||
defaultChecked={backup.Options?.Subtitles}
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('Subtitles')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Trickplay'
|
||||
defaultChecked={backup.Options?.Trickplay}
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('Trickplay')}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>
|
||||
{globalize.translate('ButtonOk')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackupInfoDialog;
|
||||
@@ -1,27 +0,0 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
type IProps = {
|
||||
open: boolean
|
||||
};
|
||||
|
||||
const BackupProgressDialog: FunctionComponent<IProps> = ({ open }) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
maxWidth={'xs'}
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>{globalize.translate('MessageBackupInProgress')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<LinearProgress />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackupProgressDialog;
|
||||
@@ -1,123 +0,0 @@
|
||||
import React, { FunctionComponent, useCallback } from 'react';
|
||||
import globalize from 'lib/globalize';
|
||||
import type { BackupOptionsDto } from '@jellyfin/sdk/lib/generated-client/models/backup-options-dto';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
|
||||
type IProps = {
|
||||
open: boolean,
|
||||
onClose?: () => void,
|
||||
onCreate: (backupOptions: BackupOptionsDto) => void
|
||||
};
|
||||
|
||||
const CreateBackupForm: FunctionComponent<IProps> = ({ open, onClose, onCreate }) => {
|
||||
const onSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
const backupOptions: BackupOptionsDto = {
|
||||
'Metadata': data.Metadata?.toString() === 'on',
|
||||
'Trickplay': data.Trickplay?.toString() === 'on',
|
||||
'Subtitles': data.Subtitles?.toString() === 'on'
|
||||
};
|
||||
|
||||
onCreate(backupOptions);
|
||||
}, [ onCreate ]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
maxWidth={'xs'}
|
||||
fullWidth
|
||||
onClose={onClose}
|
||||
slotProps={{
|
||||
paper: {
|
||||
component: 'form',
|
||||
onSubmit
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle>{globalize.translate('ButtonCreateBackup')}</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Stack spacing={2}>
|
||||
<DialogContentText>
|
||||
{globalize.translate('MessageBackupDisclaimer')}
|
||||
</DialogContentText>
|
||||
<FormGroup>
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Database'
|
||||
defaultChecked={true}
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('LabelDatabase')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Metadata'
|
||||
defaultChecked={false}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('LabelMetadata')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Subtitles'
|
||||
defaultChecked={false}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('Subtitles')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='Trickplay'
|
||||
defaultChecked={false}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('Trickplay')}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant='text'
|
||||
>{globalize.translate('ButtonCancel')}</Button>
|
||||
<Button type='submit'>{globalize.translate('Create')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateBackupForm;
|
||||
@@ -1,46 +0,0 @@
|
||||
import Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import globalize from 'lib/globalize';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
type IProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
||||
const RestoreConfirmationDialog: FunctionComponent<IProps> = ({ open, onClose, onConfirm }: IProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth={'xs'}
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
{globalize.translate('LabelRestore')}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{globalize.translate('MessageRestoreDisclaimer')}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant='text'>
|
||||
{globalize.translate('ButtonCancel')}
|
||||
</Button>
|
||||
<Button onClick={onConfirm}>
|
||||
{globalize.translate('LabelRestore')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default RestoreConfirmationDialog;
|
||||
@@ -1,32 +0,0 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
type IProps = {
|
||||
open: boolean
|
||||
};
|
||||
|
||||
const RestoreProgressDialog: FunctionComponent<IProps> = ({ open }) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
maxWidth={'xs'}
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>{globalize.translate('MessageRestoreInProgress')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack spacing={2}>
|
||||
<DialogContentText>{globalize.translate('MessageWaitingForServer')}</DialogContentText>
|
||||
<LinearProgress />
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default RestoreProgressDialog;
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { getBrandingApi } from '@jellyfin/sdk/lib/utils/api/branding-api';
|
||||
import { queryOptions, useQuery } from '@tanstack/react-query';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
export const QUERY_KEY = 'BrandingOptions';
|
||||
|
||||
const fetchBrandingOptions = async (
|
||||
api: Api,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
return getBrandingApi(api)
|
||||
.getBrandingOptions(options)
|
||||
.then(({ data }) => data);
|
||||
};
|
||||
|
||||
export const getBrandingOptionsQuery = (
|
||||
api?: Api
|
||||
) => queryOptions({
|
||||
queryKey: [ QUERY_KEY ],
|
||||
queryFn: ({ signal }) => fetchBrandingOptions(api!, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
|
||||
export const useBrandingOptions = () => {
|
||||
const { api } = useApi();
|
||||
return useQuery(getBrandingOptionsQuery(api));
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { DevicesApiDeleteDeviceRequest } from '@jellyfin/sdk/lib/generated-client/api/devices-api';
|
||||
import { getDevicesApi } from '@jellyfin/sdk/lib/utils/api/devices-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import { QUERY_KEY } from './useDevices';
|
||||
|
||||
export const useDeleteDevice = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (params: DevicesApiDeleteDeviceRequest) => (
|
||||
getDevicesApi(api!)
|
||||
.deleteDevice(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QUERY_KEY ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
import type { DevicesApiGetDevicesRequest } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import type { Api } from '@jellyfin/sdk';
|
||||
import { getDevicesApi } from '@jellyfin/sdk/lib/utils/api/devices-api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
export const QUERY_KEY = 'Devices';
|
||||
|
||||
const fetchDevices = async (
|
||||
api: Api,
|
||||
requestParams?: DevicesApiGetDevicesRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
const response = await getDevicesApi(api).getDevices(requestParams, {
|
||||
signal: options?.signal
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const useDevices = (
|
||||
requestParams: DevicesApiGetDevicesRequest
|
||||
) => {
|
||||
const { api } = useApi();
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEY, requestParams],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchDevices(api!, requestParams, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { DevicesApiUpdateDeviceOptionsRequest } from '@jellyfin/sdk/lib/generated-client/api/devices-api';
|
||||
import { getDevicesApi } from '@jellyfin/sdk/lib/utils/api/devices-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import { QUERY_KEY } from './useDevices';
|
||||
|
||||
export const useUpdateDevice = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (params: DevicesApiUpdateDeviceOptionsRequest) => (
|
||||
getDevicesApi(api!)
|
||||
.updateDeviceOptions(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QUERY_KEY ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,259 +0,0 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type { SessionInfo } from '@jellyfin/sdk/lib/generated-client/models/session-info';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardMedia from '@mui/material/CardMedia';
|
||||
import { getDeviceIcon } from 'utils/image';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import getNowPlayingName from '../../sessions/utils/getNowPlayingName';
|
||||
import getSessionNowPlayingTime from '../../sessions/utils/getSessionNowPlayingTime';
|
||||
import getNowPlayingImageUrl from '../../sessions/utils/getNowPlayingImageUrl';
|
||||
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
|
||||
import Comment from '@mui/icons-material/Comment';
|
||||
import PlayArrow from '@mui/icons-material/PlayArrow';
|
||||
import Pause from '@mui/icons-material/Pause';
|
||||
import Stop from '@mui/icons-material/Stop';
|
||||
import Info from '@mui/icons-material/Info';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import CardActions from '@mui/material/CardActions';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import SimpleAlert from 'components/SimpleAlert';
|
||||
import playmethodhelper from 'components/playback/playmethodhelper';
|
||||
import globalize from 'lib/globalize';
|
||||
import getSessionNowPlayingStreamInfo from '../../sessions/utils/getSessionNowPlayingStreamInfo';
|
||||
import { useSendPlayStateCommand } from '../../sessions/api/usePlayPauseSession';
|
||||
import { PlaystateCommand } from '@jellyfin/sdk/lib/generated-client/models/playstate-command';
|
||||
import InputDialog from 'components/InputDialog';
|
||||
import { useSendMessage } from '../../sessions/api/useSendMessage';
|
||||
|
||||
type DeviceCardProps = {
|
||||
device: SessionInfo;
|
||||
};
|
||||
|
||||
const DeviceCard = ({ device }: DeviceCardProps) => {
|
||||
const [ playbackInfoTitle, setPlaybackInfoTitle ] = useState('');
|
||||
const [ playbackInfoDesc, setPlaybackInfoDesc ] = useState('');
|
||||
const [ isPlaybackInfoOpen, setIsPlaybackInfoOpen ] = useState(false);
|
||||
const [ isMessageDialogOpen, setIsMessageDialogOpen ] = useState(false);
|
||||
const sendMessage = useSendMessage();
|
||||
const playStateCommand = useSendPlayStateCommand();
|
||||
|
||||
const onPlayPauseSession = useCallback(() => {
|
||||
if (device.Id) {
|
||||
playStateCommand.mutate({
|
||||
sessionId: device.Id,
|
||||
command: PlaystateCommand.PlayPause
|
||||
});
|
||||
}
|
||||
}, [ device, playStateCommand ]);
|
||||
|
||||
const onStopSession = useCallback(() => {
|
||||
if (device.Id) {
|
||||
playStateCommand.mutate({
|
||||
sessionId: device.Id,
|
||||
command: PlaystateCommand.Stop
|
||||
});
|
||||
}
|
||||
}, [ device, playStateCommand ]);
|
||||
|
||||
const onMessageSend = useCallback((message: string) => {
|
||||
if (device.Id) {
|
||||
sendMessage.mutate({
|
||||
sessionId: device.Id,
|
||||
messageCommand: {
|
||||
Text: message,
|
||||
TimeoutMs: 5000
|
||||
}
|
||||
});
|
||||
setIsMessageDialogOpen(false);
|
||||
}
|
||||
}, [ sendMessage, device ]);
|
||||
|
||||
const showMessageDialog = useCallback(() => {
|
||||
setIsMessageDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const onMessageDialogClose = useCallback(() => {
|
||||
setIsMessageDialogOpen(false);
|
||||
}, []);
|
||||
|
||||
const closePlaybackInfo = useCallback(() => {
|
||||
setIsPlaybackInfoOpen(false);
|
||||
}, []);
|
||||
|
||||
const showPlaybackInfo = useCallback(() => {
|
||||
const displayPlayMethod = playmethodhelper.getDisplayPlayMethod(device);
|
||||
|
||||
switch (displayPlayMethod) {
|
||||
case 'Remux':
|
||||
setPlaybackInfoTitle(globalize.translate('Remuxing'));
|
||||
setPlaybackInfoDesc(globalize.translate('RemuxHelp1') + '\n' + globalize.translate('RemuxHelp2'));
|
||||
break;
|
||||
case 'DirectStream':
|
||||
setPlaybackInfoTitle(globalize.translate('DirectStreaming'));
|
||||
setPlaybackInfoDesc(globalize.translate('DirectStreamHelp1') + '\n' + globalize.translate('DirectStreamHelp2'));
|
||||
break;
|
||||
case 'DirectPlay':
|
||||
setPlaybackInfoTitle(globalize.translate('DirectPlaying'));
|
||||
setPlaybackInfoDesc(globalize.translate('DirectPlayHelp'));
|
||||
break;
|
||||
case 'Transcode': {
|
||||
const transcodeReasons = device.TranscodingInfo?.TranscodeReasons as string[] | undefined;
|
||||
const localizedTranscodeReasons = transcodeReasons?.map(transcodeReason => globalize.translate(transcodeReason)) || [];
|
||||
setPlaybackInfoTitle(globalize.translate('Transcoding'));
|
||||
setPlaybackInfoDesc(
|
||||
globalize.translate('MediaIsBeingConverted')
|
||||
+ '\n\n' + getSessionNowPlayingStreamInfo(device)
|
||||
+ '\n\n' + globalize.translate('LabelReasonForTranscoding')
|
||||
+ '\n' + localizedTranscodeReasons.join('\n')
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setIsPlaybackInfoOpen(true);
|
||||
}, [ device ]);
|
||||
|
||||
const nowPlayingName = useMemo(() => (
|
||||
getNowPlayingName(device)
|
||||
), [ device ]);
|
||||
|
||||
const nowPlayingImage = useMemo(() => (
|
||||
device.NowPlayingItem && getNowPlayingImageUrl(device.NowPlayingItem)
|
||||
), [device]);
|
||||
|
||||
const runningTime = useMemo(() => (
|
||||
getSessionNowPlayingTime(device)
|
||||
), [ device ]);
|
||||
|
||||
const deviceIcon = useMemo(() => (
|
||||
getDeviceIcon(device)
|
||||
), [ device ]);
|
||||
|
||||
const canControl = device.ServerId && device.SupportsRemoteControl;
|
||||
const isPlayingMedia = !!device.NowPlayingItem;
|
||||
|
||||
return (
|
||||
<Card sx={{ width: { xs: '100%', sm: '360px' } }}>
|
||||
<InputDialog
|
||||
open={isMessageDialogOpen}
|
||||
onClose={onMessageDialogClose}
|
||||
title={globalize.translate('HeaderSendMessage')}
|
||||
label={globalize.translate('LabelMessageText')}
|
||||
confirmButtonText={globalize.translate('ButtonSend')}
|
||||
onConfirm={onMessageSend}
|
||||
/>
|
||||
<SimpleAlert
|
||||
open={isPlaybackInfoOpen}
|
||||
title={playbackInfoTitle}
|
||||
text={playbackInfoDesc}
|
||||
onClose={closePlaybackInfo}
|
||||
/>
|
||||
<CardMedia
|
||||
sx={{
|
||||
height: 200,
|
||||
display: 'flex'
|
||||
}}
|
||||
className={getDefaultBackgroundClass(device.Id)}
|
||||
image={nowPlayingImage || undefined}
|
||||
>
|
||||
<Stack
|
||||
justifyContent={'space-between'}
|
||||
flexGrow={1}
|
||||
sx={{
|
||||
backgroundColor: nowPlayingImage ? 'rgba(0, 0, 0, 0.7)' : null,
|
||||
padding: 2
|
||||
}}>
|
||||
<Stack direction='row' alignItems='center' spacing={1}>
|
||||
<img
|
||||
src={deviceIcon}
|
||||
style={{
|
||||
maxWidth: '2.5em',
|
||||
maxHeight: '2.5em'
|
||||
}}
|
||||
alt={device.DeviceName || ''}
|
||||
/>
|
||||
<Stack>
|
||||
<Typography>{device.DeviceName}</Typography>
|
||||
<Typography>{device.Client + ' ' + device.ApplicationVersion}</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction='row' alignItems={'end'}>
|
||||
<Stack flexGrow={1}>
|
||||
{nowPlayingName.image ? (
|
||||
<img
|
||||
src={nowPlayingName.image}
|
||||
style={{
|
||||
maxHeight: '24px',
|
||||
maxWidth: '130px',
|
||||
alignSelf: 'flex-start'
|
||||
}}
|
||||
alt='Media Icon'
|
||||
/>
|
||||
) : (
|
||||
<Typography>{nowPlayingName.topText}</Typography>
|
||||
)}
|
||||
<Typography>{nowPlayingName.bottomText}</Typography>
|
||||
</Stack>
|
||||
{device.NowPlayingItem && (
|
||||
<Typography>{runningTime.start} / {runningTime.end}</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CardMedia>
|
||||
{(device.PlayState?.PositionTicks != null && device.NowPlayingItem?.RunTimeTicks != null) && (
|
||||
<LinearProgress
|
||||
variant='buffer'
|
||||
value={(device.PlayState.PositionTicks / device.NowPlayingItem.RunTimeTicks) * 100}
|
||||
valueBuffer={device.TranscodingInfo?.CompletionPercentage || 0}
|
||||
sx={{
|
||||
'& .MuiLinearProgress-dashed': {
|
||||
animation: 'none',
|
||||
backgroundImage: 'none',
|
||||
backgroundColor: 'background.paper'
|
||||
},
|
||||
'& .MuiLinearProgress-bar2': {
|
||||
backgroundColor: '#dd4919'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<CardActions disableSpacing>
|
||||
<Stack direction='row' flexGrow={1} justifyContent='center'>
|
||||
{canControl && isPlayingMedia && (
|
||||
<>
|
||||
<IconButton onClick={onPlayPauseSession}>
|
||||
{device.PlayState?.IsPaused ? <PlayArrow /> : <Pause />}
|
||||
</IconButton>
|
||||
<IconButton onClick={onStopSession}>
|
||||
<Stop />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
{isPlayingMedia && (
|
||||
<IconButton onClick={showPlaybackInfo}>
|
||||
<Info />
|
||||
</IconButton>
|
||||
)}
|
||||
{canControl && (
|
||||
<IconButton onClick={showMessageDialog}>
|
||||
<Comment />
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
</CardActions>
|
||||
{device.UserName && (
|
||||
<Stack
|
||||
direction='row'
|
||||
flexGrow={1}
|
||||
justifyContent='center'
|
||||
sx={{ paddingBottom: 2 }}
|
||||
>
|
||||
<Typography>{device.UserName}</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeviceCard;
|
||||
@@ -1,22 +0,0 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { DeviceInfoCell } from 'apps/dashboard/features/devices/types/deviceInfoCell';
|
||||
import { getDeviceIcon } from 'utils/image';
|
||||
|
||||
const DeviceNameCell: FC<DeviceInfoCell> = ({ row, renderedCellValue }) => (
|
||||
<>
|
||||
<img
|
||||
alt={row.original.AppName || undefined}
|
||||
src={getDeviceIcon(row.original)}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
maxWidth: '1.5em',
|
||||
maxHeight: '1.5em',
|
||||
marginRight: '1rem'
|
||||
}}
|
||||
/>
|
||||
{renderedCellValue}
|
||||
</>
|
||||
);
|
||||
|
||||
export default DeviceNameCell;
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { DeviceInfoDto } from '@jellyfin/sdk/lib/generated-client/models/device-info-dto';
|
||||
import type { MRT_Row } from 'material-react-table';
|
||||
|
||||
export interface DeviceInfoCell {
|
||||
renderedCellValue: React.ReactNode
|
||||
row: MRT_Row<DeviceInfoDto>
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { getApiKeyApi } from '@jellyfin/sdk/lib/utils/api/api-key-api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
export const QUERY_KEY = 'ApiKeys';
|
||||
|
||||
const fetchApiKeys = async (api: Api, options?: AxiosRequestConfig) => {
|
||||
const response = await getApiKeyApi(api).getKeys(options);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const useApiKeys = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [ QUERY_KEY ],
|
||||
queryFn: ({ signal }) => fetchApiKeys(api!, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { ApiKeyApiCreateKeyRequest } from '@jellyfin/sdk/lib/generated-client/api/api-key-api';
|
||||
import { getApiKeyApi } from '@jellyfin/sdk/lib/utils/api/api-key-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import { QUERY_KEY } from './useApiKeys';
|
||||
|
||||
export const useCreateKey = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (params: ApiKeyApiCreateKeyRequest) => (
|
||||
getApiKeyApi(api!)
|
||||
.createKey(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QUERY_KEY ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { ApiKeyApiRevokeKeyRequest } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { getApiKeyApi } from '@jellyfin/sdk/lib/utils/api/api-key-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import { QUERY_KEY } from './useApiKeys';
|
||||
|
||||
export const useRevokeKey = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (params: ApiKeyApiRevokeKeyRequest) => (
|
||||
getApiKeyApi(api!)
|
||||
.revokeKey(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QUERY_KEY ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { getLocalizationApi } from '@jellyfin/sdk/lib/utils/api/localization-api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
|
||||
const fetchCountries = async (api: Api, options?: AxiosRequestConfig) => {
|
||||
const response = await getLocalizationApi(api).getCountries(options);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const useCountries = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [ 'Countries' ],
|
||||
queryFn: ({ signal }) => fetchCountries(api!, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { getLocalizationApi } from '@jellyfin/sdk/lib/utils/api/localization-api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
|
||||
const fetchCultures = async (api: Api, options?: AxiosRequestConfig) => {
|
||||
const response = await getLocalizationApi(api).getCultures(options);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const useCultures = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [ 'Cultures' ],
|
||||
queryFn: ({ signal }) => fetchCultures(api!, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { getLibraryStructureApi } from '@jellyfin/sdk/lib/utils/api/library-structure-api';
|
||||
import { LibraryStructureApiRemoveVirtualFolderRequest } from '@jellyfin/sdk/lib/generated-client/api/library-structure-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
export const useRemoveVirtualFolder = () => {
|
||||
const { api } = useApi();
|
||||
return useMutation({
|
||||
mutationFn: (params: LibraryStructureApiRemoveVirtualFolderRequest) => (
|
||||
getLibraryStructureApi(api!)
|
||||
.removeVirtualFolder(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ 'VirtualFolders' ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { getLibraryStructureApi } from '@jellyfin/sdk/lib/utils/api/library-structure-api';
|
||||
import { LibraryStructureApiRenameVirtualFolderRequest } from '@jellyfin/sdk/lib/generated-client/api/library-structure-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
export const useRenameVirtualFolder = () => {
|
||||
const { api } = useApi();
|
||||
return useMutation({
|
||||
mutationFn: (params: LibraryStructureApiRenameVirtualFolderRequest) => (
|
||||
getLibraryStructureApi(api!)
|
||||
.renameVirtualFolder(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ 'VirtualFolders' ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import { getLibraryStructureApi } from '@jellyfin/sdk/lib/utils/api/library-structure-api';
|
||||
|
||||
const fetchVirtualFolders = async (api: Api, options?: AxiosRequestConfig) => {
|
||||
const response = await getLibraryStructureApi(api).getVirtualFolders(options);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const useVirtualFolders = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [ 'VirtualFolders' ],
|
||||
queryFn: ({ signal }) => fetchVirtualFolders(api!, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user