mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Integrate branch 'master' into feature/osd_chapter_selection
This commit is contained in:
commit
19af2bf834
756 changed files with 42708 additions and 21088 deletions
|
@ -5,7 +5,6 @@
|
|||
"not": [
|
||||
"./dist/libraries/pdf.worker.js",
|
||||
"./dist/libraries/worker-bundle.js",
|
||||
"./dist/libraries/wasm-gen/libarchive.js",
|
||||
"./dist/serviceworker.js"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
node_modules
|
||||
dist
|
||||
.idea
|
||||
.vscode
|
298
.eslintrc.js
298
.eslintrc.js
|
@ -1,298 +0,0 @@
|
|||
const restrictedGlobals = require('confusing-browser-globals');
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
'react',
|
||||
'import',
|
||||
'eslint-comments',
|
||||
'sonarjs'
|
||||
],
|
||||
env: {
|
||||
node: true,
|
||||
es6: true,
|
||||
es2017: true,
|
||||
es2020: true
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:import/errors',
|
||||
'plugin:eslint-comments/recommended',
|
||||
'plugin:compat/recommended',
|
||||
'plugin:sonarjs/recommended'
|
||||
],
|
||||
rules: {
|
||||
'array-callback-return': ['error', { 'checkForEach': true }],
|
||||
'block-spacing': ['error'],
|
||||
'brace-style': ['error', '1tbs', { 'allowSingleLine': true }],
|
||||
'comma-dangle': ['error', 'never'],
|
||||
'comma-spacing': ['error'],
|
||||
'curly': ['error', 'multi-line', 'consistent'],
|
||||
'default-case-last': ['error'],
|
||||
'eol-last': ['error'],
|
||||
'indent': ['error', 4, { 'SwitchCase': 1 }],
|
||||
'jsx-quotes': ['error', 'prefer-single'],
|
||||
'keyword-spacing': ['error'],
|
||||
'max-statements-per-line': ['error'],
|
||||
'max-params': ['error', 7],
|
||||
'new-cap': [
|
||||
'error',
|
||||
{
|
||||
'capIsNewExceptions': ['jQuery.Deferred'],
|
||||
'newIsCapExceptionPattern': '\\.default$'
|
||||
}
|
||||
],
|
||||
'no-duplicate-imports': ['error'],
|
||||
'no-empty-function': ['error'],
|
||||
'no-extend-native': ['error'],
|
||||
'no-floating-decimal': ['error'],
|
||||
'no-lonely-if': ['error'],
|
||||
'no-multi-spaces': ['error'],
|
||||
'no-multiple-empty-lines': ['error', { 'max': 1 }],
|
||||
'no-nested-ternary': ['error'],
|
||||
'no-redeclare': ['off'],
|
||||
'@typescript-eslint/no-redeclare': ['error', { builtinGlobals: false }],
|
||||
'no-restricted-globals': ['error'].concat(restrictedGlobals),
|
||||
'no-return-assign': ['error'],
|
||||
'no-return-await': ['error'],
|
||||
'no-sequences': ['error', { 'allowInParentheses': false }],
|
||||
'no-shadow': ['off'],
|
||||
'@typescript-eslint/no-shadow': ['error'],
|
||||
'no-throw-literal': ['error'],
|
||||
'no-trailing-spaces': ['error'],
|
||||
'no-undef-init': ['error'],
|
||||
'no-unneeded-ternary': ['error'],
|
||||
'no-unused-expressions': ['off'],
|
||||
'@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
|
||||
'no-unused-private-class-members': ['error'],
|
||||
'no-useless-rename': ['error'],
|
||||
'no-useless-constructor': ['off'],
|
||||
'@typescript-eslint/no-useless-constructor': ['error'],
|
||||
'no-var': ['error'],
|
||||
'no-void': ['error', { 'allowAsStatement': true }],
|
||||
'no-warning-comments': ['warn', { 'terms': ['fixme', 'hack', 'xxx'] }],
|
||||
'object-curly-spacing': ['error', 'always'],
|
||||
'one-var': ['error', 'never'],
|
||||
'operator-linebreak': ['error', 'before', { overrides: { '?': 'after', ':': 'after', '=': 'after' } }],
|
||||
'padded-blocks': ['error', 'never'],
|
||||
'prefer-const': ['error', { 'destructuring': 'all' }],
|
||||
'@typescript-eslint/prefer-for-of': ['error'],
|
||||
'@typescript-eslint/prefer-optional-chain': ['error'],
|
||||
'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
|
||||
'radix': ['error'],
|
||||
'@typescript-eslint/semi': ['error'],
|
||||
'space-before-blocks': ['error'],
|
||||
'space-infix-ops': 'error',
|
||||
'yoda': 'error',
|
||||
|
||||
'react/jsx-filename-extension': ['error', { 'extensions': ['.jsx', '.tsx'] }],
|
||||
'react/jsx-no-bind': ['error'],
|
||||
'react/jsx-no-useless-fragment': ['error'],
|
||||
'react/jsx-no-constructed-context-values': ['error'],
|
||||
'react/no-array-index-key': ['error'],
|
||||
|
||||
'sonarjs/no-inverted-boolean-check': ['error'],
|
||||
// TODO: Enable the following rules and fix issues
|
||||
'sonarjs/cognitive-complexity': ['off'],
|
||||
'sonarjs/no-duplicate-string': ['off']
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
},
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': [ '.ts', '.tsx' ]
|
||||
},
|
||||
'import/resolver': {
|
||||
node: {
|
||||
extensions: [
|
||||
'.js',
|
||||
'.ts',
|
||||
'.jsx',
|
||||
'.tsx'
|
||||
],
|
||||
moduleDirectory: [
|
||||
'node_modules',
|
||||
'src'
|
||||
]
|
||||
}
|
||||
},
|
||||
polyfills: [
|
||||
// Native Promises Only
|
||||
'Promise',
|
||||
// whatwg-fetch
|
||||
'fetch',
|
||||
// document-register-element
|
||||
'document.registerElement',
|
||||
// resize-observer-polyfill
|
||||
'ResizeObserver',
|
||||
// fast-text-encoding
|
||||
'TextEncoder',
|
||||
// intersection-observer
|
||||
'IntersectionObserver',
|
||||
// Core-js
|
||||
'Object.assign',
|
||||
'Object.is',
|
||||
'Object.setPrototypeOf',
|
||||
'Object.toString',
|
||||
'Object.freeze',
|
||||
'Object.seal',
|
||||
'Object.preventExtensions',
|
||||
'Object.isFrozen',
|
||||
'Object.isSealed',
|
||||
'Object.isExtensible',
|
||||
'Object.getOwnPropertyDescriptor',
|
||||
'Object.getPrototypeOf',
|
||||
'Object.keys',
|
||||
'Object.entries',
|
||||
'Object.getOwnPropertyNames',
|
||||
'Function.name',
|
||||
'Function.hasInstance',
|
||||
'Array.from',
|
||||
'Array.arrayOf',
|
||||
'Array.copyWithin',
|
||||
'Array.fill',
|
||||
'Array.find',
|
||||
'Array.findIndex',
|
||||
'Array.iterator',
|
||||
'String.fromCodePoint',
|
||||
'String.raw',
|
||||
'String.iterator',
|
||||
'String.codePointAt',
|
||||
'String.endsWith',
|
||||
'String.includes',
|
||||
'String.repeat',
|
||||
'String.startsWith',
|
||||
'String.trim',
|
||||
'String.anchor',
|
||||
'String.big',
|
||||
'String.blink',
|
||||
'String.bold',
|
||||
'String.fixed',
|
||||
'String.fontcolor',
|
||||
'String.fontsize',
|
||||
'String.italics',
|
||||
'String.link',
|
||||
'String.small',
|
||||
'String.strike',
|
||||
'String.sub',
|
||||
'String.sup',
|
||||
'RegExp',
|
||||
'Number',
|
||||
'Math',
|
||||
'Date',
|
||||
'async',
|
||||
'Symbol',
|
||||
'Map',
|
||||
'Set',
|
||||
'WeakMap',
|
||||
'WeakSet',
|
||||
'ArrayBuffer',
|
||||
'DataView',
|
||||
'Int8Array',
|
||||
'Uint8Array',
|
||||
'Uint8ClampedArray',
|
||||
'Int16Array',
|
||||
'Uint16Array',
|
||||
'Int32Array',
|
||||
'Uint32Array',
|
||||
'Float32Array',
|
||||
'Float64Array',
|
||||
'Reflect',
|
||||
// Temporary while eslint-compat-plugin is buggy
|
||||
'document.querySelector'
|
||||
]
|
||||
},
|
||||
overrides: [
|
||||
// Config files and development scripts
|
||||
{
|
||||
files: [
|
||||
'./babel.config.js',
|
||||
'./.eslintrc.js',
|
||||
'./postcss.config.js',
|
||||
'./webpack.*.js',
|
||||
'./scripts/**/*.js'
|
||||
]
|
||||
},
|
||||
// JavaScript source files
|
||||
{
|
||||
files: [
|
||||
'./src/**/*.{js,jsx,ts,tsx}'
|
||||
],
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json']
|
||||
},
|
||||
env: {
|
||||
node: false,
|
||||
amd: true,
|
||||
browser: true,
|
||||
es6: true,
|
||||
es2017: true,
|
||||
es2020: true
|
||||
},
|
||||
globals: {
|
||||
// Browser globals
|
||||
'MediaMetadata': 'readonly',
|
||||
// Tizen globals
|
||||
'tizen': 'readonly',
|
||||
'webapis': 'readonly',
|
||||
// WebOS globals
|
||||
'webOS': 'readonly',
|
||||
// Dependency globals
|
||||
'$': 'readonly',
|
||||
'jQuery': 'readonly',
|
||||
// Jellyfin globals
|
||||
'ApiClient': 'writable',
|
||||
'Events': 'writable',
|
||||
'chrome': 'writable',
|
||||
'DlnaProfilePage': 'writable',
|
||||
'DashboardPage': 'writable',
|
||||
'Emby': 'readonly',
|
||||
'Globalize': 'writable',
|
||||
'Hls': 'writable',
|
||||
'LibraryMenu': 'writable',
|
||||
'LinkParser': 'writable',
|
||||
'LiveTvHelpers': 'writable',
|
||||
'Loading': 'writable',
|
||||
'MetadataEditor': 'writable',
|
||||
'ServerNotifications': 'writable',
|
||||
'TaskButton': 'writable',
|
||||
'UserParentalControlPage': 'writable',
|
||||
'Windows': 'readonly',
|
||||
// Build time definitions
|
||||
__JF_BUILD_VERSION__: 'readonly',
|
||||
__PACKAGE_JSON_NAME__: 'readonly',
|
||||
__PACKAGE_JSON_VERSION__: 'readonly',
|
||||
__USE_SYSTEM_FONTS__: 'readonly',
|
||||
__WEBPACK_SERVE__: 'readonly'
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/prefer-string-starts-ends-with': ['error']
|
||||
}
|
||||
},
|
||||
// TypeScript source files
|
||||
{
|
||||
files: [
|
||||
'./src/**/*.{ts,tsx}'
|
||||
],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:import/typescript',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:eslint-comments/recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:jsx-a11y/recommended'
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-floating-promises': ['error'],
|
||||
'@typescript-eslint/no-unused-vars': ['error'],
|
||||
|
||||
'sonarjs/cognitive-complexity': ['error']
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
32
.github/ISSUE_TEMPLATE/1-bug-report.md
vendored
32
.github/ISSUE_TEMPLATE/1-bug-report.md
vendored
|
@ -1,32 +0,0 @@
|
|||
---
|
||||
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
Normal file
122
.github/ISSUE_TEMPLATE/1-bug-report.yml
vendored
Normal file
|
@ -0,0 +1,122 @@
|
|||
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
22
.github/ISSUE_TEMPLATE/2-playback-issue.md
vendored
|
@ -1,22 +0,0 @@
|
|||
---
|
||||
name: Playback Issue
|
||||
about: You have playback issues with some files
|
||||
labels: playback
|
||||
---
|
||||
|
||||
**Describe The Bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**Media Information**
|
||||
<!-- Please paste any ffprobe or MediaInfo logs. -->
|
||||
|
||||
**Screenshots**
|
||||
<!-- Add screenshots from the Playback Data and Media Info. -->
|
||||
|
||||
**System (please complete the following information):**
|
||||
- Platform: [e.g. Linux, Windows, iPhone, Tizen]
|
||||
- Browser: [e.g. Firefox, Chrome, Safari]
|
||||
- Jellyfin Version: [e.g. 10.6.0]
|
||||
|
||||
**Additional Context**
|
||||
<!-- Add any other context about the problem here. -->
|
145
.github/ISSUE_TEMPLATE/2-playback-issue.yml
vendored
Normal file
145
.github/ISSUE_TEMPLATE/2-playback-issue.yml
vendored
Normal file
|
@ -0,0 +1,145 @@
|
|||
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
13
.github/ISSUE_TEMPLATE/3-technical-discussion.md
vendored
|
@ -1,13 +0,0 @@
|
|||
---
|
||||
name: Technical Discussion
|
||||
about: You want to discuss technical aspects of changes you intend to make
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
<!-- Explain the change and the motivations behind it.
|
||||
|
||||
For example, if you plan to rely on a new dependency, explain why and what
|
||||
it brings to the project.
|
||||
|
||||
If you plan to make significant changes, go roughly over the steps you intend
|
||||
to take and how you would divide the change in PRs of a manageable size. -->
|
9
.github/ISSUE_TEMPLATE/4-meta-issue.md
vendored
9
.github/ISSUE_TEMPLATE/4-meta-issue.md
vendored
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: Meta Issue
|
||||
about: You want to track a number of other issues as part of a larger project
|
||||
labels: meta
|
||||
---
|
||||
|
||||
* [ ] Issue 1 [#123]
|
||||
* [ ] Issue 2 [#456]
|
||||
* [ ] ...
|
22
.github/renovate.json
vendored
22
.github/renovate.json
vendored
|
@ -2,7 +2,27 @@
|
|||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"github>jellyfin/.github//renovate-presets/nodejs",
|
||||
":semanticCommitsDisabled",
|
||||
":dependencyDashboard"
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": false
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackageNames": [ "@jellyfin/sdk" ],
|
||||
"followTag": "unstable",
|
||||
"minimumReleaseAge": null,
|
||||
"prPriority": 5,
|
||||
"schedule": [ "after 7:00 am" ]
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [ "dompurify" ],
|
||||
"matchUpdateTypes": [ "major" ],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [ "hls.js" ],
|
||||
"prPriority": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
15
.github/workflows/__automation.yml
vendored
Normal file
15
.github/workflows/__automation.yml
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
name: Automation 🎛️
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
conflicts:
|
||||
name: Merge conflict labeling 🏷️
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
|
||||
with:
|
||||
dirtyLabel: 'merge conflict'
|
||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
||||
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
40
.github/workflows/__codeql.yml
vendored
Normal file
40
.github/workflows/__codeql.yml
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
name: GitHub CodeQL 🔬
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
commit:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze ${{ matrix.language }} 🔬
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language:
|
||||
- javascript-typescript
|
||||
|
||||
steps:
|
||||
- name: Checkout repository ⬇️
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.commit }}
|
||||
show-progress: false
|
||||
|
||||
- name: Initialize CodeQL 🛠️
|
||||
uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
|
||||
with:
|
||||
queries: security-and-quality
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild 📦
|
||||
uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
|
||||
|
||||
- name: Perform CodeQL Analysis 🧪
|
||||
uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
59
.github/workflows/__deploy.yml
vendored
Normal file
59
.github/workflows/__deploy.yml
vendored
Normal file
|
@ -0,0 +1,59 @@
|
|||
name: Deploy 🏗️
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
branch:
|
||||
required: true
|
||||
type: string
|
||||
commit:
|
||||
required: false
|
||||
type: string
|
||||
comment:
|
||||
required: false
|
||||
type: boolean
|
||||
artifact_name:
|
||||
required: false
|
||||
type: string
|
||||
default: frontend
|
||||
|
||||
jobs:
|
||||
cf-pages:
|
||||
name: CloudFlare Pages 📃
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: ${{ inputs.branch == 'master' && 'Production' || 'Preview' }}
|
||||
url: ${{ steps.cf.outputs.deployment-url }}
|
||||
outputs:
|
||||
url: ${{ steps.cf.outputs.deployment-url }}
|
||||
|
||||
steps:
|
||||
- name: Download workflow artifact ⬇️
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}
|
||||
path: dist
|
||||
|
||||
- name: Publish to Cloudflare Pages 📃
|
||||
uses: cloudflare/wrangler-action@392082e81ffbcb9ebdde27400634aa004b35ea37 # v3.14.0
|
||||
id: cf
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy dist --project-name=jellyfin-web --branch=${{ inputs.branch }}
|
||||
|
||||
compose-comment:
|
||||
name: Compose and push comment 📝
|
||||
# Always run so the comment is composed for the workflow summary
|
||||
if: ${{ always() }}
|
||||
uses: ./.github/workflows/__job_messages.yml
|
||||
secrets: inherit
|
||||
needs:
|
||||
- cf-pages
|
||||
|
||||
with:
|
||||
branch: ${{ inputs.branch }}
|
||||
commit: ${{ inputs.commit }}
|
||||
preview_url: ${{ needs.cf-pages.outputs.url }}
|
||||
in_progress: false
|
||||
comment: ${{ inputs.comment }}
|
|
@ -1,4 +1,4 @@
|
|||
name: Job messages
|
||||
name: Job messages ⚙️
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
@ -12,32 +12,26 @@ on:
|
|||
preview_url:
|
||||
required: false
|
||||
type: string
|
||||
build_workflow_run_id:
|
||||
required: false
|
||||
type: number
|
||||
commenting_workflow_run_id:
|
||||
required: true
|
||||
type: string
|
||||
in_progress:
|
||||
required: true
|
||||
type: boolean
|
||||
outputs:
|
||||
msg:
|
||||
description: The composed message
|
||||
value: ${{ jobs.msg.outputs.msg }}
|
||||
comment:
|
||||
required: false
|
||||
type: boolean
|
||||
marker:
|
||||
description: Hidden marker to detect PR comments composed by the bot
|
||||
value: "CFPages-deployment"
|
||||
required: false
|
||||
type: string
|
||||
default: "CFPages-deployment"
|
||||
|
||||
|
||||
jobs:
|
||||
msg:
|
||||
name: Deployment status
|
||||
cf_pages_msg:
|
||||
name: CloudFlare Pages deployment 📃🚀
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
msg: ${{ env.msg }}
|
||||
|
||||
steps:
|
||||
- name: Compose message
|
||||
- name: Compose message 📃
|
||||
if: ${{ always() }}
|
||||
id: compose
|
||||
env:
|
||||
|
@ -45,8 +39,7 @@ jobs:
|
|||
PREVIEW_URL: ${{ inputs.preview_url != '' && (inputs.branch != 'master' && inputs.preview_url || format('https://jellyfin-web.pages.dev ({0})', inputs.preview_url)) || 'Not available' }}
|
||||
DEPLOY_STATUS: ${{ inputs.in_progress && '🔄 Deploying...' || (inputs.preview_url != '' && '✅ Deployed!' || '❌ Failure. Check workflow logs for details') }}
|
||||
DEPLOYMENT_TYPE: ${{ inputs.branch != 'master' && '🔀 Preview' || '⚙️ Production' }}
|
||||
BUILD_WORKFLOW_RUN: ${{ !inputs.in_progress && format('**[View build logs](https://github.com/{0}/actions/runs/{1})**', 'jellyfin/jellyfin-web', inputs.build_workflow_run_id) || '' }}
|
||||
COMMENTING_WORKFLOW_RUN: ${{ format('**[View bot logs](https://github.com/{0}/actions/runs/{1})**', 'jellyfin/jellyfin-web', inputs.commenting_workflow_run_id) }}
|
||||
WORKFLOW_RUN: ${{ !inputs.in_progress && format('**[View build logs](https://github.com/{0}/actions/runs/{1})**', github.repository, github.run_id) || '' }}
|
||||
# EOF is needed for multiline environment variables in a GitHub Actions context
|
||||
run: |
|
||||
echo "## Cloudflare Pages deployment" > $GITHUB_STEP_SUMMARY
|
||||
|
@ -57,9 +50,16 @@ jobs:
|
|||
echo "| **Preview URL** | $PREVIEW_URL |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Type** | $DEPLOYMENT_TYPE |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "$BUILD_WORKFLOW_RUN" >> $GITHUB_STEP_SUMMARY
|
||||
echo "$COMMENTING_WORKFLOW_RUN" >> $GITHUB_STEP_SUMMARY
|
||||
echo "$WORKFLOW_RUN" >> $GITHUB_STEP_SUMMARY
|
||||
COMPOSED_MSG=$(cat $GITHUB_STEP_SUMMARY)
|
||||
echo "msg<<EOF" >> $GITHUB_ENV
|
||||
echo "$COMPOSED_MSG" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Push comment to Pull Request 🔼
|
||||
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1
|
||||
if: ${{ inputs.comment && steps.compose.conclusion == 'success' }}
|
||||
with:
|
||||
github-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
message: ${{ env.msg }}
|
||||
comment-tag: ${{ inputs.marker }}
|
45
.github/workflows/__package.yml
vendored
Normal file
45
.github/workflows/__package.yml
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
name: Packaging 📦
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
commit:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
run-build-prod:
|
||||
name: Run production build 🏗️
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.commit || github.sha }}
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
check-latest: true
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run a production build
|
||||
env:
|
||||
JELLYFIN_VERSION: ${{ inputs.commit || github.sha }}
|
||||
run: npm run build:production
|
||||
|
||||
- name: Update config.json for testing
|
||||
run: |
|
||||
jq '.multiserver=true | .servers=["https://demo.jellyfin.org/unstable"]' dist/config.json > dist/config.tmp.json
|
||||
mv dist/config.tmp.json dist/config.json
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||
with:
|
||||
name: frontend
|
||||
path: dist
|
61
.github/workflows/__quality_checks.yml
vendored
Normal file
61
.github/workflows/__quality_checks.yml
vendored
Normal file
|
@ -0,0 +1,61 @@
|
|||
name: Quality checks 👌🧪
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
commit:
|
||||
required: true
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
name: Vulnerable dependencies 🔎
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.commit }}
|
||||
show-progress: false
|
||||
|
||||
- name: Scan
|
||||
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
|
||||
with:
|
||||
## Workaround from https://github.com/actions/dependency-review-action/issues/456
|
||||
## TODO: Remove when necessary
|
||||
base-ref: ${{ github.event.pull_request.base.sha || 'master' }}
|
||||
head-ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
quality:
|
||||
name: Run ${{ matrix.command }} 🕵️♂️
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
command:
|
||||
- build:es-check
|
||||
- lint
|
||||
- stylelint
|
||||
- build:check
|
||||
- test
|
||||
|
||||
steps:
|
||||
- name: Checkout ⬇️
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.commit }}
|
||||
show-progress: false
|
||||
|
||||
- name: Setup node environment ⚙️
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
check-latest: true
|
||||
|
||||
- name: Install dependencies 📦
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run ${{ matrix.command }} ⚙️
|
||||
run: npm run ${{ matrix.command }}
|
21
.github/workflows/automation.yml
vendored
21
.github/workflows/automation.yml
vendored
|
@ -1,21 +0,0 @@
|
|||
name: 'Automation'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request_target:
|
||||
types:
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
name: 'Merge conflict labeling'
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
steps:
|
||||
- uses: eps1lon/actions-label-merge-conflict@6d74047dcef155976a15e4a124dde2c7fe0c5522 # v3.0.1
|
||||
with:
|
||||
dirtyLabel: 'merge conflict'
|
||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
||||
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
75
.github/workflows/build.yml
vendored
75
.github/workflows/build.yml
vendored
|
@ -1,75 +0,0 @@
|
|||
name: Build
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, release* ]
|
||||
pull_request:
|
||||
branches: [ master, release* ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
run-build-prod:
|
||||
name: Run production build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
cache: npm
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run a production build
|
||||
env:
|
||||
JELLYFIN_VERSION: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
run: npm run build:production
|
||||
|
||||
- name: Update config.json for testing
|
||||
run: |
|
||||
jq '.multiserver=true | .servers=["https://demo.jellyfin.org/unstable"]' dist/config.json > dist/config.tmp.json
|
||||
mv dist/config.tmp.json dist/config.json
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: jellyfin-web__prod
|
||||
path: |
|
||||
dist
|
||||
|
||||
pr_context:
|
||||
name: Save PR context as artifact
|
||||
if: ${{ always() && !cancelled() && github.event_name == 'pull_request' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- run-build-prod
|
||||
|
||||
steps:
|
||||
- name: Save PR context
|
||||
env:
|
||||
PR_BRANCH: ${{ github.ref_name }}
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
PR_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
echo $PR_BRANCH > PR_branch
|
||||
echo $PR_NUMBER > PR_number
|
||||
echo $PR_SHA > PR_sha
|
||||
|
||||
- name: Upload PR number as artifact
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: PR_context
|
||||
path: |
|
||||
PR_branch
|
||||
PR_number
|
||||
PR_sha
|
34
.github/workflows/codeql.yml
vendored
34
.github/workflows/codeql.yml
vendored
|
@ -1,34 +0,0 @@
|
|||
name: CodeQL
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, release* ]
|
||||
pull_request:
|
||||
branches: [ master, release* ]
|
||||
schedule:
|
||||
- cron: '30 7 * * 6'
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
name: Run CodeQL
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5
|
||||
with:
|
||||
languages: javascript
|
||||
queries: +security-extended
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5
|
36
.github/workflows/commands.yml
vendored
36
.github/workflows/commands.yml
vendored
|
@ -1,36 +0,0 @@
|
|||
name: Commands
|
||||
on:
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
- edited
|
||||
|
||||
jobs:
|
||||
rebase:
|
||||
name: Rebase
|
||||
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify as seen
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reactions: '+1'
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
- name: Automatic Rebase
|
||||
uses: cirrus-actions/rebase@b87d48154a87a85666003575337e27b8cd65f691 # 1.8
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
- name: Comment on failure
|
||||
if: failure()
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
I'm sorry @${{ github.event.comment.user.login }}, I'm afraid I can't do that.
|
38
.github/workflows/pr-suggestions.yml
vendored
38
.github/workflows/pr-suggestions.yml
vendored
|
@ -1,38 +0,0 @@
|
|||
name: PR suggestions
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.id || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: [ master, release* ]
|
||||
types:
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
run-eslint:
|
||||
name: Run eslint suggestions
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
cache: npm
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run eslint
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
uses: CatChen/eslint-suggestion-action@b110ac684564c7b73e47cc223eb7a5266ec83fd3 # v4.1.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
102
.github/workflows/publish.yml
vendored
102
.github/workflows/publish.yml
vendored
|
@ -1,102 +0,0 @@
|
|||
name: Publish
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Build
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
pr-context:
|
||||
name: PR context
|
||||
if: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
branch: ${{ env.pr_branch }}
|
||||
commit: ${{ env.pr_sha }}
|
||||
pr_number: ${{ env.pr_number }}
|
||||
|
||||
steps:
|
||||
- name: Get PR context
|
||||
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
|
||||
id: pr_context
|
||||
with:
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: PR_context
|
||||
|
||||
- name: Set PR context environment variables
|
||||
if: ${{ steps.pr_context.conclusion == 'success' }}
|
||||
run: |
|
||||
echo "pr_branch=$(cat PR_branch)" >> $GITHUB_ENV
|
||||
echo "pr_number=$(cat PR_number)" >> $GITHUB_ENV
|
||||
echo "pr_sha=$(cat PR_sha)" >> $GITHUB_ENV
|
||||
|
||||
publish:
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
|
||||
name: Deploy to Cloudflare Pages
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pr-context
|
||||
outputs:
|
||||
url: ${{ steps.cf.outputs.url }}
|
||||
|
||||
steps:
|
||||
- name: Download workflow artifact
|
||||
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
|
||||
with:
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: jellyfin-web__prod
|
||||
path: dist
|
||||
|
||||
- name: Publish
|
||||
id: cf
|
||||
uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # 1
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: jellyfin-web
|
||||
branch: ${{ needs.pr-context.outputs.branch || github.ref_name }}
|
||||
directory: dist
|
||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
compose-comment:
|
||||
name: Compose comment
|
||||
if: ${{ always() }}
|
||||
uses: ./.github/workflows/job-messages.yml
|
||||
needs:
|
||||
- publish
|
||||
- pr-context
|
||||
|
||||
with:
|
||||
branch: ${{ needs.pr-context.outputs.branch || github.ref_name }}
|
||||
commit: ${{ needs.pr-context.outputs.commit != '' && needs.pr-context.outputs.commit || github.event.workflow_run.head_sha }}
|
||||
preview_url: ${{ needs.publish.outputs.url }}
|
||||
build_workflow_run_id: ${{ github.event.workflow_run.id }}
|
||||
commenting_workflow_run_id: ${{ github.run_id }}
|
||||
in_progress: false
|
||||
|
||||
comment-status:
|
||||
name: Create comment status
|
||||
if: |
|
||||
always() &&
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
needs.pr-context.outputs.pr_number != ''
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- compose-comment
|
||||
- pr-context
|
||||
|
||||
steps:
|
||||
- name: Update job summary in PR comment
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
message: ${{ needs.compose-comment.outputs.msg }}
|
||||
pr_number: ${{ needs.pr-context.outputs.pr_number }}
|
||||
comment_tag: ${{ needs.compose-comment.outputs.marker }}
|
||||
mode: recreate
|
100
.github/workflows/pull_request.yml
vendored
Normal file
100
.github/workflows/pull_request.yml
vendored
Normal file
|
@ -0,0 +1,100 @@
|
|||
name: Pull Request 📥
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- master
|
||||
- release*
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
push-comment:
|
||||
name: Create comments ✍️
|
||||
if: ${{ always() && !cancelled() && github.repository == 'jellyfin/jellyfin-web' }}
|
||||
uses: ./.github/workflows/__job_messages.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha }}
|
||||
in_progress: true
|
||||
comment: true
|
||||
|
||||
build:
|
||||
name: Build 🏗️
|
||||
if: ${{ always() && !cancelled() }}
|
||||
uses: ./.github/workflows/__package.yml
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
automation:
|
||||
name: Automation 🎛️
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
uses: ./.github/workflows/__automation.yml
|
||||
secrets: inherit
|
||||
|
||||
quality_checks:
|
||||
name: Quality checks 👌🧪
|
||||
if: ${{ always() && !cancelled() }}
|
||||
uses: ./.github/workflows/__quality_checks.yml
|
||||
permissions: {}
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
codeql:
|
||||
name: GitHub CodeQL 🔬
|
||||
if: ${{ always() && !cancelled() }}
|
||||
uses: ./.github/workflows/__codeql.yml
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
with:
|
||||
commit: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
deploy:
|
||||
name: Deploy 🚀
|
||||
uses: ./.github/workflows/__deploy.yml
|
||||
if: ${{ always() && !cancelled() && needs.build.result == 'success' && github.repository == 'jellyfin/jellyfin-web' }}
|
||||
needs:
|
||||
- push-comment
|
||||
- build
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
secrets: inherit
|
||||
with:
|
||||
# If the PR is from the master branch of a fork, append the fork's name to the branch name
|
||||
branch: ${{ github.event.pull_request.head.repo.full_name != github.repository && github.event.pull_request.head.ref == 'master' && format('{0}/{1}', github.event.pull_request.head.repo.full_name, github.event.pull_request.head.ref) || github.event.pull_request.head.ref }}
|
||||
comment: true
|
||||
commit: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
run-eslint:
|
||||
name: Run eslint suggestions
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
check-latest: true
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run eslint
|
||||
uses: CatChen/eslint-suggestion-action@3ba53ce078667d5f60a73a8005627cf95ab57dce # v4.1.9
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
58
.github/workflows/push.yml
vendored
Normal file
58
.github/workflows/push.yml
vendored
Normal file
|
@ -0,0 +1,58 @@
|
|||
name: Push & Release 🌍
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- release*
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
|
||||
jobs:
|
||||
automation:
|
||||
name: Automation 🎛️
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
uses: ./.github/workflows/__automation.yml
|
||||
secrets: inherit
|
||||
|
||||
main:
|
||||
name: 'Unstable release 🚀⚠️'
|
||||
uses: ./.github/workflows/__package.yml
|
||||
with:
|
||||
commit: ${{ github.sha }}
|
||||
|
||||
quality_checks:
|
||||
name: Quality checks 👌🧪
|
||||
if: ${{ always() && !cancelled() }}
|
||||
uses: ./.github/workflows/__quality_checks.yml
|
||||
permissions: {}
|
||||
with:
|
||||
commit: ${{ github.sha }}
|
||||
|
||||
codeql:
|
||||
name: GitHub CodeQL 🔬
|
||||
uses: ./.github/workflows/__codeql.yml
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
with:
|
||||
commit: ${{ github.sha }}
|
||||
|
||||
deploy:
|
||||
name: Deploy 🚀
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
uses: ./.github/workflows/__deploy.yml
|
||||
needs:
|
||||
- main
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
secrets: inherit
|
||||
with:
|
||||
branch: ${{ github.ref_name }}
|
||||
comment: false
|
123
.github/workflows/quality.yml
vendored
123
.github/workflows/quality.yml
vendored
|
@ -1,123 +0,0 @@
|
|||
name: Quality checks
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, release* ]
|
||||
pull_request:
|
||||
branches: [ master, release* ]
|
||||
|
||||
jobs:
|
||||
run-escheck:
|
||||
name: Run es-check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
cache: npm
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run a production build
|
||||
run: npm run build:production
|
||||
|
||||
- name: Run es-check
|
||||
run: npm run escheck
|
||||
|
||||
run-eslint:
|
||||
name: Run eslint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
cache: npm
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run eslint
|
||||
run: npx eslint --quiet "."
|
||||
|
||||
run-stylelint:
|
||||
name: Run stylelint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
cache: npm
|
||||
|
||||
- name: Set up stylelint matcher
|
||||
uses: xt0rted/stylelint-problem-matcher@34db1b874c0452909f0696aedef70b723870a583 # tag=v1
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run stylelint
|
||||
run: npm run stylelint
|
||||
|
||||
run-tsc:
|
||||
name: Run TypeScript build check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
cache: npm
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run tsc
|
||||
run: npm run build:check
|
||||
|
||||
run-test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
cache: npm
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci --no-audit
|
||||
|
||||
- name: Run test suite
|
||||
run: npm run test
|
|
@ -1,28 +1,26 @@
|
|||
name: Stale Check
|
||||
name: Scheduled tasks 🕑
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
issues:
|
||||
name: Check issues
|
||||
name: Check stale issues and PRs
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
operations-per-run: 75
|
||||
# Issues receive a stale warning after 120 days and close after an additional 21 days
|
||||
days-before-stale: 120
|
||||
days-before-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: |-
|
||||
|
@ -31,21 +29,10 @@ 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-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
|
||||
# PRs are closed after having unresolved merge conflicts for 90 days
|
||||
days-before-pr-stale: 0
|
||||
days-before-pr-close: 90
|
||||
only-pr-labels: merge conflict
|
||||
stale-pr-label: stale
|
||||
close-pr-message: |-
|
||||
This PR has been closed due to having unresolved merge conflicts.
|
52
.github/workflows/update-sdk.yml
vendored
52
.github/workflows/update-sdk.yml
vendored
|
@ -1,52 +0,0 @@
|
|||
name: Update the Jellyfin SDK
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 7 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: unstable-sdk-pr
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
ref: master
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
cache: npm
|
||||
|
||||
- name: Install latest unstable SDK
|
||||
run: |
|
||||
npm i --save @jellyfin/sdk@unstable
|
||||
VERSION=$(jq -r '.dependencies["@jellyfin/sdk"]' package.json)
|
||||
echo "JF_SDK_VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Open a pull request
|
||||
uses: peter-evans/create-pull-request@6d6857d36972b65feb161a90e484f2984215f83e # v6.0.5
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
commit-message: Update @jellyfin/sdk to ${{env.JF_SDK_VERSION}}
|
||||
committer: jellyfin-bot <team@jellyfin.org>
|
||||
author: jellyfin-bot <team@jellyfin.org>
|
||||
branch: update-jf-sdk
|
||||
delete-branch: true
|
||||
title: Update @jellyfin/sdk to ${{env.JF_SDK_VERSION}}
|
||||
body: |
|
||||
**Changes**
|
||||
Updates to the latest unstable @jellyfin/sdk build
|
||||
labels: |
|
||||
dependencies
|
||||
npm
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -3,6 +3,9 @@ dist
|
|||
web
|
||||
node_modules
|
||||
|
||||
# test coverage
|
||||
coverage
|
||||
|
||||
# config
|
||||
config.json
|
||||
|
||||
|
@ -10,12 +13,6 @@ config.json
|
|||
.idea
|
||||
.vs
|
||||
|
||||
# log
|
||||
yarn-error.log
|
||||
|
||||
# vim
|
||||
*.sw?
|
||||
|
||||
# build artifacts
|
||||
fedora/jellyfin-web-*.src.rpm
|
||||
fedora/jellyfin-web-*.tar.gz
|
||||
|
|
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
@ -0,0 +1 @@
|
|||
20
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"plugins": [
|
||||
"@stylistic/stylelint-plugin",
|
||||
"stylelint-no-browser-hacks/lib"
|
||||
],
|
||||
"rules": {
|
||||
|
@ -10,20 +11,20 @@
|
|||
],
|
||||
"ignore": ["after-comment"]
|
||||
} ],
|
||||
"at-rule-name-case": "lower",
|
||||
"at-rule-name-space-after": "always-single-line",
|
||||
"@stylistic/at-rule-name-case": "lower",
|
||||
"@stylistic/at-rule-name-space-after": "always-single-line",
|
||||
"at-rule-no-unknown": true,
|
||||
"at-rule-no-vendor-prefix": true,
|
||||
"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",
|
||||
"@stylistic/at-rule-semicolon-newline-after": "always",
|
||||
"@stylistic/block-closing-brace-empty-line-before": "never",
|
||||
"@stylistic/block-closing-brace-newline-after": "always",
|
||||
"@stylistic/block-closing-brace-newline-before": "always-multi-line",
|
||||
"@stylistic/block-closing-brace-space-before": "always-single-line",
|
||||
"block-no-empty": true,
|
||||
"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",
|
||||
"@stylistic/block-opening-brace-newline-after": "always-multi-line",
|
||||
"@stylistic/block-opening-brace-space-after": "always-single-line",
|
||||
"@stylistic/block-opening-brace-space-before": "always",
|
||||
"@stylistic/color-hex-case": "lower",
|
||||
"color-hex-length": "short",
|
||||
"color-no-invalid-hex": true,
|
||||
"comment-empty-line-before": [ "always", {
|
||||
|
@ -42,8 +43,8 @@
|
|||
"inside-single-line-block"
|
||||
]
|
||||
} ],
|
||||
"declaration-bang-space-after": "never",
|
||||
"declaration-bang-space-before": "always",
|
||||
"@stylistic/declaration-bang-space-after": "never",
|
||||
"@stylistic/declaration-bang-space-before": "always",
|
||||
"declaration-block-no-duplicate-properties": [
|
||||
true,
|
||||
{
|
||||
|
@ -51,52 +52,52 @@
|
|||
}
|
||||
],
|
||||
"declaration-block-no-shorthand-property-overrides": true,
|
||||
"declaration-block-semicolon-newline-after": "always-multi-line",
|
||||
"declaration-block-semicolon-space-after": "always-single-line",
|
||||
"declaration-block-semicolon-space-before": "never",
|
||||
"@stylistic/declaration-block-semicolon-newline-after": "always-multi-line",
|
||||
"@stylistic/declaration-block-semicolon-space-after": "always-single-line",
|
||||
"@stylistic/declaration-block-semicolon-space-before": "never",
|
||||
"declaration-block-single-line-max-declarations": 1,
|
||||
"declaration-block-trailing-semicolon": "always",
|
||||
"declaration-colon-newline-after": "always-multi-line",
|
||||
"declaration-colon-space-after": "always-single-line",
|
||||
"declaration-colon-space-before": "never",
|
||||
"@stylistic/declaration-block-trailing-semicolon": "always",
|
||||
"@stylistic/declaration-colon-newline-after": "always-multi-line",
|
||||
"@stylistic/declaration-colon-space-after": "always-single-line",
|
||||
"@stylistic/declaration-colon-space-before": "never",
|
||||
"font-family-no-duplicate-names": true,
|
||||
"function-calc-no-unspaced-operator": true,
|
||||
"function-comma-newline-after": "always-multi-line",
|
||||
"function-comma-space-after": "always-single-line",
|
||||
"function-comma-space-before": "never",
|
||||
"@stylistic/function-comma-newline-after": "always-multi-line",
|
||||
"@stylistic/function-comma-space-after": "always-single-line",
|
||||
"@stylistic/function-comma-space-before": "never",
|
||||
"function-linear-gradient-no-nonstandard-direction": true,
|
||||
"function-max-empty-lines": 0,
|
||||
"@stylistic/function-max-empty-lines": 0,
|
||||
"function-name-case": "lower",
|
||||
"function-parentheses-newline-inside": "always-multi-line",
|
||||
"function-parentheses-space-inside": "never-single-line",
|
||||
"function-whitespace-after": "always",
|
||||
"indentation": 4,
|
||||
"@stylistic/function-parentheses-newline-inside": "always-multi-line",
|
||||
"@stylistic/function-parentheses-space-inside": "never-single-line",
|
||||
"@stylistic/function-whitespace-after": "always",
|
||||
"@stylistic/indentation": 4,
|
||||
"keyframe-declaration-no-important": true,
|
||||
"length-zero-no-unit": true,
|
||||
"max-empty-lines": 1,
|
||||
"media-feature-colon-space-after": "always",
|
||||
"media-feature-colon-space-before": "never",
|
||||
"media-feature-name-case": "lower",
|
||||
"@stylistic/max-empty-lines": 1,
|
||||
"@stylistic/media-feature-colon-space-after": "always",
|
||||
"@stylistic/media-feature-colon-space-before": "never",
|
||||
"@stylistic/media-feature-name-case": "lower",
|
||||
"media-feature-name-no-unknown": true,
|
||||
"media-feature-name-no-vendor-prefix": true,
|
||||
"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",
|
||||
"@stylistic/media-feature-parentheses-space-inside": "never",
|
||||
"@stylistic/media-feature-range-operator-space-after": "always",
|
||||
"@stylistic/media-feature-range-operator-space-before": "always",
|
||||
"@stylistic/media-query-list-comma-newline-after": "always-multi-line",
|
||||
"@stylistic/media-query-list-comma-space-after": "always-single-line",
|
||||
"@stylistic/media-query-list-comma-space-before": "never",
|
||||
"no-descending-specificity": true,
|
||||
"no-duplicate-at-import-rules": true,
|
||||
"no-duplicate-selectors": true,
|
||||
"no-empty-source": true,
|
||||
"no-eol-whitespace": true,
|
||||
"no-extra-semicolons": true,
|
||||
"@stylistic/no-eol-whitespace": true,
|
||||
"@stylistic/no-extra-semicolons": true,
|
||||
"no-invalid-double-slash-comments": true,
|
||||
"no-missing-end-of-source-newline": true,
|
||||
"number-leading-zero": "always",
|
||||
"number-no-trailing-zeros": true,
|
||||
"@stylistic/no-missing-end-of-source-newline": true,
|
||||
"@stylistic/number-leading-zero": "always",
|
||||
"@stylistic/number-no-trailing-zeros": true,
|
||||
"plugin/no-browser-hacks": true,
|
||||
"property-case": "lower",
|
||||
"@stylistic/property-case": "lower",
|
||||
"property-no-unknown": [
|
||||
true,
|
||||
{
|
||||
|
@ -110,20 +111,20 @@
|
|||
"except": ["first-nested"],
|
||||
"ignore": ["after-comment"]
|
||||
} ],
|
||||
"selector-attribute-brackets-space-inside": "never",
|
||||
"selector-attribute-operator-space-after": "never",
|
||||
"selector-attribute-operator-space-before": "never",
|
||||
"selector-combinator-space-after": "always",
|
||||
"selector-combinator-space-before": "always",
|
||||
"selector-descendant-combinator-no-non-space": true,
|
||||
"selector-list-comma-newline-after": "always",
|
||||
"selector-list-comma-space-before": "never",
|
||||
"selector-max-empty-lines": 0,
|
||||
"@stylistic/selector-attribute-brackets-space-inside": "never",
|
||||
"@stylistic/selector-attribute-operator-space-after": "never",
|
||||
"@stylistic/selector-attribute-operator-space-before": "never",
|
||||
"@stylistic/selector-combinator-space-after": "always",
|
||||
"@stylistic/selector-combinator-space-before": "always",
|
||||
"@stylistic/selector-descendant-combinator-no-non-space": true,
|
||||
"@stylistic/selector-list-comma-newline-after": "always",
|
||||
"@stylistic/selector-list-comma-space-before": "never",
|
||||
"@stylistic/selector-max-empty-lines": 0,
|
||||
"selector-no-vendor-prefix": true,
|
||||
"selector-pseudo-class-case": "lower",
|
||||
"@stylistic/selector-pseudo-class-case": "lower",
|
||||
"selector-pseudo-class-no-unknown": true,
|
||||
"selector-pseudo-class-parentheses-space-inside": "never",
|
||||
"selector-pseudo-element-case": "lower",
|
||||
"@stylistic/selector-pseudo-class-parentheses-space-inside": "never",
|
||||
"@stylistic/selector-pseudo-element-case": "lower",
|
||||
"selector-pseudo-element-colon-notation": "double",
|
||||
"selector-pseudo-element-no-unknown": [
|
||||
true,
|
||||
|
@ -136,13 +137,13 @@
|
|||
"selector-type-case": "lower",
|
||||
"selector-type-no-unknown": true,
|
||||
"string-no-newline": true,
|
||||
"unit-case": "lower",
|
||||
"@stylistic/unit-case": "lower",
|
||||
"unit-no-unknown": true,
|
||||
"value-no-vendor-prefix": true,
|
||||
"value-list-comma-newline-after": "always-multi-line",
|
||||
"value-list-comma-space-after": "always-single-line",
|
||||
"value-list-comma-space-before": "never",
|
||||
"value-list-max-empty-lines": 0
|
||||
"@stylistic/value-list-comma-newline-after": "always-multi-line",
|
||||
"@stylistic/value-list-comma-space-after": "always-single-line",
|
||||
"@stylistic/value-list-comma-space-before": "never",
|
||||
"@stylistic/value-list-max-empty-lines": 0
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -1,4 +1,7 @@
|
|||
{
|
||||
"[json][typescript][typescriptreact][javascript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
|
|
|
@ -87,6 +87,15 @@
|
|||
- [JPUC1143](https://github.com/Jpuc1143)
|
||||
- [David Angel](https://github.com/davidangel)
|
||||
- [Pithaya](https://github.com/Pithaya)
|
||||
- [Peter Santos](https://github.com/prsantos-com)
|
||||
- [Chaitanya Shahare](https://github.com/Chaitanya-Shahare)
|
||||
- [Venkat Karasani](https://github.com/venkat-karasani)
|
||||
- [Connor Smith](https://github.com/ConnorS1110)
|
||||
- [iFraan](https://github.com/iFraan)
|
||||
- [Ali](https://github.com/bu3alwa)
|
||||
- [K. Kyle Puchkov](https://github.com/kepper104)
|
||||
- [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode)
|
||||
- [Jxiced](https://github.com/Jxiced)
|
||||
|
||||
## Emby Contributors
|
||||
|
||||
|
|
21
README.md
21
README.md
|
@ -73,6 +73,10 @@ 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
|
||||
|
@ -82,19 +86,24 @@ Jellyfin Web is the frontend used for most of the clients available for end user
|
|||
│ └── 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 🧹
|
||||
├── constants # Common constant values
|
||||
├── controllers # Legacy page views and controllers 🧹 ❌
|
||||
├── elements # Basic webcomponents and React equivalents 🧹
|
||||
├── hooks # Custom React hooks
|
||||
├── legacy # Polyfills for legacy browsers
|
||||
├── libraries # Third party libraries 🧹
|
||||
├── 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
|
||||
├── scripts # Random assortment of visual components and utilities 🐉 ❌
|
||||
├── strings # Translation files (only commit changes to en-us.json)
|
||||
├── styles # Common app Sass stylesheets
|
||||
├── themes # CSS themes
|
||||
├── types # Common TypeScript interfaces/types
|
||||
└── utils # Utility functions
|
||||
```
|
||||
|
||||
- ❌ — Deprecated, do **not** create new files here
|
||||
- 🧹 — Needs cleanup
|
||||
- 🐉 — Serious mess (Here be dragons)
|
||||
|
|
|
@ -15,8 +15,5 @@ module.exports = {
|
|||
'@babel/preset-react'
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
'@babel/plugin-proposal-private-methods',
|
||||
'babel-plugin-dynamic-import-polyfill'
|
||||
]
|
||||
};
|
||||
|
|
385
eslint.config.mjs
Normal file
385
eslint.config.mjs
Normal file
|
@ -0,0 +1,385 @@
|
|||
// @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': 'warn',
|
||||
'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'
|
||||
}
|
||||
},
|
||||
|
||||
// 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, CSS selectors,
|
||||
// and api_key parameter
|
||||
{
|
||||
selector: [ 'objectLiteralProperty', 'typeProperty' ],
|
||||
format: null,
|
||||
filter: {
|
||||
regex: '[ &\\-]|^([0-9]+)$|^api_key$',
|
||||
match: true
|
||||
}
|
||||
}
|
||||
],
|
||||
'@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',
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
|
||||
'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'
|
||||
}
|
||||
}
|
||||
);
|
20108
package-lock.json
generated
20108
package-lock.json
generated
File diff suppressed because it is too large
Load diff
156
package.json
156
package.json
|
@ -1,130 +1,137 @@
|
|||
{
|
||||
"name": "jellyfin-web",
|
||||
"version": "10.10.0",
|
||||
"version": "10.11.0",
|
||||
"description": "Web interface for Jellyfin",
|
||||
"repository": "https://github.com/jellyfin/jellyfin-web",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.24.5",
|
||||
"@babel/plugin-proposal-class-properties": "7.18.6",
|
||||
"@babel/plugin-proposal-private-methods": "7.18.6",
|
||||
"@babel/plugin-transform-modules-umd": "7.24.1",
|
||||
"@babel/preset-env": "7.24.5",
|
||||
"@babel/preset-react": "7.24.1",
|
||||
"@babel/core": "7.26.9",
|
||||
"@babel/plugin-transform-modules-umd": "7.25.9",
|
||||
"@babel/preset-env": "7.26.9",
|
||||
"@babel/preset-react": "7.26.3",
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
|
||||
"@eslint/js": "9.20.0",
|
||||
"@stylistic/eslint-plugin": "3.1.0",
|
||||
"@stylistic/stylelint-plugin": "3.1.2",
|
||||
"@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.1",
|
||||
"@types/react": "17.0.80",
|
||||
"@types/react-dom": "17.0.25",
|
||||
"@types/markdown-it": "14.1.2",
|
||||
"@types/react": "18.3.11",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@types/react-lazy-load-image-component": "1.6.4",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "5.62.0",
|
||||
"@typescript-eslint/parser": "5.62.0",
|
||||
"@typescript-eslint/parser": "8.24.1",
|
||||
"@uupaa/dynamic-import-polyfill": "1.0.2",
|
||||
"autoprefixer": "10.4.19",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-dynamic-import-polyfill": "1.0.0",
|
||||
"@vitest/coverage-v8": "3.0.5",
|
||||
"autoprefixer": "10.4.20",
|
||||
"babel-loader": "9.2.1",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"confusing-browser-globals": "1.0.11",
|
||||
"copy-webpack-plugin": "12.0.2",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "7.1.1",
|
||||
"cssnano": "7.0.1",
|
||||
"es-check": "7.1.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-compat": "4.2.0",
|
||||
"eslint-plugin-eslint-comments": "3.2.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "6.8.0",
|
||||
"eslint-plugin-react": "7.34.1",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-sonarjs": "0.25.1",
|
||||
"expose-loader": "5.0.0",
|
||||
"css-loader": "7.1.2",
|
||||
"cssnano": "7.0.6",
|
||||
"es-check": "7.2.1",
|
||||
"eslint": "9.20.1",
|
||||
"eslint-plugin-compat": "6.0.2",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-react": "7.37.4",
|
||||
"eslint-plugin-react-hooks": "5.1.0",
|
||||
"eslint-plugin-sonarjs": "3.0.2",
|
||||
"expose-loader": "5.0.1",
|
||||
"fork-ts-checker-webpack-plugin": "9.0.2",
|
||||
"html-loader": "5.0.0",
|
||||
"html-webpack-plugin": "5.6.0",
|
||||
"jsdom": "24.0.0",
|
||||
"mini-css-extract-plugin": "2.9.0",
|
||||
"postcss": "8.4.38",
|
||||
"globals": "15.15.0",
|
||||
"html-loader": "5.1.0",
|
||||
"html-webpack-plugin": "5.6.3",
|
||||
"jsdom": "25.0.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"postcss": "8.5.2",
|
||||
"postcss-loader": "8.1.1",
|
||||
"postcss-preset-env": "9.5.12",
|
||||
"postcss-preset-env": "10.1.4",
|
||||
"postcss-scss": "4.0.9",
|
||||
"sass": "1.77.1",
|
||||
"sass-loader": "14.2.1",
|
||||
"sass": "1.85.0",
|
||||
"sass-loader": "16.0.5",
|
||||
"source-map-loader": "5.0.0",
|
||||
"speed-measure-webpack-plugin": "1.5.0",
|
||||
"style-loader": "4.0.0",
|
||||
"stylelint": "15.11.0",
|
||||
"stylelint": "16.14.1",
|
||||
"stylelint-config-rational-order": "0.1.2",
|
||||
"stylelint-no-browser-hacks": "1.3.0",
|
||||
"stylelint-order": "6.0.4",
|
||||
"stylelint-scss": "5.3.2",
|
||||
"ts-loader": "9.5.1",
|
||||
"typescript": "5.4.5",
|
||||
"vitest": "1.6.0",
|
||||
"webpack": "5.91.0",
|
||||
"stylelint-scss": "6.11.0",
|
||||
"ts-loader": "9.5.2",
|
||||
"typescript": "5.7.3",
|
||||
"typescript-eslint": "8.24.1",
|
||||
"vitest": "3.0.5",
|
||||
"webpack": "5.98.0",
|
||||
"webpack-bundle-analyzer": "4.10.2",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.0.4",
|
||||
"webpack-merge": "5.10.0",
|
||||
"webpack-dev-server": "5.2.0",
|
||||
"webpack-merge": "6.0.1",
|
||||
"worker-loader": "3.0.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.11.4",
|
||||
"@emotion/styled": "11.11.5",
|
||||
"@fontsource/noto-sans": "5.0.22",
|
||||
"@fontsource/noto-sans-hk": "5.0.19",
|
||||
"@fontsource/noto-sans-jp": "5.0.19",
|
||||
"@fontsource/noto-sans-kr": "5.0.19",
|
||||
"@fontsource/noto-sans-sc": "5.0.19",
|
||||
"@fontsource/noto-sans-tc": "5.0.19",
|
||||
"@jellyfin/libass-wasm": "4.2.1",
|
||||
"@jellyfin/sdk": "0.0.0-unstable.202405190501",
|
||||
"@loadable/component": "5.16.4",
|
||||
"@mui/icons-material": "5.15.17",
|
||||
"@mui/material": "5.15.17",
|
||||
"@mui/x-data-grid": "6.19.11",
|
||||
"@react-hook/resize-observer": "1.2.6",
|
||||
"@tanstack/react-query": "4.36.1",
|
||||
"@tanstack/react-query-devtools": "4.36.1",
|
||||
"@types/react-lazy-load-image-component": "1.6.4",
|
||||
"abortcontroller-polyfill": "1.7.5",
|
||||
"@emotion/react": "11.13.3",
|
||||
"@emotion/styled": "11.13.0",
|
||||
"@fontsource/noto-sans": "5.1.1",
|
||||
"@fontsource/noto-sans-hk": "5.1.1",
|
||||
"@fontsource/noto-sans-jp": "5.1.1",
|
||||
"@fontsource/noto-sans-kr": "5.1.1",
|
||||
"@fontsource/noto-sans-sc": "5.1.1",
|
||||
"@fontsource/noto-sans-tc": "5.1.1",
|
||||
"@jellyfin/libass-wasm": "4.2.3",
|
||||
"@jellyfin/sdk": "0.0.0-unstable.202502210501",
|
||||
"@mui/icons-material": "5.16.14",
|
||||
"@mui/material": "5.16.14",
|
||||
"@mui/x-date-pickers": "7.26.0",
|
||||
"@react-hook/resize-observer": "2.0.2",
|
||||
"@tanstack/react-query": "5.62.16",
|
||||
"@tanstack/react-query-devtools": "5.62.16",
|
||||
"abortcontroller-polyfill": "1.7.8",
|
||||
"blurhash": "2.0.5",
|
||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.37.0",
|
||||
"core-js": "3.38.1",
|
||||
"date-fns": "2.30.0",
|
||||
"dompurify": "3.0.1",
|
||||
"dompurify": "2.5.8",
|
||||
"epubjs": "0.3.93",
|
||||
"escape-html": "1.0.3",
|
||||
"fast-text-encoding": "1.0.6",
|
||||
"flv.js": "1.6.2",
|
||||
"headroom.js": "0.12.0",
|
||||
"history": "5.3.0",
|
||||
"hls.js": "1.5.8",
|
||||
"hls.js": "1.5.20",
|
||||
"intersection-observer": "0.12.2",
|
||||
"jellyfin-apiclient": "1.11.0",
|
||||
"jquery": "3.7.1",
|
||||
"jstree": "3.3.16",
|
||||
"libarchive.js": "1.3.0",
|
||||
"jstree": "3.3.17",
|
||||
"libarchive.js": "2.0.2",
|
||||
"libpgs": "0.8.1",
|
||||
"lodash-es": "4.17.21",
|
||||
"markdown-it": "14.1.0",
|
||||
"material-design-icons-iconfont": "6.7.0",
|
||||
"material-react-table": "2.13.3",
|
||||
"native-promise-only": "0.8.1",
|
||||
"pdfjs-dist": "3.11.174",
|
||||
"react": "17.0.2",
|
||||
"react": "18.3.1",
|
||||
"react-blurhash": "0.3.0",
|
||||
"react-dom": "17.0.2",
|
||||
"react-lazy-load-image-component": "1.6.0",
|
||||
"react-router-dom": "6.23.1",
|
||||
"react-dom": "18.3.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.2",
|
||||
"swiper": "11.1.1",
|
||||
"usehooks-ts": "3.1.0",
|
||||
"sortablejs": "1.15.6",
|
||||
"swiper": "11.2.3",
|
||||
"usehooks-ts": "3.1.1",
|
||||
"webcomponents.js": "0.7.24",
|
||||
"whatwg-fetch": "3.6.20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sass-embedded": "1.85.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 Firefox versions",
|
||||
"last 2 Chrome versions",
|
||||
|
@ -148,6 +155,7 @@
|
|||
"build:development": "webpack --config webpack.dev.js",
|
||||
"build:production": "cross-env NODE_ENV=\"production\" webpack --config webpack.prod.js",
|
||||
"build:check": "tsc --noEmit",
|
||||
"build:es-check": "npm run build:production && npm run escheck",
|
||||
"escheck": "es-check",
|
||||
"lint": "eslint \"./\"",
|
||||
"test": "vitest --watch=false --config vite.config.ts",
|
||||
|
|
|
@ -1,37 +1,31 @@
|
|||
import loadable from '@loadable/component';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { History } from '@remix-run/router';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import React from 'react';
|
||||
|
||||
import { ApiProvider } from 'hooks/useApi';
|
||||
import { UserSettingsProvider } from 'hooks/useUserSettings';
|
||||
import { WebConfigProvider } from 'hooks/useWebConfig';
|
||||
import theme from 'themes/theme';
|
||||
import browser from 'scripts/browser';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
const StableAppRouter = loadable(() => import('./apps/stable/AppRouter'));
|
||||
const RootAppRouter = loadable(() => import('./RootAppRouter'));
|
||||
import RootAppRouter from 'RootAppRouter';
|
||||
|
||||
const RootApp = ({ history }: Readonly<{ history: History }>) => {
|
||||
const layoutMode = localStorage.getItem('layout');
|
||||
const isExperimentalLayout = layoutMode === 'experimental';
|
||||
const useReactQueryDevtools = window.Proxy // '@tanstack/query-devtools' requires 'Proxy', which cannot be polyfilled for legacy browsers
|
||||
&& !browser.tv; // Don't use devtools on the TV as the navigation is weird
|
||||
|
||||
return (
|
||||
const RootApp = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ApiProvider>
|
||||
<UserSettingsProvider>
|
||||
<WebConfigProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
{isExperimentalLayout ?
|
||||
<RootAppRouter history={history} /> :
|
||||
<StableAppRouter history={history} />
|
||||
}
|
||||
</ThemeProvider>
|
||||
<RootAppRouter />
|
||||
</WebConfigProvider>
|
||||
</UserSettingsProvider>
|
||||
</ApiProvider>
|
||||
{useReactQueryDevtools && (
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default RootApp;
|
||||
|
|
|
@ -1,31 +1,41 @@
|
|||
|
||||
import { History } from '@remix-run/router';
|
||||
import React from 'react';
|
||||
import {
|
||||
RouterProvider,
|
||||
createHashRouter,
|
||||
Outlet
|
||||
Outlet,
|
||||
useLocation
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { DASHBOARD_APP_PATHS, DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
||||
import { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/routes/routes';
|
||||
import { STABLE_APP_ROUTES } from 'apps/stable/routes/routes';
|
||||
import AppHeader from 'components/AppHeader';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
import { useLegacyRouterSync } from 'hooks/useLegacyRouterSync';
|
||||
import { DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
||||
import BangRedirect from 'components/router/BangRedirect';
|
||||
import { createRouterHistory } from 'components/router/routerHistory';
|
||||
import UserThemeProvider from 'themes/UserThemeProvider';
|
||||
|
||||
const layoutMode = localStorage.getItem('layout');
|
||||
const isExperimentalLayout = layoutMode === 'experimental';
|
||||
|
||||
const router = createHashRouter([
|
||||
{
|
||||
element: <RootAppLayout />,
|
||||
children: [
|
||||
...EXPERIMENTAL_APP_ROUTES,
|
||||
...DASHBOARD_APP_ROUTES
|
||||
...(isExperimentalLayout ? EXPERIMENTAL_APP_ROUTES : STABLE_APP_ROUTES),
|
||||
...DASHBOARD_APP_ROUTES,
|
||||
{
|
||||
path: '!/*',
|
||||
Component: BangRedirect
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
export default function RootAppRouter({ history }: Readonly<{ history: History}>) {
|
||||
useLegacyRouterSync({ router, history });
|
||||
export const history = createRouterHistory(router);
|
||||
|
||||
export default function RootAppRouter() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
|
@ -34,12 +44,16 @@ export default function RootAppRouter({ history }: Readonly<{ history: History}>
|
|||
* NOTE: The app will crash if these get removed from the DOM.
|
||||
*/
|
||||
function RootAppLayout() {
|
||||
const location = useLocation();
|
||||
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
|
||||
.some(path => location.pathname.startsWith(`/${path}`));
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserThemeProvider>
|
||||
<Backdrop />
|
||||
<AppHeader isHidden />
|
||||
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
|
||||
|
||||
<Outlet />
|
||||
</>
|
||||
</UserThemeProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ import AppBar from '@mui/material/AppBar';
|
|||
import Box from '@mui/material/Box';
|
||||
import { type Theme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
||||
import React, { FC, StrictMode, useCallback, useEffect, useState } from 'react';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
|
||||
import AppBody from 'components/AppBody';
|
||||
|
@ -10,34 +12,45 @@ import AppToolbar from 'components/toolbar/AppToolbar';
|
|||
import ElevationScroll from 'components/ElevationScroll';
|
||||
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
|
||||
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';
|
||||
|
||||
interface AppLayoutProps {
|
||||
drawerlessPaths: string[]
|
||||
}
|
||||
const DRAWERLESS_PATHS = [ DASHBOARD_APP_PATHS.MetadataManager ];
|
||||
|
||||
const AppLayout: FC<AppLayoutProps> = ({
|
||||
drawerlessPaths
|
||||
}) => {
|
||||
export const Component: FC = () => {
|
||||
const [ isDrawerActive, setIsDrawerActive ] = useState(false);
|
||||
const location = useLocation();
|
||||
const { user } = useApi();
|
||||
const { dateFnsLocale } = useLocale();
|
||||
|
||||
const isMediumScreen = useMediaQuery((t: Theme) => t.breakpoints.up('md'));
|
||||
const isDrawerAvailable = Boolean(user)
|
||||
&& !drawerlessPaths.some(path => location.pathname.startsWith(`/${path}`));
|
||||
&& !DRAWERLESS_PATHS.some(path => location.pathname.startsWith(`/${path}`));
|
||||
const isDrawerOpen = isDrawerActive && isDrawerAvailable;
|
||||
|
||||
const onToggleDrawer = useCallback(() => {
|
||||
setIsDrawerActive(!isDrawerActive);
|
||||
}, [ isDrawerActive, setIsDrawerActive ]);
|
||||
|
||||
// Update body class
|
||||
useEffect(() => {
|
||||
document.body.classList.add('dashboardDocument');
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove('dashboardDocument');
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={dateFnsLocale}>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<StrictMode>
|
||||
<ElevationScroll elevate={false}>
|
||||
<AppBar
|
||||
position='fixed'
|
||||
|
@ -56,6 +69,9 @@ const AppLayout: FC<AppLayoutProps> = ({
|
|||
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
|
||||
isDrawerOpen={isDrawerOpen}
|
||||
onDrawerButtonClick={onToggleDrawer}
|
||||
buttons={
|
||||
<HelpButton />
|
||||
}
|
||||
>
|
||||
<AppTabs isDrawerOpen={isDrawerOpen} />
|
||||
</AppToolbar>
|
||||
|
@ -71,6 +87,7 @@ const AppLayout: FC<AppLayoutProps> = ({
|
|||
/>
|
||||
)
|
||||
}
|
||||
</StrictMode>
|
||||
|
||||
<Box
|
||||
component='main'
|
||||
|
@ -84,7 +101,6 @@ const AppLayout: FC<AppLayoutProps> = ({
|
|||
</AppBody>
|
||||
</Box>
|
||||
</Box>
|
||||
</LocalizationProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
|
|
|
@ -5,10 +5,14 @@ $mui-bp-md: 900px;
|
|||
$mui-bp-lg: 1200px;
|
||||
$mui-bp-xl: 1536px;
|
||||
|
||||
$drawer-width: 240px;
|
||||
|
||||
// Fix dashboard pages layout to work with drawer
|
||||
.dashboardDocument {
|
||||
.mainAnimatedPage {
|
||||
position: relative;
|
||||
.mainAnimatedPage:not(.metadataEditorPage) {
|
||||
@media all and (min-width: $mui-bp-md) {
|
||||
left: $drawer-width;
|
||||
}
|
||||
}
|
||||
|
||||
.skinBody {
|
||||
|
@ -27,4 +31,8 @@ $mui-bp-xl: 1536px;
|
|||
padding-top: 3.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.metadataEditorPage {
|
||||
padding-top: 3.25rem !important;
|
||||
}
|
||||
}
|
||||
|
|
35
src/apps/dashboard/components/UserAvatarButton.tsx
Normal file
35
src/apps/dashboard/components/UserAvatarButton.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
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/IconButton';
|
||||
import React, { type FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import UserAvatar from 'components/UserAvatar';
|
||||
|
||||
interface UserAvatarButtonProps {
|
||||
user?: UserDto
|
||||
sx?: SxProps<Theme>
|
||||
}
|
||||
|
||||
const UserAvatarButton: FC<UserAvatarButtonProps> = ({
|
||||
user,
|
||||
sx
|
||||
}) => (
|
||||
user?.Id ? (
|
||||
<IconButton
|
||||
size='large'
|
||||
color='inherit'
|
||||
sx={{
|
||||
padding: 0,
|
||||
...sx
|
||||
}}
|
||||
title={user.Name || undefined}
|
||||
component={Link}
|
||||
to={`/dashboard/users/profile?userId=${user.Id}`}
|
||||
>
|
||||
<UserAvatar user={user} />
|
||||
</IconButton>
|
||||
) : undefined
|
||||
);
|
||||
|
||||
export default UserAvatarButton;
|
|
@ -1,17 +0,0 @@
|
|||
import React, { type RefAttributes } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { GridActionsCellItem, type GridActionsCellItemProps } from '@mui/x-data-grid';
|
||||
|
||||
type GridActionsCellLinkProps = { to: string } & GridActionsCellItemProps & RefAttributes<HTMLButtonElement>;
|
||||
|
||||
/**
|
||||
* Link component to use in mui's data-grid action column due to a current bug with passing props to custom link components.
|
||||
* @see https://github.com/mui/mui-x/issues/4654
|
||||
*/
|
||||
const GridActionsCellLink = ({ to, ...props }: GridActionsCellLinkProps) => (
|
||||
<Link to={to}>
|
||||
<GridActionsCellItem {...props} />
|
||||
</Link>
|
||||
);
|
||||
|
||||
export default GridActionsCellLink;
|
|
@ -29,8 +29,8 @@ const AppDrawer: FC<ResponsiveDrawerProps> = ({
|
|||
<ServerDrawerSection />
|
||||
<DevicesDrawerSection />
|
||||
<LiveTvDrawerSection />
|
||||
<AdvancedDrawerSection />
|
||||
<PluginDrawerSection />
|
||||
<AdvancedDrawerSection />
|
||||
</ResponsiveDrawer>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,36 +1,18 @@
|
|||
import Article from '@mui/icons-material/Article';
|
||||
import EditNotifications from '@mui/icons-material/EditNotifications';
|
||||
import ExpandLess from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
import Extension from '@mui/icons-material/Extension';
|
||||
import Lan from '@mui/icons-material/Lan';
|
||||
import Schedule from '@mui/icons-material/Schedule';
|
||||
import VpnKey from '@mui/icons-material/VpnKey';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
const PLUGIN_PATHS = [
|
||||
'/dashboard/plugins',
|
||||
'/dashboard/plugins/catalog',
|
||||
'/dashboard/plugins/repositories',
|
||||
'/dashboard/plugins/add',
|
||||
'/configurationpage'
|
||||
];
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
const AdvancedDrawerSection = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const isPluginSectionOpen = PLUGIN_PATHS.includes(location.pathname);
|
||||
|
||||
return (
|
||||
<List
|
||||
aria-labelledby='advanced-subheader'
|
||||
|
@ -64,36 +46,6 @@ const AdvancedDrawerSection = () => {
|
|||
<ListItemText primary={globalize.translate('TabLogs')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/notifications'>
|
||||
<ListItemIcon>
|
||||
<EditNotifications />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('Notifications')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/plugins' selected={false}>
|
||||
<ListItemIcon>
|
||||
<Extension />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabPlugins')} />
|
||||
{isPluginSectionOpen ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<Collapse in={isPluginSectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
<ListItemLink to='/dashboard/plugins' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabMyPlugins')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dashboard/plugins/catalog' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabCatalog')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dashboard/plugins/repositories' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabRepositories')} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
</Collapse>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/tasks'>
|
||||
<ListItemIcon>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Devices, Analytics, Input } from '@mui/icons-material';
|
||||
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';
|
||||
|
@ -7,7 +7,7 @@ import ListSubheader from '@mui/material/ListSubheader';
|
|||
import React from 'react';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'scripts/globalize';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
const DevicesDrawerSection = () => {
|
||||
return (
|
||||
|
@ -35,14 +35,6 @@ const DevicesDrawerSection = () => {
|
|||
<ListItemText primary={globalize.translate('HeaderActivity')} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/dlna'>
|
||||
<ListItemIcon>
|
||||
<Input />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={'DLNA'} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@ import ListSubheader from '@mui/material/ListSubheader';
|
|||
import React from 'react';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'scripts/globalize';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
const LiveTvDrawerSection = () => {
|
||||
return (
|
||||
|
|
|
@ -1,41 +1,26 @@
|
|||
import { ConfigurationPageInfo } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { getDashboardApi } from '@jellyfin/sdk/lib/utils/api/dashboard-api';
|
||||
import { Folder } from '@mui/icons-material';
|
||||
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 ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import globalize from 'scripts/globalize';
|
||||
import globalize from 'lib/globalize';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import { useConfigurationPages } from 'apps/dashboard/features/plugins/api/useConfigurationPages';
|
||||
|
||||
const PluginDrawerSection = () => {
|
||||
const { api } = useApi();
|
||||
const [ pagesInfo, setPagesInfo ] = useState<ConfigurationPageInfo[]>([]);
|
||||
const {
|
||||
data: pagesInfo,
|
||||
error
|
||||
} = useConfigurationPages({ enableInMainMenu: true });
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPluginPages = async () => {
|
||||
if (!api) return;
|
||||
|
||||
const pagesResponse = await getDashboardApi(api)
|
||||
.getConfigurationPages({ enableInMainMenu: true });
|
||||
|
||||
setPagesInfo(pagesResponse.data);
|
||||
};
|
||||
|
||||
fetchPluginPages()
|
||||
.catch(err => {
|
||||
console.error('[PluginDrawerSection] unable to fetch plugin config pages', err);
|
||||
});
|
||||
}, [ api ]);
|
||||
|
||||
if (!api || pagesInfo.length < 1) {
|
||||
return null;
|
||||
}
|
||||
if (error) console.error('[PluginDrawerSection] unable to fetch plugin config pages', error);
|
||||
}, [ error ]);
|
||||
|
||||
return (
|
||||
<List
|
||||
|
@ -46,19 +31,39 @@ const PluginDrawerSection = () => {
|
|||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
{
|
||||
pagesInfo.map(pageInfo => (
|
||||
<ListItem key={pageInfo.PluginId} disablePadding>
|
||||
<ListItemLink to={`/${Dashboard.getPluginUrl(pageInfo.Name)}`}>
|
||||
<ListItemLink
|
||||
to='/dashboard/plugins'
|
||||
includePaths={[ '/configurationpage' ]}
|
||||
excludePaths={pagesInfo?.map(p => `/${Dashboard.getPluginUrl(p.Name)}`)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Extension />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabMyPlugins')} />
|
||||
</ListItemLink>
|
||||
|
||||
<ListItemLink
|
||||
to='/dashboard/plugins/catalog'
|
||||
includePaths={[ '/dashboard/plugins/repositories' ]}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Public />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabCatalog')} />
|
||||
</ListItemLink>
|
||||
|
||||
{pagesInfo?.map(pageInfo => (
|
||||
<ListItemLink
|
||||
key={pageInfo.PluginId}
|
||||
to={`/${Dashboard.getPluginUrl(pageInfo.Name)}`}
|
||||
>
|
||||
<ListItemIcon>
|
||||
{/* TODO: Support different icons? */}
|
||||
<Folder />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={pageInfo.DisplayName} />
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
))
|
||||
}
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import { Dashboard, ExpandLess, ExpandMore, LibraryAdd, People, PlayCircle, Settings } from '@mui/icons-material';
|
||||
import Palette from '@mui/icons-material/Palette';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemButton from '@mui/material/ListItemButton/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import React from 'react';
|
||||
import React, { type MouseEvent, useCallback, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import ListItemLink from 'components/ListItemLink';
|
||||
import globalize from 'scripts/globalize';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
const LIBRARY_PATHS = [
|
||||
'/dashboard/libraries',
|
||||
|
@ -28,8 +30,20 @@ const PLAYBACK_PATHS = [
|
|||
const ServerDrawerSection = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const isLibrarySectionOpen = LIBRARY_PATHS.includes(location.pathname);
|
||||
const isPlaybackSectionOpen = PLAYBACK_PATHS.includes(location.pathname);
|
||||
const [ isLibrarySectionOpen, setIsLibrarySectionOpen ] = useState(LIBRARY_PATHS.includes(location.pathname));
|
||||
const [ isPlaybackSectionOpen, setIsPlaybackSectionOpen ] = useState(PLAYBACK_PATHS.includes(location.pathname));
|
||||
|
||||
const onLibrarySectionClick = useCallback((e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsLibrarySectionOpen(isOpen => !isOpen);
|
||||
}, []);
|
||||
|
||||
const onPlaybackSectionClick = useCallback((e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsPlaybackSectionOpen(isOpen => !isOpen);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<List
|
||||
|
@ -56,6 +70,12 @@ 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>
|
||||
|
@ -65,13 +85,13 @@ const ServerDrawerSection = () => {
|
|||
</ListItemLink>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/libraries' selected={false}>
|
||||
<ListItemButton onClick={onLibrarySectionClick}>
|
||||
<ListItemIcon>
|
||||
<LibraryAdd />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('HeaderLibraries')} />
|
||||
{isLibrarySectionOpen ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemLink>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<Collapse in={isLibrarySectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
|
@ -82,7 +102,7 @@ const ServerDrawerSection = () => {
|
|||
<ListItemText inset primary={globalize.translate('Display')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dashboard/libraries/metadata' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Metadata')} />
|
||||
<ListItemText inset primary={globalize.translate('LabelMetadata')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dashboard/libraries/nfo' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabNfoSettings')} />
|
||||
|
@ -90,13 +110,13 @@ const ServerDrawerSection = () => {
|
|||
</List>
|
||||
</Collapse>
|
||||
<ListItem disablePadding>
|
||||
<ListItemLink to='/dashboard/playback/transcoding' selected={false}>
|
||||
<ListItemButton onClick={onPlaybackSectionClick}>
|
||||
<ListItemIcon>
|
||||
<PlayCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TitlePlayback')} />
|
||||
{isPlaybackSectionOpen ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemLink>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<Collapse in={isPlaybackSectionOpen} timeout='auto' unmountOnExit>
|
||||
<List component='div' disablePadding>
|
||||
|
|
17
src/apps/dashboard/components/table/DateTimeCell.tsx
Normal file
17
src/apps/dashboard/components/table/DateTimeCell.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
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;
|
72
src/apps/dashboard/components/table/TablePage.tsx
Normal file
72
src/apps/dashboard/components/table/TablePage.tsx
Normal file
|
@ -0,0 +1,72 @@
|
|||
import Box from '@mui/material/Box/Box';
|
||||
import Stack from '@mui/material/Stack/Stack';
|
||||
import Typography from '@mui/material/Typography/Typography';
|
||||
import { type MRT_RowData, type MRT_TableInstance, 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 = {
|
||||
// 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='h2'>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle && (
|
||||
<Typography>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
<MaterialReactTable table={table} />
|
||||
</Box>
|
||||
{children}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default TablePage;
|
36
src/apps/dashboard/components/toolbar/HelpButton.tsx
Normal file
36
src/apps/dashboard/components/toolbar/HelpButton.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import HelpOutline from '@mui/icons-material/HelpOutline';
|
||||
import IconButton from '@mui/material/IconButton/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;
|
54
src/apps/dashboard/constants/helpLinks.ts
Normal file
54
src/apps/dashboard/constants/helpLinks.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
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',
|
||||
'/dashboard/plugins/catalog'
|
||||
],
|
||||
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/'
|
||||
}
|
||||
];
|
|
@ -1,4 +1,4 @@
|
|||
<div id="dashboardPage" data-role="page" class="page type-interior dashboardHomePage fullWidthContent">
|
||||
<div id="dashboardPage" data-role="page" class="page type-interior dashboardHomePage fullWidthContent" data-title="${TabDashboard}">
|
||||
<div class="content-primary">
|
||||
<div class="dashboardSections" style="padding-top:.5em;">
|
||||
<div class="dashboardColumn dashboardColumn-2-60 dashboardColumn-3-46">
|
||||
|
@ -23,10 +23,10 @@
|
|||
<button is="emby-button" type="button" class="raised btnRefresh">
|
||||
<span>${ButtonScanAllLibraries}</span>
|
||||
</button>
|
||||
<button is="emby-button" type="button" id="btnRestartServer" class="raised" onclick="DashboardPage.restart(this);">
|
||||
<button is="emby-button" type="button" id="btnRestartServer" class="raised">
|
||||
<span>${Restart}</span>
|
||||
</button>
|
||||
<button is="emby-button" type="button" id="btnShutdown" class="raised" onclick="DashboardPage.shutdown(this);">
|
||||
<button is="emby-button" type="button" id="btnShutdown" class="raised">
|
||||
<span>${ButtonShutdown}</span>
|
||||
</button>
|
||||
</div>
|
|
@ -1,36 +1,36 @@
|
|||
import escapeHtml from 'escape-html';
|
||||
|
||||
import datetime from '../../scripts/datetime';
|
||||
import Events from '../../utils/events.ts';
|
||||
import itemHelper from '../../components/itemHelper';
|
||||
import serverNotifications from '../../scripts/serverNotifications';
|
||||
import dom from '../../scripts/dom';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import datetime from 'scripts/datetime';
|
||||
import Events from 'utils/events.ts';
|
||||
import itemHelper from 'components/itemHelper';
|
||||
import serverNotifications from 'scripts/serverNotifications';
|
||||
import dom from 'scripts/dom';
|
||||
import globalize from 'lib/globalize';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { getLocaleWithSuffix } from '../../utils/dateFnsLocale.ts';
|
||||
import loading from '../../components/loading/loading';
|
||||
import playMethodHelper from '../../components/playback/playmethodhelper';
|
||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
||||
import imageLoader from '../../components/images/imageLoader';
|
||||
import ActivityLog from '../../components/activitylog';
|
||||
import imageHelper from '../../utils/image';
|
||||
import indicators from '../../components/indicators/indicators';
|
||||
import taskButton from '../../scripts/taskbutton';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import ServerConnections from '../../components/ServerConnections';
|
||||
import alert from '../../components/alert';
|
||||
import confirm from '../../components/confirm/confirm';
|
||||
import { getDefaultBackgroundClass } from '../../components/cardbuilder/cardBuilderUtils';
|
||||
import { getLocaleWithSuffix } from 'utils/dateFnsLocale.ts';
|
||||
import loading from 'components/loading/loading';
|
||||
import playMethodHelper from 'components/playback/playmethodhelper';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import imageLoader from 'components/images/imageLoader';
|
||||
import ActivityLog from 'components/activitylog';
|
||||
import imageHelper from 'utils/image';
|
||||
import indicators from 'components/indicators/indicators';
|
||||
import taskButton from 'scripts/taskbutton';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import alert from 'components/alert';
|
||||
import confirm from 'components/confirm/confirm';
|
||||
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
|
||||
|
||||
import { getSystemInfoQuery } from 'hooks/useSystemInfo';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
||||
import '../../components/listview/listview.scss';
|
||||
import '../../styles/flexstyles.scss';
|
||||
import 'components/listview/listview.scss';
|
||||
import 'styles/flexstyles.scss';
|
||||
import './dashboard.scss';
|
||||
|
||||
function showPlaybackInfo(btn, session) {
|
||||
|
@ -72,7 +72,7 @@ function showPlaybackInfo(btn, session) {
|
|||
}
|
||||
|
||||
function showSendMessageForm(btn, session) {
|
||||
import('../../components/prompt/prompt').then(({ default: prompt }) => {
|
||||
import('components/prompt/prompt').then(({ default: prompt }) => {
|
||||
prompt({
|
||||
title: globalize.translate('HeaderSendMessage'),
|
||||
label: globalize.translate('LabelMessageText'),
|
||||
|
@ -89,7 +89,7 @@ function showSendMessageForm(btn, session) {
|
|||
}
|
||||
|
||||
function showOptionsMenu(btn, session) {
|
||||
import('../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
import('components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
const menuItems = [];
|
||||
|
||||
if (session.ServerId && session.DeviceId !== ServerConnections.deviceId()) {
|
||||
|
@ -208,7 +208,12 @@ function refreshActiveRecordings(view, apiClient) {
|
|||
|
||||
function reloadSystemInfo(view, apiClient) {
|
||||
view.querySelector('#buildVersion').innerText = __JF_BUILD_VERSION__;
|
||||
view.querySelector('#webVersion').innerText = __PACKAGE_JSON_VERSION__;
|
||||
|
||||
let webVersion = __PACKAGE_JSON_VERSION__;
|
||||
if (__COMMIT_SHA__) {
|
||||
webVersion += ` (${__COMMIT_SHA__})`;
|
||||
}
|
||||
view.querySelector('#webVersion').innerText = webVersion;
|
||||
|
||||
queryClient
|
||||
.fetchQuery(getSystemInfoQuery(toApi(apiClient)))
|
||||
|
@ -319,7 +324,7 @@ function renderActiveConnections(view, sessions) {
|
|||
html += '<div class="sessionCardButtons flex align-items-center justify-content-center">';
|
||||
|
||||
let btnCssClass = session.ServerId && session.NowPlayingItem && session.SupportsRemoteControl ? '' : ' hide';
|
||||
const playIcon = session.PlayState.IsPaused ? 'pause' : 'play_arrow';
|
||||
const playIcon = session.PlayState.IsPaused ? 'play_arrow' : 'pause';
|
||||
|
||||
html += '<button is="paper-icon-button-light" class="sessionCardButton btnSessionPlayPause paper-icon-button-light ' + btnCssClass + '"><span class="material-icons ' + playIcon + '" aria-hidden="true"></span></button>';
|
||||
html += '<button is="paper-icon-button-light" class="sessionCardButton btnSessionStop paper-icon-button-light ' + btnCssClass + '"><span class="material-icons stop" aria-hidden="true"></span></button>';
|
||||
|
@ -389,7 +394,7 @@ function renderRunningTasks(view, tasks) {
|
|||
view.querySelector('#divRunningTasks').innerHTML = html;
|
||||
}
|
||||
|
||||
window.DashboardPage = {
|
||||
const DashboardPage = {
|
||||
startInterval: function (apiClient) {
|
||||
apiClient.sendMessage('SessionsStart', '0,1500');
|
||||
apiClient.sendMessage('ScheduledTasksInfoStart', '0,1000');
|
||||
|
@ -709,33 +714,38 @@ window.DashboardPage = {
|
|||
pollForInfo(page, ApiClient);
|
||||
});
|
||||
},
|
||||
restart: function (btn) {
|
||||
restart: function (event) {
|
||||
confirm({
|
||||
title: globalize.translate('Restart'),
|
||||
text: globalize.translate('MessageConfirmRestart'),
|
||||
confirmText: globalize.translate('Restart'),
|
||||
primary: 'delete'
|
||||
}).then(function () {
|
||||
const page = dom.parentWithClass(btn, 'page');
|
||||
}).then(() => {
|
||||
const page = dom.parentWithClass(event.target, 'page');
|
||||
page.querySelector('#btnRestartServer').disabled = true;
|
||||
page.querySelector('#btnShutdown').disabled = true;
|
||||
ApiClient.restartServer();
|
||||
}).catch(() => {
|
||||
// Confirm dialog closed
|
||||
});
|
||||
},
|
||||
shutdown: function (btn) {
|
||||
shutdown: function (event) {
|
||||
confirm({
|
||||
title: globalize.translate('ButtonShutdown'),
|
||||
text: globalize.translate('MessageConfirmShutdown'),
|
||||
confirmText: globalize.translate('ButtonShutdown'),
|
||||
primary: 'delete'
|
||||
}).then(function () {
|
||||
const page = dom.parentWithClass(btn, 'page');
|
||||
}).then(() => {
|
||||
const page = dom.parentWithClass(event.target, 'page');
|
||||
page.querySelector('#btnRestartServer').disabled = true;
|
||||
page.querySelector('#btnShutdown').disabled = true;
|
||||
ApiClient.shutdownServer();
|
||||
}).catch(() => {
|
||||
// Confirm dialog closed
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default function (view) {
|
||||
function onRestartRequired(evt, apiClient) {
|
||||
console.debug('onRestartRequired not implemented', evt, apiClient);
|
||||
|
@ -811,7 +821,11 @@ export default function (view) {
|
|||
taskKey: 'RefreshLibrary',
|
||||
button: page.querySelector('.btnRefresh')
|
||||
});
|
||||
|
||||
page.querySelector('#btnRestartServer').addEventListener('click', DashboardPage.restart);
|
||||
page.querySelector('#btnShutdown').addEventListener('click', DashboardPage.shutdown);
|
||||
});
|
||||
|
||||
view.addEventListener('viewbeforehide', function () {
|
||||
const apiClient = ApiClient;
|
||||
const page = this;
|
||||
|
@ -833,6 +847,9 @@ export default function (view) {
|
|||
taskKey: 'RefreshLibrary',
|
||||
button: page.querySelector('.btnRefresh')
|
||||
});
|
||||
|
||||
page.querySelector('#btnRestartServer').removeEventListener('click', DashboardPage.restart);
|
||||
page.querySelector('#btnShutdown').removeEventListener('click', DashboardPage.shutdown);
|
||||
});
|
||||
view.addEventListener('viewdestroy', function () {
|
||||
const page = this;
|
|
@ -1,17 +1,16 @@
|
|||
<div id="encodingSettingsPage" data-role="page" class="page type-interior playbackConfigurationPage">
|
||||
<div id="encodingSettingsPage" data-role="page" class="page type-interior playbackConfigurationPage" data-title="${TitlePlayback}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="encodingSettingsForm">
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${Transcoding}</h2>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/transcoding">${Help}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectVideoDecoder" label="${LabelHardwareAccelerationType}">
|
||||
<option value="">${None}</option>
|
||||
<option value="none">${None}</option>
|
||||
<option value="amf">AMD AMF</option>
|
||||
<option value="nvenc">Nvidia NVENC</option>
|
||||
<option value="qsv">Intel QuickSync (QSV)</option>
|
||||
|
@ -30,6 +29,11 @@
|
|||
<div class="fieldDescription">${LabelVaapiDeviceHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer hide fldQsvDevice">
|
||||
<input is="emby-input" type="text" id="txtQsvDevice" label="${LabelQsvDevice}" />
|
||||
<div class="fieldDescription">${LabelQsvDeviceHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="hardwareAccelerationOptions hide">
|
||||
<div class="checkboxListContainer decodingCodecsList">
|
||||
<h3 class="checkboxListLabel">${LabelEnableHardwareDecodingFor}</h3>
|
||||
|
@ -81,6 +85,16 @@
|
|||
<span>VP9 10bit</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkboxList hide fldHevcRextHwDecoding">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkDecodingColorDepth10HevcRext" />
|
||||
<span>HEVC RExt 8/10bit</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkDecodingColorDepth12HevcRext" />
|
||||
<span>HEVC RExt 12bit</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxListContainer hide fldEnhancedNvdec">
|
||||
|
@ -168,12 +182,13 @@
|
|||
</div>
|
||||
|
||||
<div class="tonemappingOptions hide">
|
||||
<div class="checkboxListContainer checkboxContainer-withDescription">
|
||||
<div class="checkboxListContainer checkboxContainer-withDescription fldTonemapCheckbox hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkTonemapping" />
|
||||
<span>${EnableTonemapping}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowTonemappingHelp}</div>
|
||||
<div class="fieldDescription checkboxFieldDescription allowTonemappingHardwareHelp">${AllowTonemappingHelp}</div>
|
||||
<div class="fieldDescription checkboxFieldDescription allowTonemappingSoftwareHelp">${AllowTonemappingSoftwareHelp}</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectTonemappingAlgorithm" label="${LabelTonemappingAlgorithm}">
|
||||
|
@ -190,11 +205,13 @@
|
|||
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="http://ffmpeg.org/ffmpeg-all.html#tonemap_005fopencl" target="_blank">${TonemappingAlgorithmHelp}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<div class="tonemappingModeOptions selectContainer">
|
||||
<select is="emby-select" id="selectTonemappingMode" label="${LabelTonemappingMode}">
|
||||
<option value="auto">${Auto}</option>
|
||||
<option value="max">MAX</option>
|
||||
<option value="rgb">RGB</option>
|
||||
<option value="lum">LUM</option>
|
||||
<option value="itp">ITP</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${TonemappingModeHelp}</div>
|
||||
</div>
|
||||
|
@ -297,6 +314,8 @@
|
|||
<option value="None">${None}</option>
|
||||
<option value="Dave750">Dave750</option>
|
||||
<option value="NightmodeDialogue">NightmodeDialogue</option>
|
||||
<option value="Rfc7845">RFC7845</option>
|
||||
<option value="Ac4">AC-4</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${StereoDownmixAlgorithmHelp}</div>
|
||||
</div>
|
||||
|
@ -307,7 +326,7 @@
|
|||
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectEncoderPreset" label="${LabelEncoderPreset}">
|
||||
<option value="">${Auto}</option>
|
||||
<option value="auto">${Auto}</option>
|
||||
<option value="veryslow">veryslow</option>
|
||||
<option value="slower">slower</option>
|
||||
<option value="slow">slow</option>
|
|
@ -1,9 +1,9 @@
|
|||
import 'jquery';
|
||||
import loading from '../../components/loading/loading';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import dom from '../../scripts/dom';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import alert from '../../components/alert';
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import dom from 'scripts/dom';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import alert from 'components/alert';
|
||||
|
||||
function loadPage(page, config, systemInfo) {
|
||||
Array.prototype.forEach.call(page.querySelectorAll('.chkDecodeCodec'), function (c) {
|
||||
|
@ -11,6 +11,8 @@ function loadPage(page, config, systemInfo) {
|
|||
});
|
||||
page.querySelector('#chkDecodingColorDepth10Hevc').checked = config.EnableDecodingColorDepth10Hevc;
|
||||
page.querySelector('#chkDecodingColorDepth10Vp9').checked = config.EnableDecodingColorDepth10Vp9;
|
||||
page.querySelector('#chkDecodingColorDepth10HevcRext').checked = config.EnableDecodingColorDepth10HevcRext;
|
||||
page.querySelector('#chkDecodingColorDepth12HevcRext').checked = config.EnableDecodingColorDepth12HevcRext;
|
||||
page.querySelector('#chkEnhancedNvdecDecoder').checked = config.EnableEnhancedNvdecDecoder;
|
||||
page.querySelector('#chkSystemNativeHwDecoder').checked = config.PreferSystemNativeHwDecoder;
|
||||
page.querySelector('#chkIntelLpH264HwEncoder').checked = config.EnableIntelLowPowerH264HwEncoder;
|
||||
|
@ -18,32 +20,33 @@ function loadPage(page, config, systemInfo) {
|
|||
page.querySelector('#chkHardwareEncoding').checked = config.EnableHardwareEncoding;
|
||||
page.querySelector('#chkAllowHevcEncoding').checked = config.AllowHevcEncoding;
|
||||
page.querySelector('#chkAllowAv1Encoding').checked = config.AllowAv1Encoding;
|
||||
$('#selectVideoDecoder', page).val(config.HardwareAccelerationType);
|
||||
$('#selectThreadCount', page).val(config.EncodingThreadCount);
|
||||
page.querySelector('#selectVideoDecoder').value = config.HardwareAccelerationType || 'none';
|
||||
page.querySelector('#selectThreadCount').value = config.EncodingThreadCount;
|
||||
page.querySelector('#chkEnableAudioVbr').checked = config.EnableAudioVbr;
|
||||
$('#txtDownMixAudioBoost', page).val(config.DownMixAudioBoost);
|
||||
$('#selectStereoDownmixAlgorithm').val(config.DownMixStereoAlgorithm || 'None');
|
||||
page.querySelector('#txtDownMixAudioBoost').value = config.DownMixAudioBoost;
|
||||
page.querySelector('#selectStereoDownmixAlgorithm').value = config.DownMixStereoAlgorithm || 'None';
|
||||
page.querySelector('#txtMaxMuxingQueueSize').value = config.MaxMuxingQueueSize || '';
|
||||
page.querySelector('.txtEncoderPath').value = config.EncoderAppPathDisplay || '';
|
||||
$('#txtTranscodingTempPath', page).val(systemInfo.TranscodingTempPath || '');
|
||||
page.querySelector('#txtTranscodingTempPath').value = systemInfo.TranscodingTempPath || '';
|
||||
page.querySelector('#txtFallbackFontPath').value = config.FallbackFontPath || '';
|
||||
page.querySelector('#chkEnableFallbackFont').checked = config.EnableFallbackFont;
|
||||
$('#txtVaapiDevice', page).val(config.VaapiDevice || '');
|
||||
page.querySelector('#txtVaapiDevice').value = config.VaapiDevice || '';
|
||||
page.querySelector('#txtQsvDevice').value = config.QsvDevice || '';
|
||||
page.querySelector('#chkTonemapping').checked = config.EnableTonemapping;
|
||||
page.querySelector('#chkVppTonemapping').checked = config.EnableVppTonemapping;
|
||||
page.querySelector('#chkVideoToolboxTonemapping').checked = config.EnableVideoToolboxTonemapping;
|
||||
page.querySelector('#selectTonemappingAlgorithm').value = config.TonemappingAlgorithm;
|
||||
page.querySelector('#selectTonemappingMode').value = config.TonemappingMode;
|
||||
page.querySelector('#selectTonemappingRange').value = config.TonemappingRange;
|
||||
page.querySelector('#selectTonemappingAlgorithm').value = config.TonemappingAlgorithm || 'none';
|
||||
page.querySelector('#selectTonemappingMode').value = config.TonemappingMode || 'auto';
|
||||
page.querySelector('#selectTonemappingRange').value = config.TonemappingRange || 'auto';
|
||||
page.querySelector('#txtTonemappingDesat').value = config.TonemappingDesat;
|
||||
page.querySelector('#txtTonemappingPeak').value = config.TonemappingPeak;
|
||||
page.querySelector('#txtTonemappingParam').value = config.TonemappingParam || '';
|
||||
page.querySelector('#txtVppTonemappingBrightness').value = config.VppTonemappingBrightness;
|
||||
page.querySelector('#txtVppTonemappingContrast').value = config.VppTonemappingContrast;
|
||||
page.querySelector('#selectEncoderPreset').value = config.EncoderPreset || '';
|
||||
page.querySelector('#selectEncoderPreset').value = config.EncoderPreset || 'auto';
|
||||
page.querySelector('#txtH264Crf').value = config.H264Crf || '';
|
||||
page.querySelector('#txtH265Crf').value = config.H265Crf || '';
|
||||
page.querySelector('#selectDeinterlaceMethod').value = config.DeinterlaceMethod || '';
|
||||
page.querySelector('#selectDeinterlaceMethod').value = config.DeinterlaceMethod || 'yadif';
|
||||
page.querySelector('#chkDoubleRateDeinterlacing').checked = config.DeinterlaceDoubleRate;
|
||||
page.querySelector('#chkEnableSubtitleExtraction').checked = config.EnableSubtitleExtraction || false;
|
||||
page.querySelector('#chkEnableThrottling').checked = config.EnableThrottling || false;
|
||||
|
@ -82,15 +85,16 @@ function onSubmit() {
|
|||
loading.show();
|
||||
ApiClient.getNamedConfiguration('encoding').then(function (config) {
|
||||
config.EnableAudioVbr = form.querySelector('#chkEnableAudioVbr').checked;
|
||||
config.DownMixAudioBoost = $('#txtDownMixAudioBoost', form).val();
|
||||
config.DownMixStereoAlgorithm = $('#selectStereoDownmixAlgorithm', form).val() || 'None';
|
||||
config.DownMixAudioBoost = form.querySelector('#txtDownMixAudioBoost').value;
|
||||
config.DownMixStereoAlgorithm = form.querySelector('#selectStereoDownmixAlgorithm').value || 'None';
|
||||
config.MaxMuxingQueueSize = form.querySelector('#txtMaxMuxingQueueSize').value;
|
||||
config.TranscodingTempPath = $('#txtTranscodingTempPath', form).val();
|
||||
config.TranscodingTempPath = form.querySelector('#txtTranscodingTempPath').value;
|
||||
config.FallbackFontPath = form.querySelector('#txtFallbackFontPath').value;
|
||||
config.EnableFallbackFont = form.querySelector('#txtFallbackFontPath').value ? form.querySelector('#chkEnableFallbackFont').checked : false;
|
||||
config.EncodingThreadCount = $('#selectThreadCount', form).val();
|
||||
config.HardwareAccelerationType = $('#selectVideoDecoder', form).val();
|
||||
config.VaapiDevice = $('#txtVaapiDevice', form).val();
|
||||
config.EncodingThreadCount = form.querySelector('#selectThreadCount').value;
|
||||
config.HardwareAccelerationType = form.querySelector('#selectVideoDecoder').value;
|
||||
config.VaapiDevice = form.querySelector('#txtVaapiDevice').value;
|
||||
config.QsvDevice = form.querySelector('#txtQsvDevice').value;
|
||||
config.EnableTonemapping = form.querySelector('#chkTonemapping').checked;
|
||||
config.EnableVppTonemapping = form.querySelector('#chkVppTonemapping').checked;
|
||||
config.EnableVideoToolboxTonemapping = form.querySelector('#chkVideoToolboxTonemapping').checked;
|
||||
|
@ -119,6 +123,8 @@ function onSubmit() {
|
|||
});
|
||||
config.EnableDecodingColorDepth10Hevc = form.querySelector('#chkDecodingColorDepth10Hevc').checked;
|
||||
config.EnableDecodingColorDepth10Vp9 = form.querySelector('#chkDecodingColorDepth10Vp9').checked;
|
||||
config.EnableDecodingColorDepth10HevcRext = form.querySelector('#chkDecodingColorDepth10HevcRext').checked;
|
||||
config.EnableDecodingColorDepth12HevcRext = form.querySelector('#chkDecodingColorDepth12HevcRext').checked;
|
||||
config.EnableEnhancedNvdecDecoder = form.querySelector('#chkEnhancedNvdecDecoder').checked;
|
||||
config.PreferSystemNativeHwDecoder = form.querySelector('#chkSystemNativeHwDecoder').checked;
|
||||
config.EnableIntelLowPowerH264HwEncoder = form.querySelector('#chkIntelLpH264HwEncoder').checked;
|
||||
|
@ -135,7 +141,7 @@ function onSubmit() {
|
|||
});
|
||||
};
|
||||
|
||||
if ($('#selectVideoDecoder', form).val()) {
|
||||
if (form.querySelector('#selectVideoDecoder').value !== 'none') {
|
||||
alert({
|
||||
title: globalize.translate('TitleHardwareAcceleration'),
|
||||
text: globalize.translate('HardwareAccelerationWarning')
|
||||
|
@ -188,18 +194,34 @@ $(document).on('pageinit', '#encodingSettingsPage', function () {
|
|||
page.querySelector('#txtVaapiDevice').removeAttribute('required');
|
||||
}
|
||||
|
||||
if (this.value == 'amf' || this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi' || this.value == 'rkmpp' || this.value == 'videotoolbox') {
|
||||
if (this.value == 'amf' || this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi' || this.value == 'rkmpp') {
|
||||
page.querySelector('.fld10bitHevcVp9HwDecoding').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fld10bitHevcVp9HwDecoding').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'amf' || this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi' || this.value == 'rkmpp' || this.value == 'videotoolbox') {
|
||||
if (this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi') {
|
||||
page.querySelector('.fldHevcRextHwDecoding').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldHevcRextHwDecoding').classList.add('hide');
|
||||
}
|
||||
|
||||
const isHwaSelected = [ 'amf', 'nvenc', 'qsv', 'vaapi', 'rkmpp', 'videotoolbox' ].includes(this.value);
|
||||
if (this.value === 'none') {
|
||||
page.querySelector('.tonemappingOptions').classList.remove('hide');
|
||||
page.querySelector('.fldTonemapCheckbox').classList.add('hide');
|
||||
} else if (isHwaSelected) {
|
||||
page.querySelector('.tonemappingOptions').classList.remove('hide');
|
||||
page.querySelector('.fldTonemapCheckbox').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.tonemappingOptions').classList.add('hide');
|
||||
page.querySelector('.fldTonemapCheckbox').classList.add('hide');
|
||||
}
|
||||
|
||||
page.querySelector('.tonemappingModeOptions').classList.toggle('hide', !isHwaSelected);
|
||||
page.querySelector('.allowTonemappingHardwareHelp').classList.toggle('hide', !isHwaSelected);
|
||||
page.querySelector('.allowTonemappingSoftwareHelp').classList.toggle('hide', isHwaSelected);
|
||||
|
||||
if (this.value == 'qsv' || this.value == 'vaapi') {
|
||||
page.querySelector('.fldIntelLp').classList.remove('hide');
|
||||
} else {
|
||||
|
@ -220,8 +242,10 @@ $(document).on('pageinit', '#encodingSettingsPage', function () {
|
|||
|
||||
if (this.value == 'qsv') {
|
||||
page.querySelector('.fldSysNativeHwDecoder').classList.remove('hide');
|
||||
page.querySelector('.fldQsvDevice').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldSysNativeHwDecoder').classList.add('hide');
|
||||
page.querySelector('.fldQsvDevice').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'nvenc') {
|
||||
|
@ -230,7 +254,7 @@ $(document).on('pageinit', '#encodingSettingsPage', function () {
|
|||
page.querySelector('.fldEnhancedNvdec').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value) {
|
||||
if (this.value !== 'none') {
|
||||
page.querySelector('.hardwareAccelerationOptions').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.hardwareAccelerationOptions').classList.add('hide');
|
||||
|
@ -239,12 +263,12 @@ $(document).on('pageinit', '#encodingSettingsPage', function () {
|
|||
setDecodingCodecsVisible(page, this.value);
|
||||
});
|
||||
$('#btnSelectTranscodingTempPath', page).on('click.selectDirectory', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
$('#txtTranscodingTempPath', page).val(path);
|
||||
page.querySelector('#txtTranscodingTempPath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
|
@ -256,7 +280,7 @@ $(document).on('pageinit', '#encodingSettingsPage', function () {
|
|||
});
|
||||
});
|
||||
$('#btnSelectFallbackFontPath', page).on('click.selectDirectory', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
includeDirectories: true,
|
|
@ -1,11 +1,10 @@
|
|||
<div id="dashboardGeneralPage" data-role="page" class="page type-interior dashboardHomePage">
|
||||
<div id="dashboardGeneralPage" data-role="page" class="page type-interior dashboardHomePage" data-title="${General}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="dashboardGeneralForm">
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${Settings}</h2>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/settings">${Help}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -62,24 +61,6 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div class="verticalSection">
|
||||
<h2>${HeaderBranding}</h2>
|
||||
<div class="inputContainer">
|
||||
<textarea is="emby-textarea" id="txtLoginDisclaimer" label="${LabelLoginDisclaimer}" class="textarea-mono"></textarea>
|
||||
<div class="fieldDescription">${LabelLoginDisclaimerHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer customCssContainer">
|
||||
<textarea is="emby-textarea" id="txtCustomCss" label="${LabelCustomCss}" class="textarea-mono"></textarea>
|
||||
<div class="fieldDescription">${LabelCustomCssHelp}</div>
|
||||
</div>
|
||||
<div class="checkboxList paperList" style="padding:.5em 1em;">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkSplashScreenAvailable" />
|
||||
<span>${EnableSplashScreen}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="verticalSection">
|
||||
<h2>${HeaderPerformance}</h2>
|
||||
<div class="inputContainer">
|
|
@ -1,24 +1,26 @@
|
|||
import 'jquery';
|
||||
|
||||
import loading from '../../components/loading/loading';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../elements/emby-textarea/emby-textarea';
|
||||
import '../../elements/emby-input/emby-input';
|
||||
import '../../elements/emby-select/emby-select';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import alert from '../../components/alert';
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-textarea/emby-textarea';
|
||||
import 'elements/emby-input/emby-input';
|
||||
import 'elements/emby-select/emby-select';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import alert from 'components/alert';
|
||||
|
||||
function loadPage(page, config, languageOptions, systemInfo) {
|
||||
page.querySelector('#txtServerName').value = systemInfo.ServerName;
|
||||
page.querySelector('#txtCachePath').value = systemInfo.CachePath || '';
|
||||
page.querySelector('#chkQuickConnectAvailable').checked = config.QuickConnectAvailable === true;
|
||||
$('#txtMetadataPath', page).val(systemInfo.InternalMetadataPath || '');
|
||||
$('#txtMetadataNetworkPath', page).val(systemInfo.MetadataNetworkPath || '');
|
||||
$('#selectLocalizationLanguage', page).html(languageOptions.map(function (language) {
|
||||
page.querySelector('#txtMetadataPath').value = systemInfo.InternalMetadataPath || '';
|
||||
page.querySelector('#txtMetadataNetworkPath').value = systemInfo.MetadataNetworkPath || '';
|
||||
const localizationLanguageElem = page.querySelector('#selectLocalizationLanguage');
|
||||
localizationLanguageElem.innerHTML = languageOptions.map(function (language) {
|
||||
return '<option value="' + language.Value + '">' + language.Name + '</option>';
|
||||
})).val(config.UICulture);
|
||||
}).join('');
|
||||
localizationLanguageElem.value = config.UICulture;
|
||||
page.querySelector('#txtLibraryScanFanoutConcurrency').value = config.LibraryScanFanoutConcurrency || '';
|
||||
page.querySelector('#txtParallelImageEncodingLimit').value = config.ParallelImageEncodingLimit || '';
|
||||
|
||||
|
@ -28,39 +30,30 @@ function loadPage(page, config, languageOptions, systemInfo) {
|
|||
function onSubmit() {
|
||||
loading.show();
|
||||
const form = this;
|
||||
$(form).parents('.page');
|
||||
ApiClient.getServerConfiguration().then(function (config) {
|
||||
config.ServerName = $('#txtServerName', form).val();
|
||||
config.UICulture = $('#selectLocalizationLanguage', form).val();
|
||||
config.ServerName = form.querySelector('#txtServerName').value;
|
||||
config.UICulture = form.querySelector('#selectLocalizationLanguage').value;
|
||||
config.CachePath = form.querySelector('#txtCachePath').value;
|
||||
config.MetadataPath = $('#txtMetadataPath', form).val();
|
||||
config.MetadataNetworkPath = $('#txtMetadataNetworkPath', form).val();
|
||||
config.MetadataPath = form.querySelector('#txtMetadataPath').value;
|
||||
config.MetadataNetworkPath = form.querySelector('#txtMetadataNetworkPath').value;
|
||||
config.QuickConnectAvailable = form.querySelector('#chkQuickConnectAvailable').checked;
|
||||
config.LibraryScanFanoutConcurrency = parseInt(form.querySelector('#txtLibraryScanFanoutConcurrency').value || '0', 10);
|
||||
config.ParallelImageEncodingLimit = parseInt(form.querySelector('#txtParallelImageEncodingLimit').value || '0', 10);
|
||||
|
||||
ApiClient.updateServerConfiguration(config).then(function() {
|
||||
ApiClient.getNamedConfiguration(brandingConfigKey).then(function(brandingConfig) {
|
||||
brandingConfig.LoginDisclaimer = form.querySelector('#txtLoginDisclaimer').value;
|
||||
brandingConfig.CustomCss = form.querySelector('#txtCustomCss').value;
|
||||
brandingConfig.SplashscreenEnabled = form.querySelector('#chkSplashScreenAvailable').checked;
|
||||
|
||||
ApiClient.updateNamedConfiguration(brandingConfigKey, brandingConfig).then(function () {
|
||||
return ApiClient.updateServerConfiguration(config)
|
||||
.then(() => {
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
});
|
||||
});
|
||||
}, function () {
|
||||
}).catch(() => {
|
||||
loading.hide();
|
||||
alert(globalize.translate('ErrorDefault'));
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
});
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const brandingConfigKey = 'branding';
|
||||
export default function (view) {
|
||||
$('#btnSelectCachePath', view).on('click.selectDirectory', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
callback: function (path) {
|
||||
|
@ -77,18 +70,18 @@ export default function (view) {
|
|||
});
|
||||
});
|
||||
$('#btnSelectMetadataPath', view).on('click.selectDirectory', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
path: $('#txtMetadataPath', view).val(),
|
||||
networkSharePath: $('#txtMetadataNetworkPath', view).val(),
|
||||
path: view.querySelector('#txtMetadataPath').value,
|
||||
networkSharePath: view.querySelector('#txtMetadataNetworkPath').value,
|
||||
callback: function (path, networkPath) {
|
||||
if (path) {
|
||||
$('#txtMetadataPath', view).val(path);
|
||||
view.querySelector('#txtMetadataPath').value = path;
|
||||
}
|
||||
|
||||
if (networkPath) {
|
||||
$('#txtMetadataNetworkPath', view).val(networkPath);
|
||||
view.querySelector('#txtMetadataNetworkPath').value = networkPath;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
|
@ -107,11 +100,6 @@ export default function (view) {
|
|||
Promise.all([promiseConfig, promiseLanguageOptions, promiseSystemInfo]).then(function (responses) {
|
||||
loadPage(view, responses[0], responses[1], responses[2]);
|
||||
});
|
||||
ApiClient.getNamedConfiguration(brandingConfigKey).then(function (config) {
|
||||
view.querySelector('#txtLoginDisclaimer').value = config.LoginDisclaimer || '';
|
||||
view.querySelector('#txtCustomCss').value = config.CustomCss || '';
|
||||
view.querySelector('#chkSplashScreenAvailable').checked = config.SplashscreenEnabled === true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<div id="mediaLibraryPage" data-role="page" class="page type-interior mediaLibraryPage librarySectionPage fullWidthContent">
|
||||
<div id="mediaLibraryPage" data-role="page" class="page type-interior mediaLibraryPage librarySectionPage fullWidthContent" data-title="${HeaderLibraries}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="padded-top padded-bottom">
|
||||
|
@ -6,7 +6,6 @@
|
|||
<span>${ButtonScanAllLibraries}</span>
|
||||
</button>
|
||||
<progress max="100" min="0" style="display: inline-block; vertical-align: middle;" class="refreshProgress"></progress>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt" target="_blank" href="https://jellyfin.org/docs/general/server/libraries">${Help}</a>
|
||||
</div>
|
||||
|
||||
<div id="divVirtualFolders"></div>
|
|
@ -1,18 +1,18 @@
|
|||
import escapeHtml from 'escape-html';
|
||||
import 'jquery';
|
||||
import taskButton from '../../scripts/taskbutton';
|
||||
import loading from '../../components/loading/loading';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import dom from '../../scripts/dom';
|
||||
import imageHelper from '../../utils/image';
|
||||
import '../../components/cardbuilder/card.scss';
|
||||
import '../../elements/emby-itemrefreshindicator/emby-itemrefreshindicator';
|
||||
import Dashboard, { pageClassOn, pageIdOn } from '../../utils/dashboard';
|
||||
import confirm from '../../components/confirm/confirm';
|
||||
import { getDefaultBackgroundClass } from '../../components/cardbuilder/cardBuilderUtils';
|
||||
|
||||
import taskButton from 'scripts/taskbutton';
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import dom from 'scripts/dom';
|
||||
import imageHelper from 'utils/image';
|
||||
import 'components/cardbuilder/card.scss';
|
||||
import 'elements/emby-itemrefreshindicator/emby-itemrefreshindicator';
|
||||
import Dashboard, { pageClassOn, pageIdOn } from 'utils/dashboard';
|
||||
import confirm from 'components/confirm/confirm';
|
||||
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
|
||||
|
||||
function addVirtualFolder(page) {
|
||||
import('../../components/mediaLibraryCreator/mediaLibraryCreator').then(({ default: MediaLibraryCreator }) => {
|
||||
import('components/mediaLibraryCreator/mediaLibraryCreator').then(({ default: MediaLibraryCreator }) => {
|
||||
new MediaLibraryCreator({
|
||||
collectionTypeOptions: getCollectionTypeOptions().filter(function (f) {
|
||||
return !f.hidden;
|
||||
|
@ -27,7 +27,7 @@ function addVirtualFolder(page) {
|
|||
}
|
||||
|
||||
function editVirtualFolder(page, virtualFolder) {
|
||||
import('../../components/mediaLibraryEditor/mediaLibraryEditor').then(({ default: MediaLibraryEditor }) => {
|
||||
import('components/mediaLibraryEditor/mediaLibraryEditor').then(({ default: MediaLibraryEditor }) => {
|
||||
new MediaLibraryEditor({
|
||||
refresh: shouldRefreshLibraryAfterChanges(page),
|
||||
library: virtualFolder
|
||||
|
@ -61,7 +61,7 @@ function deleteVirtualFolder(page, virtualFolder) {
|
|||
}
|
||||
|
||||
function refreshVirtualFolder(page, virtualFolder) {
|
||||
import('../../components/refreshdialog/refreshdialog').then(({ default: RefreshDialog }) => {
|
||||
import('components/refreshdialog/refreshdialog').then(({ default: RefreshDialog }) => {
|
||||
new RefreshDialog({
|
||||
itemIds: [virtualFolder.ItemId],
|
||||
serverId: ApiClient.serverId(),
|
||||
|
@ -71,7 +71,7 @@ function refreshVirtualFolder(page, virtualFolder) {
|
|||
}
|
||||
|
||||
function renameVirtualFolder(page, virtualFolder) {
|
||||
import('../../components/prompt/prompt').then(({ default: prompt }) => {
|
||||
import('components/prompt/prompt').then(({ default: prompt }) => {
|
||||
prompt({
|
||||
label: globalize.translate('LabelNewName'),
|
||||
description: globalize.translate('MessageRenameMediaFolder'),
|
||||
|
@ -118,7 +118,7 @@ function showCardMenu(page, elem, virtualFolders) {
|
|||
icon: 'delete'
|
||||
});
|
||||
|
||||
import('../../components/actionSheet/actionSheet').then((actionsheet) => {
|
||||
import('components/actionSheet/actionSheet').then((actionsheet) => {
|
||||
actionsheet.show({
|
||||
items: menuItems,
|
||||
positionTo: elem,
|
||||
|
@ -181,14 +181,20 @@ function reloadVirtualFolders(page, virtualFolders) {
|
|||
divVirtualFolders.innerHTML = html;
|
||||
divVirtualFolders.classList.add('itemsContainer');
|
||||
divVirtualFolders.classList.add('vertical-wrap');
|
||||
$('.btnCardMenu', divVirtualFolders).on('click', function () {
|
||||
showCardMenu(page, this, virtualFolders);
|
||||
const btnCardMenuElements = divVirtualFolders.querySelectorAll('.btnCardMenu');
|
||||
btnCardMenuElements.forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
showCardMenu(page, btn, virtualFolders);
|
||||
});
|
||||
});
|
||||
divVirtualFolders.querySelector('#addLibrary').addEventListener('click', function () {
|
||||
addVirtualFolder(page);
|
||||
});
|
||||
$('.editLibrary', divVirtualFolders).on('click', function () {
|
||||
const card = $(this).parents('.card')[0];
|
||||
|
||||
const libraryEditElements = divVirtualFolders.querySelectorAll('.editLibrary');
|
||||
libraryEditElements.forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
const card = dom.parentWithClass(btn, 'card');
|
||||
const index = parseInt(card.getAttribute('data-index'), 10);
|
||||
const virtualFolder = virtualFolders[index];
|
||||
|
||||
|
@ -196,11 +202,12 @@ function reloadVirtualFolders(page, virtualFolders) {
|
|||
editVirtualFolder(page, virtualFolder);
|
||||
}
|
||||
});
|
||||
});
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function editImages(page, virtualFolder) {
|
||||
import('../../components/imageeditor/imageeditor').then((imageEditor) => {
|
||||
import('components/imageeditor/imageeditor').then((imageEditor) => {
|
||||
imageEditor.show({
|
||||
itemId: virtualFolder.ItemId,
|
||||
serverId: ApiClient.serverId()
|
|
@ -1,4 +1,4 @@
|
|||
<div id="libraryDisplayPage" data-role="page" class="page type-interior librarySectionPage">
|
||||
<div id="libraryDisplayPage" data-role="page" class="page type-interior librarySectionPage" data-title="${Display}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form>
|
|
@ -1,7 +1,7 @@
|
|||
import loading from '../../components/loading/loading';
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import loading from 'components/loading/loading';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
|
||||
export default function(view) {
|
||||
function loadData() {
|
||||
|
@ -29,7 +29,7 @@ export default function(view) {
|
|||
ApiClient.updateServerConfiguration(config).then(Dashboard.processServerConfigurationUpdateResult);
|
||||
});
|
||||
ApiClient.getNamedConfiguration('metadata').then(function(config) {
|
||||
config.UseFileCreationTimeForDateAdded = $('#selectDateAdded', form).val() === '1';
|
||||
config.UseFileCreationTimeForDateAdded = form.querySelector('#selectDateAdded').value === '1';
|
||||
ApiClient.updateNamedConfiguration('metadata', config);
|
||||
});
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import loading from '../components/loading/loading';
|
||||
import globalize from '../scripts/globalize';
|
||||
import Dashboard, { pageIdOn } from '../utils/dashboard';
|
||||
import { getParameterByName } from '../utils/url.ts';
|
||||
import Events from '../utils/events.ts';
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import Dashboard, { pageIdOn } from 'utils/dashboard';
|
||||
import { getParameterByName } from 'utils/url';
|
||||
import Events from 'utils/events';
|
||||
|
||||
function onListingsSubmitted() {
|
||||
Dashboard.navigate('dashboard/livetv');
|
||||
}
|
||||
|
||||
function init(page, type, providerId) {
|
||||
import(`../components/tvproviders/${type}`).then(({ default: ProviderFactory }) => {
|
||||
import(`components/tvproviders/${type}`).then(({ default: ProviderFactory }) => {
|
||||
const instance = new ProviderFactory(page, providerId, {});
|
||||
Events.on(instance, 'submitted', onListingsSubmitted);
|
||||
instance.init();
|
||||
|
@ -17,7 +17,7 @@ function init(page, type, providerId) {
|
|||
}
|
||||
|
||||
function loadTemplate(page, type, providerId) {
|
||||
import(`../components/tvproviders/${type}.template.html`).then(({ default: html }) => {
|
||||
import(`components/tvproviders/${type}.template.html`).then(({ default: html }) => {
|
||||
page.querySelector('.providerTemplate').innerHTML = globalize.translateHtml(html);
|
||||
init(page, type, providerId);
|
||||
});
|
|
@ -1,10 +1,9 @@
|
|||
<div id="liveTvSettingsPage" data-role="page" class="page type-interior liveTvPage">
|
||||
<div id="liveTvSettingsPage" data-role="page" class="page type-interior liveTvPage" data-title="${HeaderDVR}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${HeaderDVR}</h2>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/live-tv/">${Help}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
import 'jquery';
|
||||
import loading from '../components/loading/loading';
|
||||
import globalize from '../scripts/globalize';
|
||||
import '../elements/emby-button/emby-button';
|
||||
import Dashboard from '../utils/dashboard';
|
||||
import alert from '../components/alert';
|
||||
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import alert from 'components/alert';
|
||||
|
||||
function loadPage(page, config) {
|
||||
$('.liveTvSettingsForm', page).show();
|
||||
$('.noLiveTvServices', page).hide();
|
||||
$('#selectGuideDays', page).val(config.GuideDays || '');
|
||||
$('#txtPrePaddingMinutes', page).val(config.PrePaddingSeconds / 60);
|
||||
$('#txtPostPaddingMinutes', page).val(config.PostPaddingSeconds / 60);
|
||||
page.querySelector('.liveTvSettingsForm').classList.remove('hide');
|
||||
page.querySelector('.noLiveTvServices')?.classList.add('hide');
|
||||
page.querySelector('#selectGuideDays').value = config.GuideDays || '';
|
||||
page.querySelector('#txtPrePaddingMinutes').value = config.PrePaddingSeconds / 60;
|
||||
page.querySelector('#txtPostPaddingMinutes').value = config.PostPaddingSeconds / 60;
|
||||
page.querySelector('#txtRecordingPath').value = config.RecordingPath || '';
|
||||
page.querySelector('#txtMovieRecordingPath').value = config.MovieRecordingPath || '';
|
||||
page.querySelector('#txtSeriesRecordingPath').value = config.SeriesRecordingPath || '';
|
||||
|
@ -25,7 +26,7 @@ function onSubmit() {
|
|||
loading.show();
|
||||
const form = this;
|
||||
ApiClient.getNamedConfiguration('livetv').then(function (config) {
|
||||
config.GuideDays = $('#selectGuideDays', form).val() || null;
|
||||
config.GuideDays = form.querySelector('#selectGuideDays').value || null;
|
||||
const recordingPath = form.querySelector('#txtRecordingPath').value || null;
|
||||
const movieRecordingPath = form.querySelector('#txtMovieRecordingPath').value || null;
|
||||
const seriesRecordingPath = form.querySelector('#txtSeriesRecordingPath').value || null;
|
||||
|
@ -34,10 +35,10 @@ function onSubmit() {
|
|||
config.MovieRecordingPath = movieRecordingPath;
|
||||
config.SeriesRecordingPath = seriesRecordingPath;
|
||||
config.RecordingEncodingFormat = 'mkv';
|
||||
config.PrePaddingSeconds = 60 * $('#txtPrePaddingMinutes', form).val();
|
||||
config.PostPaddingSeconds = 60 * $('#txtPostPaddingMinutes', form).val();
|
||||
config.RecordingPostProcessor = $('#txtPostProcessor', form).val();
|
||||
config.RecordingPostProcessorArguments = $('#txtPostProcessorArguments', form).val();
|
||||
config.PrePaddingSeconds = 60 * form.querySelector('#txtPrePaddingMinutes').value;
|
||||
config.PostPaddingSeconds = 60 * form.querySelector('#txtPostPaddingMinutes').value;
|
||||
config.RecordingPostProcessor = form.querySelector('#txtPostProcessor').value;
|
||||
config.RecordingPostProcessorArguments = form.querySelector('#txtPostProcessorArguments').value;
|
||||
config.SaveRecordingNFO = form.querySelector('#chkSaveRecordingNFO').checked;
|
||||
config.SaveRecordingImages = form.querySelector('#chkSaveRecordingImages').checked;
|
||||
ApiClient.updateNamedConfiguration('livetv', config).then(function () {
|
||||
|
@ -64,12 +65,12 @@ $(document).on('pageinit', '#liveTvSettingsPage', function () {
|
|||
const page = this;
|
||||
$('.liveTvSettingsForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
$('#btnSelectRecordingPath', page).on('click.selectDirectory', function () {
|
||||
import('../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
$('#txtRecordingPath', page).val(path);
|
||||
page.querySelector('#txtRecordingPath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
|
@ -79,12 +80,12 @@ $(document).on('pageinit', '#liveTvSettingsPage', function () {
|
|||
});
|
||||
});
|
||||
$('#btnSelectMovieRecordingPath', page).on('click.selectDirectory', function () {
|
||||
import('../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
$('#txtMovieRecordingPath', page).val(path);
|
||||
page.querySelector('#txtMovieRecordingPath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
|
@ -94,12 +95,12 @@ $(document).on('pageinit', '#liveTvSettingsPage', function () {
|
|||
});
|
||||
});
|
||||
$('#btnSelectSeriesRecordingPath', page).on('click.selectDirectory', function () {
|
||||
import('../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
$('#txtSeriesRecordingPath', page).val(path);
|
||||
page.querySelector('#txtSeriesRecordingPath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
|
@ -109,13 +110,13 @@ $(document).on('pageinit', '#liveTvSettingsPage', function () {
|
|||
});
|
||||
});
|
||||
$('#btnSelectPostProcessorPath', page).on('click.selectDirectory', function () {
|
||||
import('../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
includeFiles: true,
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
$('#txtPostProcessor', page).val(path);
|
||||
page.querySelector('#txtPostProcessor').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
|
@ -1,4 +1,4 @@
|
|||
<div id="liveTvStatusPage" data-role="page" class="page type-interior liveTvSettingsPage">
|
||||
<div id="liveTvStatusPage" data-role="page" class="page type-interior liveTvSettingsPage" data-title="${LiveTV}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
|
@ -10,7 +10,6 @@
|
|||
<button is="emby-button" type="button" class="fab btnAddDevice submit sectionTitleButton" style="margin-left:1em;" title="${Add}">
|
||||
<span class="material-icons add" aria-hidden="true"></span>
|
||||
</button>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" style="margin-left:2em!important;" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/live-tv/">${Help}</a>
|
||||
</div>
|
||||
<div class="devicesList itemsContainer vertical-wrap" data-hovermenu="false" data-multiselect="false" style="margin-top: .5em;"></div>
|
||||
</div>
|
|
@ -1,19 +1,20 @@
|
|||
import 'jquery';
|
||||
import globalize from '../scripts/globalize';
|
||||
import taskButton from '../scripts/taskbutton';
|
||||
import dom from '../scripts/dom';
|
||||
import layoutManager from '../components/layoutManager';
|
||||
import loading from '../components/loading/loading';
|
||||
import browser from '../scripts/browser';
|
||||
import '../components/listview/listview.scss';
|
||||
import '../styles/flexstyles.scss';
|
||||
import '../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import '../components/cardbuilder/card.scss';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import taskButton from 'scripts/taskbutton';
|
||||
import dom from 'scripts/dom';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import loading from 'components/loading/loading';
|
||||
import browser from 'scripts/browser';
|
||||
import 'components/listview/listview.scss';
|
||||
import 'styles/flexstyles.scss';
|
||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import 'components/cardbuilder/card.scss';
|
||||
import 'material-design-icons-iconfont';
|
||||
import '../elements/emby-button/emby-button';
|
||||
import Dashboard from '../utils/dashboard';
|
||||
import confirm from '../components/confirm/confirm';
|
||||
import { getDefaultBackgroundClass } from '../components/cardbuilder/cardBuilderUtils';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import confirm from 'components/confirm/confirm';
|
||||
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
|
||||
|
||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
|
||||
|
@ -89,8 +90,8 @@ function submitAddDeviceForm(page) {
|
|||
type: 'POST',
|
||||
url: ApiClient.getUrl('LiveTv/TunerHosts'),
|
||||
data: JSON.stringify({
|
||||
Type: $('#selectTunerDeviceType', page).val(),
|
||||
Url: $('#txtDevicePath', page).val()
|
||||
Type: page.querySelector('#selectTunerDeviceType').value,
|
||||
Url: page.querySelector('#txtDevicePath').value
|
||||
}),
|
||||
contentType: 'application/json'
|
||||
}).then(function () {
|
||||
|
@ -129,11 +130,17 @@ function renderProviders(page, providers) {
|
|||
html += '</div>';
|
||||
}
|
||||
|
||||
const elem = $('.providerList', page).html(html);
|
||||
$('.btnOptions', elem).on('click', function () {
|
||||
const elem = page.querySelector('.providerList');
|
||||
elem.innerHTML = html;
|
||||
if (elem.querySelector('.btnOptions')) {
|
||||
const btnOptionElements = elem.querySelectorAll('.btnOptions');
|
||||
btnOptionElements.forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
const id = this.getAttribute('data-id');
|
||||
showProviderOptions(page, id, this);
|
||||
showProviderOptions(page, id, btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showProviderOptions(page, providerId, button) {
|
||||
|
@ -147,7 +154,7 @@ function showProviderOptions(page, providerId, button) {
|
|||
id: 'map'
|
||||
});
|
||||
|
||||
import('../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
import('components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
actionsheet.show({
|
||||
items: items,
|
||||
positionTo: button
|
||||
|
@ -165,7 +172,7 @@ function showProviderOptions(page, providerId, button) {
|
|||
}
|
||||
|
||||
function mapChannels(page, providerId) {
|
||||
import('../components/channelMapper/channelMapper').then(({ default: ChannelMapper }) => {
|
||||
import('components/channelMapper/channelMapper').then(({ default: ChannelMapper }) => {
|
||||
new ChannelMapper({
|
||||
serverId: ApiClient.serverInfo().Id,
|
||||
providerId: providerId
|
||||
|
@ -237,7 +244,7 @@ function addProvider(button) {
|
|||
id: 'xmltv'
|
||||
});
|
||||
|
||||
import('../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
import('components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
actionsheet.show({
|
||||
items: menuItems,
|
||||
positionTo: button,
|
||||
|
@ -263,7 +270,7 @@ function showDeviceMenu(button, tunerDeviceId) {
|
|||
id: 'edit'
|
||||
});
|
||||
|
||||
import('../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
import('components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
actionsheet.show({
|
||||
items: items,
|
||||
positionTo: button
|
||||
|
@ -297,14 +304,17 @@ function onDevicesListClick(e) {
|
|||
|
||||
$(document).on('pageinit', '#liveTvStatusPage', function () {
|
||||
const page = this;
|
||||
$('.btnAddDevice', page).on('click', function () {
|
||||
page.querySelector('.btnAddDevice').addEventListener('click', function () {
|
||||
addDevice();
|
||||
});
|
||||
$('.formAddDevice', page).on('submit', function () {
|
||||
if (page.querySelector('.formAddDevice')) {
|
||||
// NOTE: unused?
|
||||
page.querySelector('.formAddDevice').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
submitAddDeviceForm(page);
|
||||
return false;
|
||||
});
|
||||
$('.btnAddProvider', page).on('click', function () {
|
||||
}
|
||||
page.querySelector('.btnAddProvider').addEventListener('click', function () {
|
||||
addProvider(this);
|
||||
});
|
||||
page.querySelector('.devicesList').addEventListener('click', onDevicesListClick);
|
|
@ -5,7 +5,6 @@
|
|||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h1 class="sectionTitle">${HeaderLiveTvTunerSetup}</h1>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/live-tv/">${Help}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -38,6 +37,11 @@
|
|||
<div class="fieldDescription">${SimultaneousConnectionLimitHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer fldFallbackMaxStreamingBitrate hide">
|
||||
<input is="emby-input" type="number" pattern="[0-9]*" required="required" min="1" step="1" class="txtFallbackMaxStreamingBitrate" label="${LabelFallbackMaxStreamingBitrate}" autocomplete="off" value="30" />
|
||||
<div class="fieldDescription">${FallbackMaxStreamingBitrateHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldFavorites hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkFavorite" />
|
||||
|
@ -54,6 +58,22 @@
|
|||
<div class="fieldDescription checkboxFieldDescription">${AllowHWTranscodingHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldFmp4Container hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkFmp4Container" />
|
||||
<span>${LabelAllowFmp4TranscodingContainer}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowFmp4TranscodingContainerHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldStreamSharing hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkStreamSharing" checked />
|
||||
<span>${LabelAllowStreamSharing}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowStreamSharingHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldStreamLoop hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkStreamLoop" />
|
|
@ -1,12 +1,12 @@
|
|||
import globalize from '../scripts/globalize';
|
||||
import loading from '../components/loading/loading';
|
||||
import dom from '../scripts/dom';
|
||||
import '../elements/emby-input/emby-input';
|
||||
import '../elements/emby-button/emby-button';
|
||||
import '../elements/emby-checkbox/emby-checkbox';
|
||||
import '../elements/emby-select/emby-select';
|
||||
import Dashboard from '../utils/dashboard';
|
||||
import { getParameterByName } from '../utils/url.ts';
|
||||
import globalize from 'lib/globalize';
|
||||
import loading from 'components/loading/loading';
|
||||
import dom from 'scripts/dom';
|
||||
import 'elements/emby-input/emby-input';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-select/emby-select';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import { getParameterByName } from 'utils/url';
|
||||
|
||||
function isM3uVariant(type) {
|
||||
return ['nextpvr'].indexOf(type || '') !== -1;
|
||||
|
@ -61,7 +61,10 @@ function fillTunerHostInfo(view, info) {
|
|||
view.querySelector('.chkFavorite').checked = info.ImportFavoritesOnly;
|
||||
view.querySelector('.chkTranscode').checked = info.AllowHWTranscoding;
|
||||
view.querySelector('.chkStreamLoop').checked = info.EnableStreamLooping;
|
||||
view.querySelector('.chkFmp4Container').checked = info.AllowFmp4TranscodingContainer;
|
||||
view.querySelector('.chkStreamSharing').checked = info.AllowStreamSharing;
|
||||
view.querySelector('.chkIgnoreDts').checked = info.IgnoreDts;
|
||||
view.querySelector('.txtFallbackMaxStreamingBitrate').value = info.FallbackMaxStreamingBitrate / 1e6 || '30';
|
||||
view.querySelector('.txtTunerCount').value = info.TunerCount || '0';
|
||||
}
|
||||
|
||||
|
@ -74,8 +77,11 @@ function submitForm(page) {
|
|||
FriendlyName: page.querySelector('.txtFriendlyName').value || null,
|
||||
DeviceId: page.querySelector('.fldDeviceId').value || null,
|
||||
TunerCount: page.querySelector('.txtTunerCount').value || 0,
|
||||
FallbackMaxStreamingBitrate: parseInt(1e6 * parseFloat(page.querySelector('.txtFallbackMaxStreamingBitrate').value || '30'), 10),
|
||||
ImportFavoritesOnly: page.querySelector('.chkFavorite').checked,
|
||||
AllowHWTranscoding: page.querySelector('.chkTranscode').checked,
|
||||
AllowFmp4TranscodingContainer: page.querySelector('.chkFmp4Container').checked,
|
||||
AllowStreamSharing: page.querySelector('.chkStreamSharing').checked,
|
||||
EnableStreamLooping: page.querySelector('.chkStreamLoop').checked,
|
||||
IgnoreDts: page.querySelector('.chkIgnoreDts').checked
|
||||
};
|
||||
|
@ -106,7 +112,7 @@ function submitForm(page) {
|
|||
}
|
||||
|
||||
function getDetectedDevice() {
|
||||
return import('../components/tunerPicker').then(({ default: TunerPicker }) => {
|
||||
return import('components/tunerPicker').then(({ default: TunerPicker }) => {
|
||||
return new TunerPicker().show({
|
||||
serverId: ApiClient.serverId()
|
||||
});
|
||||
|
@ -125,6 +131,9 @@ function onTypeChange() {
|
|||
const supportsIgnoreDts = value === 'm3u';
|
||||
const supportsTunerCount = value === 'm3u';
|
||||
const supportsUserAgent = value === 'm3u';
|
||||
const supportsFmp4Container = value === 'm3u';
|
||||
const supportsStreamSharing = value === 'm3u';
|
||||
const supportsFallbackBitrate = value === 'm3u' || value === 'hdhomerun';
|
||||
const suppportsSubmit = value !== 'other';
|
||||
const supportsSelectablePath = supportsTunerFileOrUrl;
|
||||
const txtDevicePath = view.querySelector('.txtDevicePath');
|
||||
|
@ -165,6 +174,10 @@ function onTypeChange() {
|
|||
view.querySelector('.fldTranscode').classList.add('hide');
|
||||
}
|
||||
|
||||
view.querySelector('.fldFmp4Container').classList.toggle('hide', !supportsFmp4Container);
|
||||
view.querySelector('.fldStreamSharing').classList.toggle('hide', !supportsStreamSharing);
|
||||
view.querySelector('.fldFallbackMaxStreamingBitrate').classList.toggle('hide', !supportsFallbackBitrate);
|
||||
|
||||
if (supportsStreamLooping) {
|
||||
view.querySelector('.fldStreamLoop').classList.remove('hide');
|
||||
} else {
|
||||
|
@ -222,7 +235,7 @@ export default function (view, params) {
|
|||
});
|
||||
});
|
||||
view.querySelector('.btnSelectPath').addEventListener('click', function () {
|
||||
import('../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
includeFiles: true,
|
|
@ -2,11 +2,11 @@ import { ImageResolution } from '@jellyfin/sdk/lib/generated-client/models/image
|
|||
|
||||
import 'jquery';
|
||||
|
||||
import loading from '../../components/loading/loading';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
|
||||
import '../../components/listview/listview.scss';
|
||||
import 'components/listview/listview.scss';
|
||||
|
||||
function populateImageResolutionOptions(select) {
|
||||
let html = '';
|
|
@ -1,4 +1,4 @@
|
|||
<div id="metadataImagesConfigurationPage" data-role="page" class="page type-interior metadataConfigurationPage">
|
||||
<div id="metadataImagesConfigurationPage" data-role="page" class="page type-interior metadataConfigurationPage" data-title="${LabelMetadata}">
|
||||
|
||||
<div>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<div id="metadataNfoPage" data-role="page" class="page type-interior metadataConfigurationPage">
|
||||
<div id="metadataNfoPage" data-role="page" class="page type-interior metadataConfigurationPage" data-title="${TabNfoSettings}">
|
||||
|
||||
<div>
|
||||
|
|
@ -1,17 +1,20 @@
|
|||
import escapeHtml from 'escape-html';
|
||||
import 'jquery';
|
||||
import loading from '../../components/loading/loading';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import alert from '../../components/alert';
|
||||
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import alert from 'components/alert';
|
||||
|
||||
function loadPage(page, config, users) {
|
||||
let html = '<option value="" selected="selected">' + globalize.translate('None') + '</option>';
|
||||
html += users.map(function (user) {
|
||||
return '<option value="' + user.Id + '">' + escapeHtml(user.Name) + '</option>';
|
||||
}).join('');
|
||||
$('#selectUser', page).html(html).val(config.UserId || '');
|
||||
$('#selectReleaseDateFormat', page).val(config.ReleaseDateFormat);
|
||||
const elem = page.querySelector('#selectUser');
|
||||
elem.innerHTML = html;
|
||||
elem.value = config.UserId || '';
|
||||
page.querySelector('#selectReleaseDateFormat').value = config.ReleaseDateFormat;
|
||||
page.querySelector('#chkSaveImagePaths').checked = config.SaveImagePathsInNfo;
|
||||
page.querySelector('#chkEnablePathSubstitution').checked = config.EnablePathSubstitution;
|
||||
page.querySelector('#chkEnableExtraThumbs').checked = config.EnableExtraThumbsDuplication;
|
||||
|
@ -22,8 +25,8 @@ function onSubmit() {
|
|||
loading.show();
|
||||
const form = this;
|
||||
ApiClient.getNamedConfiguration(metadataKey).then(function (config) {
|
||||
config.UserId = $('#selectUser', form).val() || null;
|
||||
config.ReleaseDateFormat = $('#selectReleaseDateFormat', form).val();
|
||||
config.UserId = form.querySelector('#selectUser').value || null;
|
||||
config.ReleaseDateFormat = form.querySelector('#selectReleaseDateFormat').value;
|
||||
config.SaveImagePathsInNfo = form.querySelector('#chkSaveImagePaths').checked;
|
||||
config.EnablePathSubstitution = form.querySelector('#chkEnablePathSubstitution').checked;
|
||||
config.EnableExtraThumbsDuplication = form.querySelector('#chkEnableExtraThumbs').checked;
|
|
@ -1,11 +1,10 @@
|
|||
<div id="networkingPage" data-role="page" class="page type-interior advancedConfigurationPage">
|
||||
<div id="networkingPage" data-role="page" class="page type-interior advancedConfigurationPage" data-title="${TabNetworking}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="dashboardHostingForm">
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${TabNetworking}</h2>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/networking/">${Help}</a>
|
||||
</div>
|
||||
|
||||
<fieldset class='verticalSection verticalSection-extrabottompadding'>
|
||||
|
@ -97,14 +96,6 @@
|
|||
<option value="blacklist">${Blacklist}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldEnableUpnp hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableUpnp" />
|
||||
<span>${LabelEnableAutomaticPortMap}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelEnableAutomaticPortMapHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer fldPublicHttpPort hide">
|
||||
<input is="emby-input" type="number" label="${LabelPublicHttpPort}" id="txtPublicHttpPort" pattern="[0-9]*" required="required" min="1" max="65535" />
|
||||
<div class="fieldDescription">${LabelPublicHttpPortHelp}</div>
|
|
@ -1,15 +1,14 @@
|
|||
import loading from '../../components/loading/loading';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../elements/emby-select/emby-select';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import alert from '../../components/alert';
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-select/emby-select';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import alert from 'components/alert';
|
||||
|
||||
function onSubmit(e) {
|
||||
const form = this;
|
||||
const localAddress = form.querySelector('#txtLocalAddress').value;
|
||||
const enableUpnp = form.querySelector('#chkEnableUpnp').checked;
|
||||
confirmSelections(localAddress, enableUpnp, function () {
|
||||
confirmSelections(localAddress, function () {
|
||||
const validationResult = getValidationAlert(form);
|
||||
|
||||
if (validationResult) {
|
||||
|
@ -54,7 +53,6 @@ function onSubmit(e) {
|
|||
config.InternalHttpsPort = form.querySelector('#txtHttpsPort').value;
|
||||
config.EnableHttps = form.querySelector('#chkEnableHttps').checked;
|
||||
config.RequireHttps = form.querySelector('#chkRequireHttps').checked;
|
||||
config.EnableUPnP = enableUpnp;
|
||||
config.BaseUrl = form.querySelector('#txtBaseUrl').value;
|
||||
config.EnableRemoteAccess = form.querySelector('#chkRemoteAccess').checked;
|
||||
config.CertificatePath = form.querySelector('#txtCertificatePath').value || null;
|
||||
|
@ -110,8 +108,8 @@ function showAlertText(options) {
|
|||
});
|
||||
}
|
||||
|
||||
function confirmSelections(localAddress, enableUpnp, callback) {
|
||||
if (localAddress || !enableUpnp) {
|
||||
function confirmSelections(localAddress, callback) {
|
||||
if (localAddress) {
|
||||
showAlertText({
|
||||
title: globalize.translate('TitleHostingSettings'),
|
||||
text: globalize.translate('SettingsWarning')
|
||||
|
@ -139,7 +137,6 @@ export default function (view) {
|
|||
const txtCertificatePath = page.querySelector('#txtCertificatePath');
|
||||
txtCertificatePath.value = config.CertificatePath || '';
|
||||
page.querySelector('#txtCertPassword').value = config.CertificatePassword || '';
|
||||
page.querySelector('#chkEnableUpnp').checked = config.EnableUPnP;
|
||||
triggerChange(page.querySelector('#chkRemoteAccess'));
|
||||
page.querySelector('#chkAutodiscovery').checked = config.AutoDiscovery;
|
||||
page.querySelector('#chkEnableIP6').checked = config.EnableIPv6;
|
||||
|
@ -154,17 +151,15 @@ export default function (view) {
|
|||
view.querySelector('.fldExternalAddressFilterMode').classList.remove('hide');
|
||||
view.querySelector('.fldPublicHttpPort').classList.remove('hide');
|
||||
view.querySelector('.fldPublicHttpsPort').classList.remove('hide');
|
||||
view.querySelector('.fldEnableUpnp').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldExternalAddressFilter').classList.add('hide');
|
||||
view.querySelector('.fldExternalAddressFilterMode').classList.add('hide');
|
||||
view.querySelector('.fldPublicHttpPort').classList.add('hide');
|
||||
view.querySelector('.fldPublicHttpsPort').classList.add('hide');
|
||||
view.querySelector('.fldEnableUpnp').classList.add('hide');
|
||||
}
|
||||
});
|
||||
view.querySelector('#btnSelectCertPath').addEventListener('click', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
includeFiles: true,
|
17
src/apps/dashboard/controllers/plugins/available/index.html
Normal file
17
src/apps/dashboard/controllers/plugins/available/index.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<div id="pluginCatalogPage" data-role="page" class="page type-interior pluginConfigurationPage fullWidthContent" data-title="${TabCatalog}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${TabCatalog}</h2>
|
||||
<a is="emby-linkbutton" class="fab" href="#/dashboard/plugins/repositories" style="margin-left:1em;" title="${Settings}">
|
||||
<span class="material-icons settings" aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input id="txtSearchPlugins" name="txtSearchPlugins" type="text" is="emby-input" label="${Search}" />
|
||||
</div>
|
||||
<div id="noPlugins" class="hide">${MessageNoAvailablePlugins}</div>
|
||||
<div id="pluginTiles" style="text-align:left;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,12 +1,14 @@
|
|||
import escapeHTML from 'escape-html';
|
||||
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import '../../../../components/cardbuilder/card.scss';
|
||||
import '../../../../elements/emby-button/emby-button';
|
||||
import '../../../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../../../elements/emby-select/emby-select';
|
||||
import { getDefaultBackgroundClass } from '../../../../components/cardbuilder/cardBuilderUtils';
|
||||
import { CATEGORY_LABELS } from 'apps/dashboard/features/plugins/constants/categoryLabels';
|
||||
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
import 'components/cardbuilder/card.scss';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-select/emby-select';
|
||||
|
||||
function reloadList(page) {
|
||||
loading.show();
|
||||
|
@ -23,19 +25,14 @@ function reloadList(page) {
|
|||
}
|
||||
|
||||
function getHeaderText(category) {
|
||||
category = category.replace(' ', '');
|
||||
// TODO: Replace with switch
|
||||
if (category === 'Channel') {
|
||||
category = 'Channels';
|
||||
} else if (category === 'Theme') {
|
||||
category = 'Themes';
|
||||
} else if (category === 'LiveTV') {
|
||||
category = 'LiveTV';
|
||||
} else if (category === 'ScreenSaver') {
|
||||
category = 'HeaderScreenSavers';
|
||||
const categoryKey = category.replaceAll(' ', '');
|
||||
|
||||
if (CATEGORY_LABELS[categoryKey]) {
|
||||
return globalize.translate(CATEGORY_LABELS[categoryKey]);
|
||||
}
|
||||
|
||||
return globalize.translate(category);
|
||||
console.warn('[AvailablePlugins] unmapped category label', category);
|
||||
return category;
|
||||
}
|
||||
|
||||
function populateList(options) {
|
||||
|
@ -43,7 +40,7 @@ function populateList(options) {
|
|||
const installedPlugins = options.installedPlugins;
|
||||
|
||||
availablePlugins.forEach(function (plugin, index, array) {
|
||||
plugin.category = plugin.category || 'General';
|
||||
plugin.category = plugin.category || 'Other';
|
||||
plugin.categoryDisplayName = getHeaderText(plugin.category);
|
||||
array[index] = plugin;
|
||||
});
|
||||
|
@ -119,7 +116,8 @@ function onSearchBarType(searchBar) {
|
|||
|
||||
function getPluginHtml(plugin, options, installedPlugins) {
|
||||
let html = '';
|
||||
let href = plugin.externalUrl ? plugin.externalUrl : '#/dashboard/plugins/add?name=' + encodeURIComponent(plugin.name) + '&guid=' + plugin.guid;
|
||||
let href = plugin.externalUrl ? plugin.externalUrl :
|
||||
`#/dashboard/plugins/${plugin.guid}?name=${encodeURIComponent(plugin.name)}`;
|
||||
|
||||
if (options.context) {
|
||||
href += '&context=' + options.context;
|
|
@ -1,6 +1,9 @@
|
|||
<div id="pluginsPage" data-role="page" class="page type-interior pluginConfigurationPage fullWidthContent">
|
||||
<div id="pluginsPage" data-role="page" class="page type-interior pluginConfigurationPage fullWidthContent" data-title="${TabPlugins}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${TabMyPlugins}</h2>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input id="txtSearchPlugins" name="txtSearchPlugins" type="text" is="emby-input" label="${Search}" />
|
||||
</div>
|
|
@ -1,11 +1,11 @@
|
|||
import loading from '../../../../components/loading/loading';
|
||||
import dom from '../../../../scripts/dom';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import '../../../../components/cardbuilder/card.scss';
|
||||
import '../../../../elements/emby-button/emby-button';
|
||||
import Dashboard, { pageIdOn } from '../../../../utils/dashboard';
|
||||
import confirm from '../../../../components/confirm/confirm';
|
||||
import { getDefaultBackgroundClass } from '../../../../components/cardbuilder/cardBuilderUtils';
|
||||
import loading from 'components/loading/loading';
|
||||
import dom from 'scripts/dom';
|
||||
import globalize from 'lib/globalize';
|
||||
import 'components/cardbuilder/card.scss';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import Dashboard, { pageIdOn } from 'utils/dashboard';
|
||||
import confirm from 'components/confirm/confirm';
|
||||
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
|
||||
|
||||
function deletePlugin(page, uniqueid, version, name) {
|
||||
const msg = globalize.translate('UninstallPluginConfirmation', name);
|
||||
|
@ -187,7 +187,7 @@ function showPluginMenu(page, elem) {
|
|||
});
|
||||
}
|
||||
|
||||
import('../../../../components/actionSheet/actionSheet').then((actionsheet) => {
|
||||
import('components/actionSheet/actionSheet').then((actionsheet) => {
|
||||
actionsheet.show({
|
||||
items: menuItems,
|
||||
positionTo: elem,
|
|
@ -1,4 +1,4 @@
|
|||
<div id="repositories" data-role="page" class="page type-interior fullWidthContent">
|
||||
<div id="repositories" data-role="page" class="page type-interior fullWidthContent" data-title="${TabRepositories}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
|
@ -6,9 +6,6 @@
|
|||
<button is="emby-button" type="button" class="fab btnNewRepository submit" style="margin-left:1em;" title="${Add}">
|
||||
<span class="material-icons add" aria-hidden="true"></span>
|
||||
</button>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/plugins/#repositories">
|
||||
${Help}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="repositories"></div>
|
|
@ -1,14 +1,14 @@
|
|||
import loading from '../../../../components/loading/loading';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import dialogHelper from '../../../../components/dialogHelper/dialogHelper';
|
||||
import confirm from '../../../../components/confirm/confirm';
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import dialogHelper from 'components/dialogHelper/dialogHelper';
|
||||
import confirm from 'components/confirm/confirm';
|
||||
|
||||
import '../../../../elements/emby-button/emby-button';
|
||||
import '../../../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../../../elements/emby-select/emby-select';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-select/emby-select';
|
||||
|
||||
import '../../../../components/formdialog.scss';
|
||||
import '../../../../components/listview/listview.scss';
|
||||
import 'components/formdialog.scss';
|
||||
import 'components/listview/listview.scss';
|
||||
|
||||
let repositories = [];
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle taskName"></h2>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/tasks">${Help}</a>
|
||||
</div>
|
||||
<p id="pTaskDescription"></p>
|
||||
</div>
|
|
@ -1,13 +1,12 @@
|
|||
import 'jquery';
|
||||
import loading from '../../../components/loading/loading';
|
||||
import datetime from '../../../scripts/datetime';
|
||||
import dom from '../../../scripts/dom';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import '../../../elements/emby-input/emby-input';
|
||||
import '../../../elements/emby-button/emby-button';
|
||||
import '../../../elements/emby-select/emby-select';
|
||||
import confirm from '../../../components/confirm/confirm';
|
||||
import { getParameterByName } from '../../../utils/url.ts';
|
||||
import loading from 'components/loading/loading';
|
||||
import datetime from 'scripts/datetime';
|
||||
import dom from 'scripts/dom';
|
||||
import globalize from 'lib/globalize';
|
||||
import 'elements/emby-input/emby-input';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-select/emby-select';
|
||||
import confirm from 'components/confirm/confirm';
|
||||
import { getParameterByName } from 'utils/url.ts';
|
||||
|
||||
function fillTimeOfDay(select) {
|
||||
const options = [];
|
||||
|
@ -33,10 +32,10 @@ const ScheduledTaskPage = {
|
|||
});
|
||||
},
|
||||
loadScheduledTask: function (view, task) {
|
||||
$('.taskName', view).html(task.Name);
|
||||
$('#pTaskDescription', view).html(task.Description);
|
||||
view.querySelector('.taskName').innerHTML = task.Name;
|
||||
view.querySelector('#pTaskDescription').innerHTML = task.Description;
|
||||
|
||||
import('../../../components/listview/listview.scss').then(() => {
|
||||
import('components/listview/listview.scss').then(() => {
|
||||
ScheduledTaskPage.loadTaskTriggers(view, task);
|
||||
});
|
||||
|
||||
|
@ -124,9 +123,9 @@ const ScheduledTaskPage = {
|
|||
return datetime.getDisplayTime(now);
|
||||
},
|
||||
showAddTriggerPopup: function (view) {
|
||||
$('#selectTriggerType', view).val('DailyTrigger');
|
||||
view.querySelector('#selectTriggerType').value = 'DailyTrigger';
|
||||
view.querySelector('#selectTriggerType').dispatchEvent(new CustomEvent('change', {}));
|
||||
$('#popupAddTrigger', view).removeClass('hide');
|
||||
view.querySelector('#popupAddTrigger').classList.remove('hide');
|
||||
},
|
||||
confirmDeleteTrigger: function (view, index) {
|
||||
confirm(globalize.translate('MessageDeleteTaskTrigger'), globalize.translate('HeaderDeleteTaskTrigger')).then(function () {
|
||||
|
@ -145,54 +144,54 @@ const ScheduledTaskPage = {
|
|||
},
|
||||
refreshTriggerFields: function (page, triggerType) {
|
||||
if (triggerType == 'DailyTrigger') {
|
||||
$('#fldTimeOfDay', page).show();
|
||||
$('#fldDayOfWeek', page).hide();
|
||||
$('#fldSelectSystemEvent', page).hide();
|
||||
$('#fldSelectInterval', page).hide();
|
||||
$('#selectTimeOfDay', page).attr('required', 'required');
|
||||
page.querySelector('#fldTimeOfDay').classList.remove('hide');
|
||||
page.querySelector('#fldDayOfWeek').classList.add('hide');
|
||||
page.querySelector('#fldSelectSystemEvent').classList.add('hide');
|
||||
page.querySelector('#fldSelectInterval').classList.add('hide');
|
||||
page.querySelector('#selectTimeOfDay').setAttribute('required', 'required');
|
||||
} else if (triggerType == 'WeeklyTrigger') {
|
||||
$('#fldTimeOfDay', page).show();
|
||||
$('#fldDayOfWeek', page).show();
|
||||
$('#fldSelectSystemEvent', page).hide();
|
||||
$('#fldSelectInterval', page).hide();
|
||||
$('#selectTimeOfDay', page).attr('required', 'required');
|
||||
page.querySelector('#fldTimeOfDay').classList.remove('hide');
|
||||
page.querySelector('#fldDayOfWeek').classList.remove('hide');
|
||||
page.querySelector('#fldSelectSystemEvent').classList.add('hide');
|
||||
page.querySelector('#fldSelectInterval').classList.add('hide');
|
||||
page.querySelector('#selectTimeOfDay').setAttribute('required', 'required');
|
||||
} else if (triggerType == 'SystemEventTrigger') {
|
||||
$('#fldTimeOfDay', page).hide();
|
||||
$('#fldDayOfWeek', page).hide();
|
||||
$('#fldSelectSystemEvent', page).show();
|
||||
$('#fldSelectInterval', page).hide();
|
||||
$('#selectTimeOfDay', page).removeAttr('required');
|
||||
page.querySelector('#fldTimeOfDay').classList.add('hide');
|
||||
page.querySelector('#fldDayOfWeek').classList.add('hide');
|
||||
page.querySelector('#fldSelectSystemEvent').classList.remove('hide');
|
||||
page.querySelector('#fldSelectInterval').classList.add('hide');
|
||||
page.querySelector('#selectTimeOfDay').removeAttribute('required');
|
||||
} else if (triggerType == 'IntervalTrigger') {
|
||||
$('#fldTimeOfDay', page).hide();
|
||||
$('#fldDayOfWeek', page).hide();
|
||||
$('#fldSelectSystemEvent', page).hide();
|
||||
$('#fldSelectInterval', page).show();
|
||||
$('#selectTimeOfDay', page).removeAttr('required');
|
||||
page.querySelector('#fldTimeOfDay').classList.add('hide');
|
||||
page.querySelector('#fldDayOfWeek').classList.add('hide');
|
||||
page.querySelector('#fldSelectSystemEvent').classList.add('hide');
|
||||
page.querySelector('#fldSelectInterval').classList.remove('hide');
|
||||
page.querySelector('#selectTimeOfDay').removeAttribute('required');
|
||||
} else if (triggerType == 'StartupTrigger') {
|
||||
$('#fldTimeOfDay', page).hide();
|
||||
$('#fldDayOfWeek', page).hide();
|
||||
$('#fldSelectSystemEvent', page).hide();
|
||||
$('#fldSelectInterval', page).hide();
|
||||
$('#selectTimeOfDay', page).removeAttr('required');
|
||||
page.querySelector('#fldTimeOfDay').classList.add('hide');
|
||||
page.querySelector('#fldDayOfWeek').classList.add('hide');
|
||||
page.querySelector('#fldSelectSystemEvent').classList.add('hide');
|
||||
page.querySelector('#fldSelectInterval').classList.add('hide');
|
||||
page.querySelector('#selectTimeOfDay').removeAttribute('required');
|
||||
}
|
||||
},
|
||||
getTriggerToAdd: function (page) {
|
||||
const trigger = {
|
||||
Type: $('#selectTriggerType', page).val()
|
||||
Type: page.querySelector('#selectTriggerType').value
|
||||
};
|
||||
|
||||
if (trigger.Type == 'DailyTrigger') {
|
||||
trigger.TimeOfDayTicks = $('#selectTimeOfDay', page).val();
|
||||
trigger.TimeOfDayTicks = page.querySelector('#selectTimeOfDay').value;
|
||||
} else if (trigger.Type == 'WeeklyTrigger') {
|
||||
trigger.DayOfWeek = $('#selectDayOfWeek', page).val();
|
||||
trigger.TimeOfDayTicks = $('#selectTimeOfDay', page).val();
|
||||
trigger.DayOfWeek = page.querySelector('#selectDayOfWeek').value;
|
||||
trigger.TimeOfDayTicks = page.querySelector('#selectTimeOfDay').value;
|
||||
} else if (trigger.Type == 'SystemEventTrigger') {
|
||||
trigger.SystemEvent = $('#selectSystemEvent', page).val();
|
||||
trigger.SystemEvent = page.querySelector('#selectSystemEvent').value;
|
||||
} else if (trigger.Type == 'IntervalTrigger') {
|
||||
trigger.IntervalTicks = $('#selectInterval', page).val();
|
||||
trigger.IntervalTicks = page.querySelector('#selectInterval').value;
|
||||
}
|
||||
|
||||
let timeLimit = $('#txtTimeLimit', page).val() || '0';
|
||||
let timeLimit = page.querySelector('#txtTimeLimit').value || '0';
|
||||
timeLimit = parseFloat(timeLimit) * 3600000;
|
||||
|
||||
trigger.MaxRuntimeTicks = timeLimit * 1e4 || null;
|
||||
|
@ -207,7 +206,7 @@ export default function (view) {
|
|||
ApiClient.getScheduledTask(id).then(function (task) {
|
||||
task.Triggers.push(ScheduledTaskPage.getTriggerToAdd(view));
|
||||
ApiClient.updateScheduledTaskTriggers(task.Id, task.Triggers).then(function () {
|
||||
$('#popupAddTrigger').addClass('hide');
|
||||
document.querySelector('#popupAddTrigger').classList.add('hide');
|
||||
ScheduledTaskPage.refreshScheduledTask(view);
|
||||
});
|
||||
});
|
||||
|
@ -216,7 +215,7 @@ export default function (view) {
|
|||
|
||||
view.querySelector('.addTriggerForm').addEventListener('submit', onSubmit);
|
||||
fillTimeOfDay(view.querySelector('#selectTimeOfDay'));
|
||||
$(view.querySelector('#popupAddTrigger').parentNode).trigger('create');
|
||||
view.querySelector('#popupAddTrigger').parentNode.trigger(new Event('create'));
|
||||
view.querySelector('.selectTriggerType').addEventListener('change', function () {
|
||||
ScheduledTaskPage.refreshTriggerFields(view, this.value);
|
||||
});
|
36
src/apps/dashboard/features/activity/api/useLogEntries.ts
Normal file
36
src/apps/dashboard/features/activity/api/useLogEntries.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import type { ActivityLogApiGetLogEntriesRequest } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import type { Api } from '@jellyfin/sdk';
|
||||
import { getActivityLogApi } from '@jellyfin/sdk/lib/utils/api/activity-log-api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
const fetchLogEntries = async (
|
||||
api?: Api,
|
||||
requestParams?: ActivityLogApiGetLogEntriesRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) => {
|
||||
if (!api) {
|
||||
console.warn('[fetchLogEntries] No API instance available');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await getActivityLogApi(api).getLogEntries(requestParams, {
|
||||
signal: options?.signal
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const useLogEntries = (
|
||||
requestParams: ActivityLogApiGetLogEntriesRequest
|
||||
) => {
|
||||
const { api } = useApi();
|
||||
return useQuery({
|
||||
queryKey: ['ActivityLogEntries', requestParams],
|
||||
queryFn: ({ signal }) =>
|
||||
fetchLogEntries(api, requestParams, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
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';
|
||||
|
||||
import type { ActivityLogEntryCell } from 'apps/dashboard/features/activity/types/ActivityLogEntryCell';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
const ActionsCell: FC<ActivityLogEntryCell> = ({ row }) => (
|
||||
row.original.ItemId ? (
|
||||
<IconButton
|
||||
size='large'
|
||||
title={globalize.translate('LabelMediaDetails')}
|
||||
component={Link}
|
||||
to={`/details?id=${row.original.ItemId}`}
|
||||
>
|
||||
<PermMedia fontSize='inherit' />
|
||||
</IconButton>
|
||||
) : undefined
|
||||
);
|
||||
|
||||
export default ActionsCell;
|
|
@ -0,0 +1,14 @@
|
|||
import type { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { ActivityLogEntryCell } from '../types/ActivityLogEntryCell';
|
||||
import LogLevelChip from './LogLevelChip';
|
||||
|
||||
const LogLevelCell: FC<ActivityLogEntryCell> = ({ cell }) => {
|
||||
const level = cell.getValue<LogLevel | undefined>();
|
||||
return level ? (
|
||||
<LogLevelChip level={level} />
|
||||
) : undefined;
|
||||
};
|
||||
|
||||
export default LogLevelCell;
|
|
@ -2,7 +2,7 @@ import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
|||
import Chip from '@mui/material/Chip';
|
||||
import React from 'react';
|
||||
|
||||
import globalize from 'scripts/globalize';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
const LogLevelChip = ({ level }: { level: LogLevel }) => {
|
||||
let color: 'info' | 'warning' | 'error' | undefined;
|
|
@ -1,12 +1,14 @@
|
|||
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
|
||||
import Info from '@mui/icons-material/Info';
|
||||
import Box from '@mui/material/Box';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import React, { type FC, useCallback, useState } from 'react';
|
||||
|
||||
const OverviewCell: FC<ActivityLogEntry> = ({ Overview, ShortOverview }) => {
|
||||
import type { ActivityLogEntryCell } from '../types/ActivityLogEntryCell';
|
||||
|
||||
const OverviewCell: FC<ActivityLogEntryCell> = ({ row }) => {
|
||||
const { ShortOverview, Overview } = row.original;
|
||||
const displayValue = ShortOverview ?? Overview;
|
||||
const [ open, setOpen ] = useState(false);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
|
||||
import type { MRT_Cell, MRT_Row } from 'material-react-table';
|
||||
|
||||
export interface ActivityLogEntryCell {
|
||||
cell: MRT_Cell<ActivityLogEntry>
|
||||
row: MRT_Row<ActivityLogEntry>
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
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
|
||||
) => {
|
||||
if (!api) {
|
||||
console.error('[fetchBrandingOptions] no Api instance provided');
|
||||
throw new Error('No Api instance provided to fetchBrandingOptions');
|
||||
}
|
||||
|
||||
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));
|
||||
};
|
24
src/apps/dashboard/features/devices/api/useDeleteDevice.ts
Normal file
24
src/apps/dashboard/features/devices/api/useDeleteDevice.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
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) => (
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
getDevicesApi(api!)
|
||||
.deleteDevice(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QUERY_KEY ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
38
src/apps/dashboard/features/devices/api/useDevices.ts
Normal file
38
src/apps/dashboard/features/devices/api/useDevices.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
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
|
||||
) => {
|
||||
if (!api) {
|
||||
console.warn('[fetchDevices] No API instance available');
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
};
|
24
src/apps/dashboard/features/devices/api/useUpdateDevice.ts
Normal file
24
src/apps/dashboard/features/devices/api/useUpdateDevice.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
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) => (
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
getDevicesApi(api!)
|
||||
.updateDeviceOptions(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QUERY_KEY ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
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;
|
|
@ -0,0 +1,7 @@
|
|||
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>
|
||||
}
|
27
src/apps/dashboard/features/keys/api/useApiKeys.ts
Normal file
27
src/apps/dashboard/features/keys/api/useApiKeys.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Api } from '@jellyfin/sdk';
|
||||
import { getApiKeyApi } from '@jellyfin/sdk/lib/utils/api/api-key-api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
|
||||
export const QUERY_KEY = 'ApiKeys';
|
||||
|
||||
const fetchApiKeys = async (api?: Api) => {
|
||||
if (!api) {
|
||||
console.error('[useApiKeys] Failed to create Api instance');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await getApiKeyApi(api).getKeys();
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const useApiKeys = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [ QUERY_KEY ],
|
||||
queryFn: () => fetchApiKeys(api),
|
||||
enabled: !!api
|
||||
});
|
||||
};
|
23
src/apps/dashboard/features/keys/api/useCreateKey.ts
Normal file
23
src/apps/dashboard/features/keys/api/useCreateKey.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
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) => (
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
getApiKeyApi(api!)
|
||||
.createKey(params)
|
||||
),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QUERY_KEY ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue