Compare commits
181 Commits
renovate/m
...
release-10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4b8aa0ed4 | ||
|
|
2413566327 | ||
|
|
c3c598e1f4 | ||
|
|
78a8642d4a | ||
|
|
76911509bc | ||
|
|
fb47403b72 | ||
|
|
2ab61b6d7b | ||
|
|
16a084b009 | ||
|
|
489731863b | ||
|
|
c335a3024e | ||
|
|
d3054985a5 | ||
|
|
0f598073a8 | ||
|
|
5d8ab6a99b | ||
|
|
84563176a1 | ||
|
|
4793223f5d | ||
|
|
8f64beec30 | ||
|
|
e8e4ff0ca9 | ||
|
|
9b3fc622c9 | ||
|
|
8994299490 | ||
|
|
36aa4dcd88 | ||
|
|
2f6781a7c2 | ||
|
|
7a07a79b12 | ||
|
|
88cc991fa5 | ||
|
|
8710de09d4 | ||
|
|
14ff6474f3 | ||
|
|
bfa53b57f4 | ||
|
|
4f17cebc02 | ||
|
|
700e72b409 | ||
|
|
cc011feffb | ||
|
|
d4b55ec67a | ||
|
|
d1aa2f3685 | ||
|
|
321822c57f | ||
|
|
e1deddcba1 | ||
|
|
b797ca4e1e | ||
|
|
74a209ed63 | ||
|
|
a553ef54f6 | ||
|
|
5b4cfbf410 | ||
|
|
e15d700d40 | ||
|
|
3d20694109 | ||
|
|
469abcc517 | ||
|
|
26df03b64c | ||
|
|
8f7974d5c6 | ||
|
|
206f70cf34 | ||
|
|
92caea08af | ||
|
|
a3872ffa25 | ||
|
|
63834e164a | ||
|
|
9e4e3b0106 | ||
|
|
a91e44828b | ||
|
|
92e8821003 | ||
|
|
306390179b | ||
|
|
4fb33badef | ||
|
|
1496542381 | ||
|
|
e22187247b | ||
|
|
b95dad4ba2 | ||
|
|
298399802a | ||
|
|
53a91227d7 | ||
|
|
d5ccc0ea37 | ||
|
|
c190f7b770 | ||
|
|
d69468a95a | ||
|
|
0c61dff5c5 | ||
|
|
5479d18082 | ||
|
|
e13668530d | ||
|
|
32d7a962ff | ||
|
|
f01b45fb04 | ||
|
|
38d9b6b528 | ||
|
|
77ebe19f40 | ||
|
|
6095248f6e | ||
|
|
5725554085 | ||
|
|
b98d74de33 | ||
|
|
bfceb43602 | ||
|
|
e54b19e2d4 | ||
|
|
2d459c75dc | ||
|
|
8a2789e316 | ||
|
|
99edacc08d | ||
|
|
377c0e3bdb | ||
|
|
d56ff77308 | ||
|
|
6391ccac1b | ||
|
|
08f7477813 | ||
|
|
2f7e751359 | ||
|
|
8a569d3692 | ||
|
|
df79405af0 | ||
|
|
be2c8a0afc | ||
|
|
6c588dcb8f | ||
|
|
00c7999ad4 | ||
|
|
1cdda2f8f6 | ||
|
|
ef42d9c8b4 | ||
|
|
8acc33705c | ||
|
|
7769137bff | ||
|
|
5728b5e5d4 | ||
|
|
13318e805f | ||
|
|
132c6ca858 | ||
|
|
f8e109bbc3 | ||
|
|
1562606b28 | ||
|
|
9fa5e438d9 | ||
|
|
b683becaf7 | ||
|
|
9cbab78cc1 | ||
|
|
05e5aa744d | ||
|
|
2e4986c497 | ||
|
|
c42d7b3f5d | ||
|
|
c60a0190ff | ||
|
|
b7bbadb2df | ||
|
|
837884c337 | ||
|
|
3143a08a33 | ||
|
|
bdcf6186ce | ||
|
|
3d9d1f5ae3 | ||
|
|
bd0c43d6c8 | ||
|
|
c5934d08de | ||
|
|
dec3d2ac19 | ||
|
|
25354caf8f | ||
|
|
3897704a40 | ||
|
|
133273a3e8 | ||
|
|
67996d3a96 | ||
|
|
048d3f1e2c | ||
|
|
e8f9bfcf57 | ||
|
|
22ae941a9a | ||
|
|
5c55e458dd | ||
|
|
f35893d0a1 | ||
|
|
2f2844c33f | ||
|
|
026893e78b | ||
|
|
1ab22fc258 | ||
|
|
ed3671a536 | ||
|
|
f43402d284 | ||
|
|
bfd1e9123a | ||
|
|
13e6766e09 | ||
|
|
ca45ddfd18 | ||
|
|
02f9d28423 | ||
|
|
3c287626f2 | ||
|
|
a1b78b3557 | ||
|
|
ef68930e52 | ||
|
|
4e18e36fa0 | ||
|
|
cee58c742e | ||
|
|
caa677b643 | ||
|
|
9ab418c69d | ||
|
|
d037fc12c3 | ||
|
|
281298b55d | ||
|
|
cafbd93ecd | ||
|
|
6f24b334f9 | ||
|
|
9c18db984c | ||
|
|
30871d0a21 | ||
|
|
bd98a7ae15 | ||
|
|
be5a12a263 | ||
|
|
35dd9a1970 | ||
|
|
0271bf4c82 | ||
|
|
2b80c59ab7 | ||
|
|
928663c3e8 | ||
|
|
ae76e82f7d | ||
|
|
e69dcbfa03 | ||
|
|
66e641199e | ||
|
|
c54a203128 | ||
|
|
5a9b511a16 | ||
|
|
b012884a8e | ||
|
|
ef24aa319f | ||
|
|
828a49f6bd | ||
|
|
81fdf371f1 | ||
|
|
f3ffd327fc | ||
|
|
43c9f853e3 | ||
|
|
965182e538 | ||
|
|
a6307eb8c8 | ||
|
|
7de923ba74 | ||
|
|
c5cc093fba | ||
|
|
6d37cfcd1b | ||
|
|
9ff9f05a26 | ||
|
|
2ed3965197 | ||
|
|
ad8868a996 | ||
|
|
1761dd1cfc | ||
|
|
d365dd1b98 | ||
|
|
05de692675 | ||
|
|
89b5e44870 | ||
|
|
be324cca22 | ||
|
|
3311a1407f | ||
|
|
d4eae7bde6 | ||
|
|
c0a9f8b544 | ||
|
|
18ea570ea1 | ||
|
|
7ccc494a5b | ||
|
|
71ab6fea5d | ||
|
|
acde0685b5 | ||
|
|
3062f0f38c | ||
|
|
37be617523 | ||
|
|
6f7ece6592 | ||
|
|
ae5afd9ea7 | ||
|
|
d4d84d0a18 |
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"ecmaVersion": "es5",
|
||||
"modules": "false",
|
||||
"files": "./dist/**/*.js",
|
||||
"not": [
|
||||
"./dist/libraries/pdf.worker.js",
|
||||
|
||||
5
.eslintignore
Normal file
5
.eslintignore
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
coverage
|
||||
dist
|
||||
.idea
|
||||
.vscode
|
||||
301
.eslintrc.js
Normal file
301
.eslintrc.js
Normal file
@@ -0,0 +1,301 @@
|
||||
const restrictedGlobals = require('confusing-browser-globals');
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: [
|
||||
'@stylistic',
|
||||
'@typescript-eslint',
|
||||
'react',
|
||||
'import',
|
||||
'sonarjs'
|
||||
],
|
||||
env: {
|
||||
node: true,
|
||||
es6: true,
|
||||
es2017: true,
|
||||
es2020: true
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:import/errors',
|
||||
'plugin:@eslint-community/eslint-comments/recommended',
|
||||
'plugin:compat/recommended',
|
||||
'plugin:sonarjs/recommended'
|
||||
],
|
||||
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'],
|
||||
'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'] }],
|
||||
'one-var': ['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'],
|
||||
'radix': ['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'],
|
||||
|
||||
'@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-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']
|
||||
},
|
||||
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-community/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]
|
||||
* [ ] ...
|
||||
12
.github/renovate.json
vendored
12
.github/renovate.json
vendored
@@ -4,25 +4,17 @@
|
||||
"github>jellyfin/.github//renovate-presets/nodejs",
|
||||
":dependencyDashboard"
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": false
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackageNames": [ "@jellyfin/sdk" ],
|
||||
"followTag": "unstable",
|
||||
"minimumReleaseAge": null,
|
||||
"prPriority": 5,
|
||||
"schedule": [ "after 7:00 am" ]
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [ "dompurify" ],
|
||||
"matchUpdateTypes": [ "major" ],
|
||||
"matchPackageNames": ["dompurify"],
|
||||
"matchUpdateTypes": ["major"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [ "hls.js" ],
|
||||
"prPriority": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
2
.github/workflows/__automation.yml
vendored
2
.github/workflows/__automation.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
name: Merge conflict labeling 🏷️
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
|
||||
- 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.'
|
||||
|
||||
8
.github/workflows/__codeql.yml
vendored
8
.github/workflows/__codeql.yml
vendored
@@ -20,21 +20,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository ⬇️
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.commit }}
|
||||
show-progress: false
|
||||
|
||||
- name: Initialize CodeQL 🛠️
|
||||
uses: github/codeql-action/init@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3.31.4
|
||||
uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
with:
|
||||
queries: security-and-quality
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild 📦
|
||||
uses: github/codeql-action/autobuild@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3.31.4
|
||||
uses: github/codeql-action/autobuild@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
|
||||
- name: Perform CodeQL Analysis 🧪
|
||||
uses: github/codeql-action/analyze@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3.31.4
|
||||
uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
4
.github/workflows/__deploy.yml
vendored
4
.github/workflows/__deploy.yml
vendored
@@ -29,13 +29,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download workflow artifact ⬇️
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}
|
||||
path: dist
|
||||
|
||||
- name: Publish to Cloudflare Pages 📃
|
||||
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
|
||||
uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3.11.0
|
||||
id: cf
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
2
.github/workflows/__job_messages.yml
vendored
2
.github/workflows/__job_messages.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Push comment to Pull Request 🔼
|
||||
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1
|
||||
uses: thollander/actions-comment-pull-request@e2c37e53a7d2227b61585343765f73a9ca57eda9 # v3.0.0
|
||||
if: ${{ inputs.comment && steps.compose.conclusion == 'success' }}
|
||||
with:
|
||||
github-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
6
.github/workflows/__package.yml
vendored
6
.github/workflows/__package.yml
vendored
@@ -14,12 +14,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.commit || github.sha }}
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
mv dist/config.tmp.json dist/config.json
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: frontend
|
||||
path: dist
|
||||
|
||||
8
.github/workflows/__quality_checks.yml
vendored
8
.github/workflows/__quality_checks.yml
vendored
@@ -14,13 +14,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.commit }}
|
||||
show-progress: false
|
||||
|
||||
- name: Scan
|
||||
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
|
||||
uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5
|
||||
with:
|
||||
## Workaround from https://github.com/actions/dependency-review-action/issues/456
|
||||
## TODO: Remove when necessary
|
||||
@@ -42,13 +42,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout ⬇️
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.commit }}
|
||||
show-progress: false
|
||||
|
||||
- name: Setup node environment ⚙️
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
|
||||
6
.github/workflows/pull_request.yml
vendored
6
.github/workflows/pull_request.yml
vendored
@@ -80,12 +80,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
@@ -95,6 +95,6 @@ jobs:
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run eslint
|
||||
uses: CatChen/eslint-suggestion-action@4ee415529307a8ca0260b4a3775484802523e5af # v4.1.19
|
||||
uses: CatChen/eslint-suggestion-action@9c12109c4943f26f0676b71c9c10e456748872cf # v4.1.7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
2
.github/workflows/push.yml
vendored
2
.github/workflows/push.yml
vendored
@@ -55,4 +55,4 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
branch: ${{ github.ref_name }}
|
||||
comment: false
|
||||
comment:
|
||||
|
||||
28
.github/workflows/schedule.yml
vendored
28
.github/workflows/schedule.yml
vendored
@@ -10,17 +10,18 @@ permissions:
|
||||
|
||||
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 +30,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.
|
||||
|
||||
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"
|
||||
},
|
||||
|
||||
@@ -93,16 +93,6 @@
|
||||
- [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)
|
||||
|
||||
@@ -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*
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
21937
package-lock.json
generated
21937
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
150
package.json
150
package.json
@@ -1,113 +1,106 @@
|
||||
{
|
||||
"name": "jellyfin-web",
|
||||
"version": "10.12.0",
|
||||
"version": "10.10.7",
|
||||
"description": "Web interface for Jellyfin",
|
||||
"repository": "https://github.com/jellyfin/jellyfin-web",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.27.4",
|
||||
"@babel/plugin-transform-modules-umd": "7.27.1",
|
||||
"@babel/preset-env": "7.27.2",
|
||||
"@babel/preset-react": "7.27.1",
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "4.5.0",
|
||||
"@eslint/js": "9.30.1",
|
||||
"@stylistic/eslint-plugin": "5.6.1",
|
||||
"@stylistic/stylelint-plugin": "3.1.3",
|
||||
"@babel/core": "7.25.8",
|
||||
"@babel/plugin-transform-modules-umd": "7.25.7",
|
||||
"@babel/preset-env": "7.25.8",
|
||||
"@babel/preset-react": "7.25.7",
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "4.4.0",
|
||||
"@stylistic/eslint-plugin": "2.9.0",
|
||||
"@types/dompurify": "3.0.5",
|
||||
"@types/escape-html": "1.0.4",
|
||||
"@types/loadable__component": "5.13.9",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/markdown-it": "14.1.2",
|
||||
"@types/react": "18.3.23",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/react-lazy-load-image-component": "1.6.4",
|
||||
"@types/react": "18.3.11",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@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.1.3",
|
||||
"autoprefixer": "10.4.20",
|
||||
"babel-loader": "9.2.1",
|
||||
"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": "7.0.1",
|
||||
"eslint-plugin-sonarjs": "3.0.4",
|
||||
"expose-loader": "5.0.1",
|
||||
"fast-glob": "3.3.3",
|
||||
"fork-ts-checker-webpack-plugin": "9.1.0",
|
||||
"globals": "16.2.0",
|
||||
"cssnano": "7.0.6",
|
||||
"es-check": "7.2.1",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-plugin-compat": "4.2.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.0",
|
||||
"eslint-plugin-react": "7.37.1",
|
||||
"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": "25.0.1",
|
||||
"mini-css-extract-plugin": "2.9.1",
|
||||
"postcss": "8.4.47",
|
||||
"postcss-loader": "8.1.1",
|
||||
"postcss-preset-env": "10.2.3",
|
||||
"postcss-preset-env": "10.0.7",
|
||||
"postcss-scss": "4.0.9",
|
||||
"sass": "1.89.2",
|
||||
"sass-loader": "16.0.5",
|
||||
"sass": "1.79.5",
|
||||
"sass-loader": "16.0.2",
|
||||
"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.6.3",
|
||||
"vitest": "2.1.3",
|
||||
"webpack": "5.95.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.1.0",
|
||||
"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",
|
||||
"@emotion/react": "11.13.3",
|
||||
"@emotion/styled": "11.13.0",
|
||||
"@fontsource/noto-sans": "5.1.0",
|
||||
"@fontsource/noto-sans-hk": "5.1.0",
|
||||
"@fontsource/noto-sans-jp": "5.1.0",
|
||||
"@fontsource/noto-sans-kr": "5.1.0",
|
||||
"@fontsource/noto-sans-sc": "5.1.0",
|
||||
"@fontsource/noto-sans-tc": "5.1.0",
|
||||
"@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",
|
||||
"@jellyfin/sdk": "0.0.0-unstable.202410250501",
|
||||
"@mui/icons-material": "5.16.7",
|
||||
"@mui/material": "5.16.7",
|
||||
"@mui/x-date-pickers": "7.20.0",
|
||||
"@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",
|
||||
"@tanstack/react-query": "5.59.13",
|
||||
"@tanstack/react-query-devtools": "5.59.13",
|
||||
"@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.38.1",
|
||||
"date-fns": "2.30.0",
|
||||
"dompurify": "2.5.8",
|
||||
"element-closest-polyfill": "1.0.7",
|
||||
"dompurify": "2.5.7",
|
||||
"epubjs": "0.3.93",
|
||||
"escape-html": "1.0.3",
|
||||
"fast-text-encoding": "1.0.6",
|
||||
"flv.js": "1.6.2",
|
||||
"headroom.js": "0.12.0",
|
||||
"history": "5.3.0",
|
||||
"hls.js": "1.6.13",
|
||||
"hls.js": "1.5.16",
|
||||
"intersection-observer": "0.12.2",
|
||||
"jellyfin-apiclient": "1.11.0",
|
||||
"jquery": "3.7.1",
|
||||
@@ -117,25 +110,24 @@
|
||||
"lodash-es": "4.17.21",
|
||||
"markdown-it": "14.1.0",
|
||||
"material-design-icons-iconfont": "6.7.0",
|
||||
"material-react-table": "3.2.1",
|
||||
"material-react-table": "2.13.3",
|
||||
"native-promise-only": "0.8.1",
|
||||
"pdfjs-dist": "3.11.174",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
"react": "18.3.1",
|
||||
"react-blurhash": "0.3.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-lazy-load-image-component": "1.6.3",
|
||||
"react-router-dom": "6.30.1",
|
||||
"react-lazy-load-image-component": "1.6.2",
|
||||
"react-router-dom": "6.27.0",
|
||||
"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.3",
|
||||
"swiper": "11.1.14",
|
||||
"usehooks-ts": "3.1.0",
|
||||
"webcomponents.js": "0.7.24",
|
||||
"whatwg-fetch": "3.6.20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sass-embedded": "1.89.2"
|
||||
"sass-embedded": "1.79.5"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 Firefox versions",
|
||||
@@ -162,14 +154,14 @@
|
||||
"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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
RouterProvider,
|
||||
@@ -10,19 +10,14 @@ 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([
|
||||
{
|
||||
@@ -30,7 +25,6 @@ const router = createHashRouter([
|
||||
children: [
|
||||
...(isExperimentalLayout ? EXPERIMENTAL_APP_ROUTES : STABLE_APP_ROUTES),
|
||||
...DASHBOARD_APP_ROUTES,
|
||||
...WIZARD_APP_ROUTES,
|
||||
{
|
||||
path: '!/*',
|
||||
Component: BangRedirect
|
||||
@@ -55,15 +49,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>
|
||||
);
|
||||
}
|
||||
|
||||
5
src/apiclient.d.ts
vendored
5
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>;
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,21 +9,19 @@ 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();
|
||||
@@ -31,8 +29,8 @@ export const Component: FC = () => {
|
||||
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(() => {
|
||||
@@ -67,18 +65,10 @@ export const Component: FC = () => {
|
||||
}}
|
||||
>
|
||||
<AppToolbar
|
||||
isBackButtonAvailable={appRouter.canGoBack()}
|
||||
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
|
||||
isDrawerOpen={isDrawerOpen}
|
||||
onDrawerButtonClick={onToggleDrawer}
|
||||
buttons={
|
||||
<HelpButton />
|
||||
}
|
||||
>
|
||||
{isMetadataManager && (
|
||||
<ServerButton />
|
||||
)}
|
||||
|
||||
<AppTabs isDrawerOpen={isDrawerOpen} />
|
||||
</AppToolbar>
|
||||
</AppBar>
|
||||
@@ -107,7 +97,6 @@ export const Component: FC = () => {
|
||||
</AppBody>
|
||||
</Box>
|
||||
</Box>
|
||||
<ThemeCss dashboard />
|
||||
</LocalizationProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,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,11 +1,4 @@
|
||||
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';
|
||||
@@ -76,12 +69,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>
|
||||
|
||||
@@ -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/'
|
||||
}
|
||||
];
|
||||
@@ -7,10 +7,15 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
const fetchLogEntries = async (
|
||||
api: Api,
|
||||
api?: Api,
|
||||
requestParams?: ActivityLogApiGetLogEntriesRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
if (!api) {
|
||||
console.warn('[fetchLogEntries] No API instance available');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await getActivityLogApi(api).getLogEntries(requestParams, {
|
||||
signal: options?.signal
|
||||
});
|
||||
@@ -18,15 +23,14 @@ const fetchLogEntries = async (
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const useLogEntries = (
|
||||
export const useLogEntires = (
|
||||
requestParams: ActivityLogApiGetLogEntriesRequest
|
||||
) => {
|
||||
const { api } = useApi();
|
||||
return useQuery({
|
||||
queryKey: ['ActivityLogEntries', requestParams],
|
||||
queryKey: ['LogEntries', requestParams],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchLogEntries(api!, requestParams, { signal }),
|
||||
enabled: !!api,
|
||||
refetchOnMount: false
|
||||
fetchLogEntries(api, requestParams, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import IconButton from '@mui/material/IconButton/IconButton';
|
||||
import PermMedia from '@mui/icons-material/PermMedia';
|
||||
import React, { type FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
@@ -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,17 +1,30 @@
|
||||
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import React, { useMemo } from 'react';
|
||||
import React 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]);
|
||||
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={getLogLevelColor(level)}
|
||||
color={color}
|
||||
label={levelText}
|
||||
title={levelText}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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 IconButton from '@mui/material/IconButton/IconButton';
|
||||
import React, { type FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
@@ -8,21 +7,14 @@ import UserAvatar from 'components/UserAvatar';
|
||||
|
||||
interface UserAvatarButtonProps {
|
||||
user?: UserDto
|
||||
sx?: SxProps<Theme>
|
||||
}
|
||||
|
||||
const UserAvatarButton: FC<UserAvatarButtonProps> = ({
|
||||
user,
|
||||
sx
|
||||
}) => (
|
||||
const UserAvatarButton: FC<UserAvatarButtonProps> = ({ user }) => (
|
||||
user?.Id ? (
|
||||
<IconButton
|
||||
size='large'
|
||||
color='inherit'
|
||||
sx={{
|
||||
padding: 0,
|
||||
...sx
|
||||
}}
|
||||
sx={{ padding: 0 }}
|
||||
title={user.Name || undefined}
|
||||
component={Link}
|
||||
to={`/dashboard/users/profile?userId=${user.Id}`}
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
@@ -1,237 +0,0 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import type { VirtualFolderInfo } from '@jellyfin/sdk/lib/generated-client/models/virtual-folder-info';
|
||||
import BaseCard from 'apps/dashboard/components/BaseCard';
|
||||
import getCollectionTypeOptions from '../utils/collectionTypeOptions';
|
||||
import globalize from 'lib/globalize';
|
||||
import Icon from '@mui/material/Icon';
|
||||
import { getLibraryIcon } from 'utils/image';
|
||||
import MediaLibraryEditor from 'components/mediaLibraryEditor/mediaLibraryEditor';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import Folder from '@mui/icons-material/Folder';
|
||||
import ImageIcon from '@mui/icons-material/Image';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import imageeditor from 'components/imageeditor/imageeditor';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import InputDialog from 'components/InputDialog';
|
||||
import { useRenameVirtualFolder } from '../api/useRenameVirtualFolder';
|
||||
import RefreshDialog from 'components/refreshdialog/refreshdialog';
|
||||
import ConfirmDialog from 'components/ConfirmDialog';
|
||||
import { useRemoveVirtualFolder } from '../api/useRemoveVirtualFolder';
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api/image-api';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||
import dom from 'utils/dom';
|
||||
|
||||
type LibraryCardProps = {
|
||||
virtualFolder: VirtualFolderInfo;
|
||||
};
|
||||
|
||||
const LibraryCard = ({ virtualFolder }: LibraryCardProps) => {
|
||||
const { api } = useApi();
|
||||
const actionRef = useRef<HTMLButtonElement | null>(null);
|
||||
const [ anchorEl, setAnchorEl ] = useState<HTMLElement | null>(null);
|
||||
const [ isMenuOpen, setIsMenuOpen ] = useState(false);
|
||||
const [ isRenameLibraryDialogOpen, setIsRenameLibraryDialogOpen ] = useState(false);
|
||||
const [ isConfirmDeleteDialogOpen, setIsConfirmDeleteDialogOpen ] = useState(false);
|
||||
const renameVirtualFolder = useRenameVirtualFolder();
|
||||
const removeVirtualFolder = useRemoveVirtualFolder();
|
||||
|
||||
const imageUrl = useMemo(() => {
|
||||
if (virtualFolder.PrimaryImageItemId && virtualFolder.ItemId && api) {
|
||||
return getImageApi(api)
|
||||
.getItemImageUrlById(virtualFolder.ItemId, ImageType.Primary, {
|
||||
maxWidth: Math.round(dom.getScreenWidth() * 0.40)
|
||||
});
|
||||
}
|
||||
}, [ api, virtualFolder ]);
|
||||
|
||||
const typeName = getCollectionTypeOptions().filter(function (t) {
|
||||
return t.value == virtualFolder.CollectionType;
|
||||
})[0]?.name || globalize.translate('Other');
|
||||
|
||||
const openRenameDialog = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
setIsMenuOpen(false);
|
||||
setIsRenameLibraryDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const hideRenameLibraryDialog = useCallback(() => {
|
||||
setIsRenameLibraryDialogOpen(false);
|
||||
}, []);
|
||||
|
||||
const onMenuClose = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
setIsMenuOpen(false);
|
||||
}, []);
|
||||
|
||||
const onActionClick = useCallback(() => {
|
||||
setAnchorEl(actionRef.current);
|
||||
setIsMenuOpen(true);
|
||||
}, []);
|
||||
|
||||
const renameLibrary = useCallback((newName: string) => {
|
||||
if (virtualFolder.Name) {
|
||||
renameVirtualFolder.mutate({
|
||||
refreshLibrary: true,
|
||||
newName: newName,
|
||||
name: virtualFolder.Name
|
||||
}, {
|
||||
onSettled: () => {
|
||||
hideRenameLibraryDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [ renameVirtualFolder, virtualFolder, hideRenameLibraryDialog ]);
|
||||
|
||||
const showRefreshDialog = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
setIsMenuOpen(false);
|
||||
|
||||
void new RefreshDialog({
|
||||
itemIds: [ virtualFolder.ItemId ],
|
||||
serverId: ServerConnections.currentApiClient()?.serverId(),
|
||||
mode: 'scan'
|
||||
}).show();
|
||||
}, [ virtualFolder ]);
|
||||
|
||||
const showMediaLibraryEditor = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
setIsMenuOpen(false);
|
||||
|
||||
const mediaLibraryEditor = new MediaLibraryEditor({
|
||||
library: virtualFolder
|
||||
}) as Promise<boolean>;
|
||||
|
||||
void mediaLibraryEditor.then((hasChanges: boolean) => {
|
||||
if (hasChanges) {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ['VirtualFolders']
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [ virtualFolder ]);
|
||||
|
||||
const showImageEditor = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
setIsMenuOpen(false);
|
||||
|
||||
void imageeditor.show({
|
||||
itemId: virtualFolder.ItemId,
|
||||
serverId: ServerConnections.currentApiClient()?.serverId()
|
||||
}).then(() => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ['VirtualFolders']
|
||||
});
|
||||
}).catch(() => {
|
||||
/* pop up closed */
|
||||
});
|
||||
}, [ virtualFolder ]);
|
||||
|
||||
const showDeleteLibraryDialog = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
setIsMenuOpen(false);
|
||||
setIsConfirmDeleteDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const onCancelDeleteLibrary = useCallback(() => {
|
||||
setIsConfirmDeleteDialogOpen(false);
|
||||
}, []);
|
||||
|
||||
const onConfirmDeleteLibrary = useCallback(() => {
|
||||
if (virtualFolder.Name) {
|
||||
removeVirtualFolder.mutate({
|
||||
name: virtualFolder.Name,
|
||||
refreshLibrary: true
|
||||
}, {
|
||||
onSettled: () => {
|
||||
setIsConfirmDeleteDialogOpen(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [ virtualFolder, removeVirtualFolder ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<InputDialog
|
||||
title={globalize.translate('ButtonRename')}
|
||||
open={isRenameLibraryDialogOpen}
|
||||
onClose={hideRenameLibraryDialog}
|
||||
label={globalize.translate('LabelNewName')}
|
||||
helperText={globalize.translate('MessageRenameMediaFolder')}
|
||||
initialText={virtualFolder.Name || ''}
|
||||
confirmButtonText={globalize.translate('ButtonRename')}
|
||||
onConfirm={renameLibrary}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={isConfirmDeleteDialogOpen}
|
||||
title={globalize.translate('HeaderRemoveMediaFolder')}
|
||||
text={
|
||||
globalize.translate('MessageAreYouSureYouWishToRemoveMediaFolder') + '\n\n'
|
||||
+ globalize.translate('MessageTheFollowingLocationWillBeRemovedFromLibrary') + '\n\n'
|
||||
+ virtualFolder.Locations?.join('\n')
|
||||
}
|
||||
confirmButtonText={globalize.translate('Delete')}
|
||||
confirmButtonColor='error'
|
||||
onConfirm={onConfirmDeleteLibrary}
|
||||
onCancel={onCancelDeleteLibrary}
|
||||
/>
|
||||
|
||||
<BaseCard
|
||||
title={virtualFolder.Name || ''}
|
||||
text={typeName}
|
||||
image={imageUrl}
|
||||
icon={<Icon sx={{ fontSize: 70 }}>{getLibraryIcon(virtualFolder.CollectionType)}</Icon>}
|
||||
action={true}
|
||||
actionRef={actionRef}
|
||||
onActionClick={onActionClick}
|
||||
onClick={showMediaLibraryEditor}
|
||||
height={260}
|
||||
/>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={isMenuOpen}
|
||||
onClose={onMenuClose}
|
||||
>
|
||||
<MenuItem onClick={showImageEditor}>
|
||||
<ListItemIcon>
|
||||
<ImageIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{globalize.translate('EditImages')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={showMediaLibraryEditor}>
|
||||
<ListItemIcon>
|
||||
<Folder />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{globalize.translate('ManageLibrary')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={openRenameDialog}>
|
||||
<ListItemIcon>
|
||||
<EditIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{globalize.translate('ButtonRename')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={showRefreshDialog}>
|
||||
<ListItemIcon>
|
||||
<RefreshIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{globalize.translate('ScanLibrary')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={showDeleteLibraryDialog}>
|
||||
<ListItemIcon>
|
||||
<DeleteIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{globalize.translate('ButtonRemove')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryCard;
|
||||
@@ -1,31 +0,0 @@
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
const getCollectionTypeOptions = () => {
|
||||
return [{
|
||||
name: '',
|
||||
value: ''
|
||||
}, {
|
||||
name: globalize.translate('Movies'),
|
||||
value: 'movies'
|
||||
}, {
|
||||
name: globalize.translate('TabMusic'),
|
||||
value: 'music'
|
||||
}, {
|
||||
name: globalize.translate('Shows'),
|
||||
value: 'tvshows'
|
||||
}, {
|
||||
name: globalize.translate('Books'),
|
||||
value: 'books'
|
||||
}, {
|
||||
name: globalize.translate('HomeVideosPhotos'),
|
||||
value: 'homevideos'
|
||||
}, {
|
||||
name: globalize.translate('MusicVideos'),
|
||||
value: 'musicvideos'
|
||||
}, {
|
||||
name: globalize.translate('MixedMoviesShows'),
|
||||
value: 'mixed'
|
||||
}];
|
||||
};
|
||||
|
||||
export default getCollectionTypeOptions;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { ImageResolution } from '@jellyfin/sdk/lib/generated-client/models/image-resolution';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
export function getImageResolutionOptions() {
|
||||
return [
|
||||
{
|
||||
name: globalize.translate('ResolutionMatchSource'),
|
||||
value: ImageResolution.MatchSource
|
||||
},
|
||||
{ name: '2160p', value: ImageResolution.P2160 },
|
||||
{ name: '1440p', value: ImageResolution.P1440 },
|
||||
{ name: '1080p', value: ImageResolution.P1080 },
|
||||
{ name: '720p', value: ImageResolution.P720 },
|
||||
{ name: '480p', value: ImageResolution.P480 },
|
||||
{ name: '360p', value: ImageResolution.P360 },
|
||||
{ name: '240p', value: ImageResolution.P240 },
|
||||
{ name: '144p', value: ImageResolution.P144 }
|
||||
];
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api';
|
||||
import { LiveTvApiDeleteListingProviderRequest } from '@jellyfin/sdk/lib/generated-client/api/live-tv-api';
|
||||
|
||||
export const useDeleteProvider = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (params: LiveTvApiDeleteListingProviderRequest) => (
|
||||
getLiveTvApi(api!)
|
||||
.deleteListingProvider(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ 'NamedConfiguration', 'livetv' ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api';
|
||||
import { LiveTvApiDeleteTunerHostRequest } from '@jellyfin/sdk/lib/generated-client/api/live-tv-api';
|
||||
|
||||
export const useDeleteTuner = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (params: LiveTvApiDeleteTunerHostRequest) => (
|
||||
getLiveTvApi(api!)
|
||||
.deleteTunerHost(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ 'NamedConfiguration', 'livetv' ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,138 +0,0 @@
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import type { ListingsProviderInfo } from '@jellyfin/sdk/lib/generated-client/models/listings-provider-info';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemAvatar from '@mui/material/ListItemAvatar';
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import DvrIcon from '@mui/icons-material/Dvr';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import getProviderConfigurationUrl from '../utils/getProviderConfigurationUrl';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import getProviderName from '../utils/getProviderName';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ConfirmDialog from 'components/ConfirmDialog';
|
||||
import globalize from 'lib/globalize';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import LocationSearchingIcon from '@mui/icons-material/LocationSearching';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import ChannelMapper from 'components/channelMapper/channelMapper';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import { useDeleteProvider } from '../api/useDeleteProvider';
|
||||
|
||||
interface ProviderProps {
|
||||
provider: ListingsProviderInfo
|
||||
}
|
||||
|
||||
const Provider = ({ provider }: ProviderProps) => {
|
||||
const [ isDeleteProviderDialogOpen, setIsDeleteProviderDialogOpen ] = useState(false);
|
||||
const actionsRef = useRef<HTMLButtonElement | null>(null);
|
||||
const [ anchorEl, setAnchorEl ] = useState<HTMLButtonElement | null>(null);
|
||||
const [ isMenuOpen, setIsMenuOpen ] = useState(false);
|
||||
const deleteProvider = useDeleteProvider();
|
||||
|
||||
const showChannelMapper = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
setIsMenuOpen(false);
|
||||
void new ChannelMapper({
|
||||
serverId: ServerConnections.currentApiClient()?.serverId(),
|
||||
providerId: provider.Id
|
||||
}).show();
|
||||
}, [ provider ]);
|
||||
|
||||
const showContextMenu = useCallback(() => {
|
||||
setAnchorEl(actionsRef.current);
|
||||
setIsMenuOpen(true);
|
||||
}, []);
|
||||
|
||||
const showDeleteDialog = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
setIsMenuOpen(false);
|
||||
setIsDeleteProviderDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const onDeleteProviderDialogCancel = useCallback(() => {
|
||||
setIsDeleteProviderDialogOpen(false);
|
||||
}, []);
|
||||
|
||||
const onMenuClose = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
setIsMenuOpen(false);
|
||||
}, []);
|
||||
|
||||
const onConfirmDelete = useCallback(() => {
|
||||
if (provider.Id) {
|
||||
deleteProvider.mutate({
|
||||
id: provider.Id
|
||||
}, {
|
||||
onSettled: () => {
|
||||
setIsDeleteProviderDialogOpen(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [ deleteProvider, provider ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmDialog
|
||||
open={isDeleteProviderDialogOpen}
|
||||
title={globalize.translate('HeaderDeleteProvider')}
|
||||
text={globalize.translate('MessageConfirmDeleteGuideProvider')}
|
||||
onCancel={onDeleteProviderDialogCancel}
|
||||
onConfirm={onConfirmDelete}
|
||||
confirmButtonText={globalize.translate('Delete')}
|
||||
confirmButtonColor='error'
|
||||
/>
|
||||
<ListItem
|
||||
disablePadding key={provider.Id}
|
||||
secondaryAction={
|
||||
<IconButton ref={actionsRef} onClick={showContextMenu}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<ListItemLink to={getProviderConfigurationUrl(provider.Type || '') + '&id=' + provider.Id}>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: 'primary.main' }}>
|
||||
<DvrIcon sx={{ color: '#fff' }} />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={getProviderName(provider.Type)}
|
||||
secondary={provider.Path || provider.ListingsId}
|
||||
slotProps={{
|
||||
primary: {
|
||||
variant: 'h3'
|
||||
},
|
||||
secondary: {
|
||||
variant: 'body1'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={isMenuOpen}
|
||||
onClose={onMenuClose}
|
||||
>
|
||||
<MenuItem onClick={showChannelMapper}>
|
||||
<ListItemIcon>
|
||||
<LocationSearchingIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{globalize.translate('MapChannels')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={showDeleteDialog}>
|
||||
<ListItemIcon>
|
||||
<DeleteIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{globalize.translate('Delete')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Provider;
|
||||
@@ -1,109 +0,0 @@
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import type { TunerHostInfo } from '@jellyfin/sdk/lib/generated-client/models/tuner-host-info';
|
||||
import BaseCard from 'apps/dashboard/components/BaseCard';
|
||||
import DvrIcon from '@mui/icons-material/Dvr';
|
||||
import getTunerName from '../utils/getTunerName';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import globalize from 'lib/globalize';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ConfirmDialog from 'components/ConfirmDialog';
|
||||
import { useDeleteTuner } from '../api/useDeleteTuner';
|
||||
|
||||
interface TunerDeviceCardProps {
|
||||
tunerHost: TunerHostInfo;
|
||||
}
|
||||
|
||||
const TunerDeviceCard = ({ tunerHost }: TunerDeviceCardProps) => {
|
||||
const navigate = useNavigate();
|
||||
const actionRef = useRef<HTMLButtonElement | null>(null);
|
||||
const [ anchorEl, setAnchorEl ] = useState<HTMLElement | null>(null);
|
||||
const [ isMenuOpen, setIsMenuOpen ] = useState(false);
|
||||
const [ isConfirmDeleteDialogOpen, setIsConfirmDeleteDialogOpen ] = useState(false);
|
||||
const deleteTuner = useDeleteTuner();
|
||||
|
||||
const navigateToEditPage = useCallback(() => {
|
||||
navigate(`/dashboard/livetv/tuner?id=${tunerHost.Id}`);
|
||||
}, [ navigate, tunerHost ]);
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
if (tunerHost.Id) {
|
||||
deleteTuner.mutate({
|
||||
id: tunerHost.Id
|
||||
}, {
|
||||
onSettled: () => {
|
||||
setIsConfirmDeleteDialogOpen(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [ deleteTuner, tunerHost ]);
|
||||
|
||||
const showDeleteDialog = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
setIsMenuOpen(false);
|
||||
setIsConfirmDeleteDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const onDeleteDialogClose = useCallback(() => {
|
||||
setIsConfirmDeleteDialogOpen(false);
|
||||
}, []);
|
||||
|
||||
const onActionClick = useCallback(() => {
|
||||
setAnchorEl(actionRef.current);
|
||||
setIsMenuOpen(true);
|
||||
}, []);
|
||||
|
||||
const onMenuClose = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
setIsMenuOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmDialog
|
||||
open={isConfirmDeleteDialogOpen}
|
||||
title={globalize.translate('HeaderDeleteDevice')}
|
||||
text={globalize.translate('MessageConfirmDeleteTunerDevice')}
|
||||
onCancel={onDeleteDialogClose}
|
||||
onConfirm={onDelete}
|
||||
confirmButtonColor='error'
|
||||
confirmButtonText={globalize.translate('Delete')}
|
||||
/>
|
||||
|
||||
<BaseCard
|
||||
title={tunerHost.FriendlyName || getTunerName(tunerHost.Type) || ''}
|
||||
text={tunerHost.Url || ''}
|
||||
icon={<DvrIcon sx={{ fontSize: 70 }} />}
|
||||
action={true}
|
||||
actionRef={actionRef}
|
||||
onActionClick={onActionClick}
|
||||
onClick={navigateToEditPage}
|
||||
/>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={isMenuOpen}
|
||||
onClose={onMenuClose}
|
||||
>
|
||||
<MenuItem onClick={navigateToEditPage}>
|
||||
<ListItemIcon>
|
||||
<EditIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{globalize.translate('Edit')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={showDeleteDialog}>
|
||||
<ListItemIcon>
|
||||
<DeleteIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{globalize.translate('Delete')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TunerDeviceCard;
|
||||
@@ -1,10 +0,0 @@
|
||||
const getProviderConfigurationUrl = (providerId: string) => {
|
||||
switch (providerId?.toLowerCase()) {
|
||||
case 'xmltv':
|
||||
return '/dashboard/livetv/guide?type=xmltv';
|
||||
case 'schedulesdirect':
|
||||
return '/dashboard/livetv/guide?type=schedulesdirect';
|
||||
}
|
||||
};
|
||||
|
||||
export default getProviderConfigurationUrl;
|
||||
@@ -1,12 +0,0 @@
|
||||
const getProviderName = (providerId: string | null | undefined) => {
|
||||
switch (providerId?.toLowerCase()) {
|
||||
case 'schedulesdirect':
|
||||
return 'Schedules Direct';
|
||||
case 'xmltv':
|
||||
return 'XMLTV';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
export default getProviderName;
|
||||
@@ -1,16 +0,0 @@
|
||||
const getTunerName = (providerId: string | null | undefined) => {
|
||||
switch (providerId?.toLowerCase()) {
|
||||
case 'm3u':
|
||||
return 'M3U';
|
||||
case 'hdhomerun':
|
||||
return 'HDHomeRun';
|
||||
case 'hauppauge':
|
||||
return 'Hauppauge';
|
||||
case 'satip':
|
||||
return 'DVB';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
export default getTunerName;
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
|
||||
const fetchServerLog = async (
|
||||
api: Api,
|
||||
name: string,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
const response = await getSystemApi(api).getLogFile({ name }, options);
|
||||
|
||||
// FIXME: TypeScript SDK thinks it is returning a File but in reality it is a string
|
||||
const data = response.data as never as string | object;
|
||||
|
||||
if (typeof data === 'object') {
|
||||
return JSON.stringify(data, null, 2);
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
export const useServerLog = (name: string) => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['ServerLog', name],
|
||||
queryFn: ({ signal }) => fetchServerLog(api!, name, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
|
||||
const fetchServerLogs = async (api: Api, options?: AxiosRequestConfig) => {
|
||||
const response = await getSystemApi(api!).getServerLogs(options);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const useServerLogs = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [ 'ServerLogs' ],
|
||||
queryFn: ({ signal }) => fetchServerLogs(api!, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import type { LogFile } from '@jellyfin/sdk/lib/generated-client/models/log-file';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import datetime from 'scripts/datetime';
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
|
||||
type LogItemProps = {
|
||||
logs: LogFile[];
|
||||
};
|
||||
|
||||
const LogItemList: FunctionComponent<LogItemProps> = ({ logs }: LogItemProps) => {
|
||||
const getDate = (logFile: LogFile) => {
|
||||
const date = datetime.parseISO8601Date(logFile.DateModified, true);
|
||||
return datetime.toLocaleDateString(date) + ' ' + datetime.getDisplayTime(date);
|
||||
};
|
||||
|
||||
return (
|
||||
<List sx={{ bgcolor: 'background.paper' }}>
|
||||
{logs.map(log => {
|
||||
return (
|
||||
<ListItem key={log.Name} disablePadding>
|
||||
<ListItemLink to={`/dashboard/logs/${log.Name}`}>
|
||||
<ListItemText
|
||||
primary={log.Name}
|
||||
secondary={getDate(log)}
|
||||
slotProps={{
|
||||
primary: {
|
||||
variant: 'h3'
|
||||
},
|
||||
secondary: {
|
||||
variant: 'body1'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogItemList;
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { Api } from '@jellyfin/sdk';
|
||||
import type { LibraryApiGetItemCountsRequest } from '@jellyfin/sdk/lib/generated-client/api/library-api';
|
||||
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
|
||||
import { queryOptions, useQuery } from '@tanstack/react-query';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
const fetchItemCounts = async (
|
||||
api: Api,
|
||||
params?: LibraryApiGetItemCountsRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
const response = await getLibraryApi(api)
|
||||
.getItemCounts(params, options);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getItemCountsQuery = (
|
||||
api?: Api,
|
||||
params?: LibraryApiGetItemCountsRequest
|
||||
) => queryOptions({
|
||||
queryKey: [ 'ItemCounts', params ],
|
||||
queryFn: ({ signal }) => fetchItemCounts(api!, params, { signal }),
|
||||
enabled: !!api,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
export const useItemCounts = (
|
||||
params?: LibraryApiGetItemCountsRequest
|
||||
) => {
|
||||
const { api } = useApi();
|
||||
return useQuery(getItemCountsQuery(api, params));
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import Skeleton from '@mui/material/Skeleton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import SvgIcon from '@mui/material/SvgIcon';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { useLocale } from 'hooks/useLocale';
|
||||
import { toDecimalString } from 'utils/number';
|
||||
|
||||
interface Metric {
|
||||
label: string
|
||||
value?: number
|
||||
}
|
||||
|
||||
export interface MetricCardProps {
|
||||
metrics: Metric[]
|
||||
Icon: typeof SvgIcon
|
||||
}
|
||||
|
||||
const MetricCard: FC<MetricCardProps> = ({
|
||||
metrics,
|
||||
Icon
|
||||
}) => {
|
||||
const { dateTimeLocale } = useLocale();
|
||||
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction='row'
|
||||
sx={{
|
||||
width: '100%',
|
||||
padding: 2,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{metrics.map(({ label, value }) => (
|
||||
<Box key={label}>
|
||||
<Typography
|
||||
variant='body2'
|
||||
color='text.secondary'
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant='h5'
|
||||
component='div'
|
||||
>
|
||||
{typeof value !== 'undefined' ? (
|
||||
toDecimalString(value, dateTimeLocale)
|
||||
) : (
|
||||
<Skeleton />
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
<Icon fontSize='large' />
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricCard;
|
||||
@@ -1,113 +0,0 @@
|
||||
/** List of codecs and their supported hardware acceleration types */
|
||||
export const CODECS = [
|
||||
{
|
||||
name: 'H264',
|
||||
codec: 'h264',
|
||||
types: [
|
||||
'amf',
|
||||
'nvenc',
|
||||
'qsv',
|
||||
'vaapi',
|
||||
'rkmpp',
|
||||
'videotoolbox',
|
||||
'v4l2m2m'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'HEVC',
|
||||
codec: 'hevc',
|
||||
types: [
|
||||
'amf',
|
||||
'nvenc',
|
||||
'qsv',
|
||||
'vaapi',
|
||||
'rkmpp',
|
||||
'videotoolbox'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'MPEG1',
|
||||
codec: 'mpeg1video',
|
||||
types: [ 'rkmpp' ]
|
||||
},
|
||||
{
|
||||
name: 'MPEG2',
|
||||
codec: 'mpeg2video',
|
||||
types: [
|
||||
'amf',
|
||||
'nvenc',
|
||||
'qsv',
|
||||
'vaapi',
|
||||
'rkmpp'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'MPEG4',
|
||||
codec: 'mpeg4',
|
||||
types: [
|
||||
'nvenc',
|
||||
'rkmpp'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'VC1',
|
||||
codec: 'vc1',
|
||||
types: [
|
||||
'amf',
|
||||
'nvenc',
|
||||
'qsv',
|
||||
'vaapi'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'VP8',
|
||||
codec: 'vp8',
|
||||
types: [
|
||||
'nvenc',
|
||||
'qsv',
|
||||
'vaapi',
|
||||
'rkmpp',
|
||||
'videotoolbox'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'VP9',
|
||||
codec: 'vp9',
|
||||
types: [
|
||||
'amf',
|
||||
'nvenc',
|
||||
'qsv',
|
||||
'vaapi',
|
||||
'rkmpp',
|
||||
'videotoolbox'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'AV1',
|
||||
codec: 'av1',
|
||||
types: [
|
||||
'amf',
|
||||
'nvenc',
|
||||
'qsv',
|
||||
'vaapi',
|
||||
'rkmpp',
|
||||
'videotoolbox'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
/** Hardware decoders which support 10-bit HEVC & VP9 */
|
||||
export const HEVC_VP9_HW_DECODING_TYPES = [
|
||||
'amf',
|
||||
'nvenc',
|
||||
'qsv',
|
||||
'vaapi',
|
||||
'rkmpp'
|
||||
];
|
||||
|
||||
/** Hardware decoders which support HEVC RExt */
|
||||
export const HEVC_REXT_DECODING_TYPES = [
|
||||
'nvenc',
|
||||
'qsv',
|
||||
'vaapi'
|
||||
];
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user