mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge branch 'master' into hadicharara/added-support-for-rtl-layouts
This commit is contained in:
commit
32f103b852
178 changed files with 25310 additions and 7347 deletions
|
@ -16,7 +16,7 @@ jobs:
|
|||
- task: NodeTool@0
|
||||
displayName: 'Install Node'
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
versionSpec: '16.x'
|
||||
|
||||
- task: Cache@2
|
||||
displayName: 'Cache node_modules'
|
||||
|
|
|
@ -42,6 +42,7 @@ module.exports = {
|
|||
'indent': ['error', 4, { 'SwitchCase': 1 }],
|
||||
'jsx-quotes': ['error', 'prefer-single'],
|
||||
'keyword-spacing': ['error'],
|
||||
'no-throw-literal': ['error'],
|
||||
'max-statements-per-line': ['error'],
|
||||
'no-duplicate-imports': ['error'],
|
||||
'no-empty-function': ['error'],
|
||||
|
@ -49,6 +50,7 @@ module.exports = {
|
|||
'no-multi-spaces': ['error'],
|
||||
'no-multiple-empty-lines': ['error', { 'max': 1 }],
|
||||
'no-restricted-globals': ['error'].concat(restrictedGlobals),
|
||||
'no-return-await': ['error'],
|
||||
'no-trailing-spaces': ['error'],
|
||||
'@babel/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
|
||||
'no-void': ['error', { 'allowAsStatement': true }],
|
||||
|
@ -62,7 +64,9 @@ module.exports = {
|
|||
'space-before-blocks': ['error'],
|
||||
'space-infix-ops': 'error',
|
||||
'yoda': 'error',
|
||||
'no-sequences': ['error', { 'allowInParentheses': false }]
|
||||
'no-sequences': ['error', { 'allowInParentheses': false }],
|
||||
|
||||
'react/jsx-filename-extension': ['error', { 'extensions': ['.jsx', '.tsx'] }]
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
@ -178,6 +182,7 @@ module.exports = {
|
|||
{
|
||||
files: [
|
||||
'./src/**/*.js',
|
||||
'./src/**/*.jsx',
|
||||
'./src/**/*.ts'
|
||||
],
|
||||
parser: '@babel/eslint-parser',
|
||||
|
|
51
.github/renovate.json
vendored
51
.github/renovate.json
vendored
|
@ -1,51 +1,4 @@
|
|||
{
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["npm"],
|
||||
"addLabels": ["javascript"]
|
||||
},
|
||||
{
|
||||
"description": "Adds label to dev dependency updates",
|
||||
"matchDepTypes": ["devDependencies"],
|
||||
"addLabels": ["dev-deps"]
|
||||
},
|
||||
{
|
||||
"description": "Collects and groups dev dependency updates",
|
||||
"matchDepTypes": ["devDependencies"],
|
||||
"groupName": "development dependencies",
|
||||
"groupSlug": "dev-deps"
|
||||
},
|
||||
{
|
||||
"description": "Collects and groups npm dependency updates",
|
||||
"matchDepTypes": ["dependencies"],
|
||||
"groupName": "dependencies",
|
||||
"groupSlug": "deps"
|
||||
},
|
||||
{
|
||||
"description": "Collects and groups GitHub Action dependency updates",
|
||||
"matchDepTypes": ["action"],
|
||||
"addLabels": ["github_actions"],
|
||||
"groupName": "CI dependencies",
|
||||
"groupSlug": "ci-deps"
|
||||
},
|
||||
{
|
||||
"description": "Disables HLS.js major updates",
|
||||
"matchPackageNames": ["hls.js"],
|
||||
"matchUpdateTypes": "major",
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"vulnerabilityAlerts": {
|
||||
"enabled": false
|
||||
},
|
||||
"dependencyDashboard": false,
|
||||
"ignoreDeps": ["npm", "node"],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": false
|
||||
},
|
||||
"enabledManagers": ["npm", "github-actions"],
|
||||
"labels": ["dependencies"],
|
||||
"prHourlyLimit": 2,
|
||||
"rebaseWhen": "conflicted",
|
||||
"rangeStrategy": "pin"
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["github>jellyfin/.github//renovate-presets/nodejs", ":semanticCommitsDisabled"]
|
||||
}
|
||||
|
|
2
.github/workflows/automation.yml
vendored
2
.github/workflows/automation.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
|
||||
steps:
|
||||
- uses: eps1lon/actions-label-merge-conflict@v2.0.1
|
||||
- uses: eps1lon/actions-label-merge-conflict@b8bf8341285ec9a4567d4318ba474fee998a6919 # tag=v2.0.1
|
||||
with:
|
||||
dirtyLabel: 'merge conflict'
|
||||
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
|
@ -19,13 +19,13 @@ jobs:
|
|||
language: [ 'javascript' ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@e0e5ded33cabb451ae0a9768fc7b0410bad9ad44 # tag=v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@e0e5ded33cabb451ae0a9768fc7b0410bad9ad44 # tag=v2
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@e0e5ded33cabb451ae0a9768fc7b0410bad9ad44 # tag=v2
|
||||
|
|
6
.github/workflows/commands.yml
vendored
6
.github/workflows/commands.yml
vendored
|
@ -12,17 +12,17 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify as seen
|
||||
uses: peter-evans/create-or-update-comment@v2.0.0
|
||||
uses: peter-evans/create-or-update-comment@c9fcb64660bc90ec1cc535646af190c992007c32 # tag=v2.0.0
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reactions: '+1'
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@v3.0.2
|
||||
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3.0.2
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
- name: Automatic Rebase
|
||||
uses: cirrus-actions/rebase@1.7
|
||||
uses: cirrus-actions/rebase@6e572f08c244e2f04f9beb85a943eb618218714d # tag=1.7
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
|
28
.github/workflows/lint.yml
vendored
28
.github/workflows/lint.yml
vendored
|
@ -13,12 +13,12 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@v3.3.0
|
||||
uses: actions/setup-node@969bd2663942d722d85b6a8626225850c2f7be4b # tag=v3.5.0
|
||||
with:
|
||||
node-version: 12
|
||||
node-version: 16
|
||||
check-latest: true
|
||||
|
||||
- name: Get npm cache directory path
|
||||
|
@ -26,7 +26,7 @@ jobs:
|
|||
run: echo "::set-output name=dir::$(npm config get cache)"
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v3.0.4
|
||||
uses: actions/cache@ac8075791e805656e71b4ba23325ace9e3421120 # tag=v3.0.9
|
||||
id: npm-cache
|
||||
with:
|
||||
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
|
||||
|
@ -48,12 +48,12 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@v3.3.0
|
||||
uses: actions/setup-node@969bd2663942d722d85b6a8626225850c2f7be4b # tag=v3.5.0
|
||||
with:
|
||||
node-version: 12
|
||||
node-version: 16
|
||||
check-latest: true
|
||||
|
||||
- name: Get npm cache directory path
|
||||
|
@ -61,7 +61,7 @@ jobs:
|
|||
run: echo "::set-output name=dir::$(npm config get cache)"
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v3.0.4
|
||||
uses: actions/cache@ac8075791e805656e71b4ba23325ace9e3421120 # tag=v3.0.9
|
||||
id: npm-cache
|
||||
with:
|
||||
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
|
||||
|
@ -70,7 +70,7 @@ jobs:
|
|||
${{ runner.os }}-npm-
|
||||
|
||||
- name: Set up stylelint matcher
|
||||
uses: xt0rted/stylelint-problem-matcher@v1
|
||||
uses: xt0rted/stylelint-problem-matcher@34db1b874c0452909f0696aedef70b723870a583 # tag=v1
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci --no-audit
|
||||
|
@ -86,12 +86,12 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@v3.3.0
|
||||
uses: actions/setup-node@969bd2663942d722d85b6a8626225850c2f7be4b # tag=v3.5.0
|
||||
with:
|
||||
node-version: 12
|
||||
node-version: 16
|
||||
check-latest: true
|
||||
|
||||
- name: Get npm cache directory path
|
||||
|
@ -99,7 +99,7 @@ jobs:
|
|||
run: echo "::set-output name=dir::$(npm config get cache)"
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v3.0.4
|
||||
uses: actions/cache@ac8075791e805656e71b4ba23325ace9e3421120 # tag=v3.0.9
|
||||
id: npm-cache
|
||||
with:
|
||||
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
|
||||
|
@ -108,7 +108,7 @@ jobs:
|
|||
${{ runner.os }}-npm-
|
||||
|
||||
- name: Set up stylelint matcher
|
||||
uses: xt0rted/stylelint-problem-matcher@v1
|
||||
uses: xt0rted/stylelint-problem-matcher@34db1b874c0452909f0696aedef70b723870a583 # tag=v1
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci --no-audit
|
||||
|
|
2
.github/workflows/repo-stale.yaml
vendored
2
.github/workflows/repo-stale.yaml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@v5.0.0
|
||||
- uses: actions/stale@3de2653986ebd134983c79fe2be5d45cc3d9f4e1 # tag=v6.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
days-before-stale: 120
|
||||
|
|
1
.npmrc
1
.npmrc
|
@ -1,2 +1,3 @@
|
|||
engine-strict=true
|
||||
fund=false
|
||||
save-exact=true
|
||||
|
|
|
@ -52,6 +52,8 @@
|
|||
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
||||
- [Matthew Jones](https://github.com/matthew-jones-uk)
|
||||
- [taku0](https://github.com/taku0)
|
||||
- [is343](https://github.com/is343)
|
||||
- [Meet Pandya](https://github.com/meet-k-pandya)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
@ -116,3 +118,4 @@
|
|||
- [Tim Hobbs](https://github.com/timhobbs)
|
||||
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
|
||||
- [jomp16](https://github.com/jomp16)
|
||||
- [Leon de Klerk](https://github.com/leondeklerk)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM centos:7
|
||||
FROM centos:8
|
||||
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/jellyfin
|
||||
|
@ -13,7 +13,7 @@ ENV IS_DOCKER=YES
|
|||
RUN yum update -y \
|
||||
&& yum install -y epel-release \
|
||||
&& yum install -y @buildsys-build rpmdevtools git yum-plugins-core autoconf automake glibc-devel gcc-c++ make \
|
||||
&& curl -fsSL https://rpm.nodesource.com/setup_12.x | bash - \
|
||||
&& curl -fsSL https://rpm.nodesource.com/setup_16.x | bash - \
|
||||
&& yum install -y nodejs
|
||||
|
||||
# Link to build script
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM debian:10
|
||||
FROM debian:11
|
||||
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/jellyfin
|
||||
|
@ -13,7 +13,7 @@ ENV IS_DOCKER=YES
|
|||
# Prepare Debian build environment
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y debhelper mmv git curl \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_12.x | bash - \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_16.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM fedora:36
|
||||
FROM fedora:37
|
||||
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/jellyfin
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM debian:10
|
||||
FROM debian:11
|
||||
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/jellyfin
|
||||
|
@ -12,7 +12,7 @@ ENV IS_DOCKER=YES
|
|||
# Prepare Debian build environment
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y mmv curl git \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_12.x | bash - \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_16.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
# Link to build script
|
||||
|
|
25303
package-lock.json
generated
25303
package-lock.json
generated
File diff suppressed because it is too large
Load diff
135
package.json
135
package.json
|
@ -1,110 +1,111 @@
|
|||
{
|
||||
"name": "jellyfin-web",
|
||||
"version": "10.8.0",
|
||||
"version": "10.9.0",
|
||||
"description": "Web interface for Jellyfin",
|
||||
"repository": "https://github.com/jellyfin/jellyfin-web",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.17.7",
|
||||
"@babel/eslint-parser": "7.17.0",
|
||||
"@babel/eslint-plugin": "7.17.7",
|
||||
"@babel/plugin-proposal-class-properties": "7.16.7",
|
||||
"@babel/plugin-proposal-private-methods": "7.16.11",
|
||||
"@babel/plugin-transform-modules-umd": "7.16.7",
|
||||
"@babel/preset-env": "7.16.11",
|
||||
"@babel/preset-react": "7.16.7",
|
||||
"@babel/preset-typescript": "7.16.7",
|
||||
"@thornbill/jellyfin-sdk": "0.4.1",
|
||||
"@types/escape-html": "1.0.1",
|
||||
"@babel/core": "7.19.1",
|
||||
"@babel/eslint-parser": "7.19.1",
|
||||
"@babel/eslint-plugin": "7.19.1",
|
||||
"@babel/plugin-proposal-class-properties": "7.18.6",
|
||||
"@babel/plugin-proposal-private-methods": "7.18.6",
|
||||
"@babel/plugin-transform-modules-umd": "7.18.6",
|
||||
"@babel/preset-env": "7.19.1",
|
||||
"@babel/preset-react": "7.18.6",
|
||||
"@babel/preset-typescript": "7.18.6",
|
||||
"@types/escape-html": "1.0.2",
|
||||
"@types/lodash-es": "4.17.6",
|
||||
"@types/react": "17.0.40",
|
||||
"@types/react-dom": "17.0.13",
|
||||
"@typescript-eslint/eslint-plugin": "5.15.0",
|
||||
"@typescript-eslint/parser": "5.15.0",
|
||||
"@types/react": "17.0.50",
|
||||
"@types/react-dom": "17.0.17",
|
||||
"@typescript-eslint/eslint-plugin": "5.38.0",
|
||||
"@typescript-eslint/parser": "5.38.0",
|
||||
"@uupaa/dynamic-import-polyfill": "1.0.2",
|
||||
"autoprefixer": "10.4.4",
|
||||
"babel-loader": "8.2.3",
|
||||
"autoprefixer": "10.4.12",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-plugin-dynamic-import-polyfill": "1.0.0",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"confusing-browser-globals": "1.0.11",
|
||||
"copy-webpack-plugin": "10.2.4",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"css-loader": "6.7.1",
|
||||
"cssnano": "5.1.4",
|
||||
"eslint": "8.11.0",
|
||||
"cssnano": "5.1.13",
|
||||
"eslint": "8.24.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-eslint-comments": "3.2.0",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-jsx-a11y": "6.5.1",
|
||||
"eslint-plugin-promise": "6.0.0",
|
||||
"eslint-plugin-react": "7.29.4",
|
||||
"eslint-plugin-react-hooks": "4.3.0",
|
||||
"expose-loader": "3.1.0",
|
||||
"html-loader": "3.1.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "6.6.1",
|
||||
"eslint-plugin-promise": "6.0.1",
|
||||
"eslint-plugin-react": "7.31.8",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"expose-loader": "4.0.0",
|
||||
"html-loader": "4.2.0",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"postcss": "8.4.12",
|
||||
"postcss-loader": "6.2.1",
|
||||
"postcss-preset-env": "7.4.2",
|
||||
"postcss-scss": "4.0.3",
|
||||
"sass": "1.49.9",
|
||||
"sass-loader": "12.6.0",
|
||||
"mini-css-extract-plugin": "2.6.1",
|
||||
"postcss": "8.4.16",
|
||||
"postcss-loader": "7.0.1",
|
||||
"postcss-preset-env": "7.8.2",
|
||||
"postcss-scss": "4.0.5",
|
||||
"sass": "1.55.0",
|
||||
"sass-loader": "13.0.2",
|
||||
"source-map-loader": "3.0.1",
|
||||
"style-loader": "3.3.1",
|
||||
"stylelint": "14.6.0",
|
||||
"stylelint": "14.12.1",
|
||||
"stylelint-config-rational-order": "0.1.2",
|
||||
"stylelint-no-browser-hacks": "1.2.1",
|
||||
"stylelint-order": "5.0.0",
|
||||
"stylelint-scss": "4.2.0",
|
||||
"ts-loader": "9.2.8",
|
||||
"typescript": "4.6.2",
|
||||
"webpack": "5.70.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-dev-server": "4.7.4",
|
||||
"stylelint-scss": "4.3.0",
|
||||
"ts-loader": "9.4.1",
|
||||
"typescript": "4.8.3",
|
||||
"webpack": "5.74.0",
|
||||
"webpack-cli": "4.10.0",
|
||||
"webpack-dev-server": "4.11.1",
|
||||
"webpack-merge": "5.8.0",
|
||||
"workbox-webpack-plugin": "6.5.1",
|
||||
"workbox-webpack-plugin": "6.5.4",
|
||||
"worker-loader": "3.0.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/noto-sans": "4.5.1",
|
||||
"@fontsource/noto-sans-hk": "4.5.2",
|
||||
"@fontsource/noto-sans-jp": "4.5.2",
|
||||
"@fontsource/noto-sans-kr": "4.5.2",
|
||||
"@fontsource/noto-sans-sc": "4.5.2",
|
||||
"@fontsource/noto-sans-tc": "4.5.2",
|
||||
"@fontsource/noto-sans": "4.5.11",
|
||||
"@fontsource/noto-sans-hk": "4.5.11",
|
||||
"@fontsource/noto-sans-jp": "4.5.11",
|
||||
"@fontsource/noto-sans-kr": "4.5.11",
|
||||
"@fontsource/noto-sans-sc": "4.5.11",
|
||||
"@fontsource/noto-sans-tc": "4.5.11",
|
||||
"@jellyfin/libass-wasm": "4.1.1",
|
||||
"blurhash": "1.1.4",
|
||||
"@jellyfin/sdk": "0.7.0",
|
||||
"blurhash": "2.0.0",
|
||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||
"classnames": "2.3.1",
|
||||
"core-js": "3.20.2",
|
||||
"date-fns": "2.28.0",
|
||||
"dompurify": "2.3.4",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.25.2",
|
||||
"date-fns": "2.29.3",
|
||||
"dompurify": "2.4.0",
|
||||
"epubjs": "0.3.93",
|
||||
"escape-html": "1.0.3",
|
||||
"fast-text-encoding": "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": "0.14.17",
|
||||
"intersection-observer": "0.12.0",
|
||||
"intersection-observer": "0.12.2",
|
||||
"jellyfin-apiclient": "1.10.0",
|
||||
"jquery": "3.6.0",
|
||||
"jquery": "3.6.1",
|
||||
"jstree": "3.3.12",
|
||||
"libarchive.js": "1.3.0",
|
||||
"lodash-es": "4.17.21",
|
||||
"marked": "4.0.10",
|
||||
"material-design-icons-iconfont": "6.1.1",
|
||||
"marked": "4.1.0",
|
||||
"material-design-icons-iconfont": "6.7.0",
|
||||
"native-promise-only": "0.8.1",
|
||||
"pdfjs-dist": "2.12.313",
|
||||
"pdfjs-dist": "2.16.105",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-router-dom": "6.3.0",
|
||||
"react-router-dom": "6.4.1",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"screenfull": "6.0.0",
|
||||
"sortablejs": "1.14.0",
|
||||
"swiper": "6.8.4",
|
||||
"screenfull": "6.0.2",
|
||||
"sortablejs": "1.15.0",
|
||||
"swiper": "8.4.2",
|
||||
"webcomponents.js": "0.7.24",
|
||||
"whatwg-fetch": "3.6.2",
|
||||
"workbox-core": "6.2.4",
|
||||
"workbox-precaching": "6.2.4"
|
||||
"workbox-core": "6.5.4",
|
||||
"workbox-precaching": "6.5.4"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 Firefox versions",
|
||||
|
@ -127,13 +128,15 @@
|
|||
"serve": "webpack serve --config webpack.dev.js",
|
||||
"prepare": "node ./scripts/prepare.js",
|
||||
"build:development": "webpack --config webpack.dev.js",
|
||||
"build:production": "webpack --config webpack.prod.js",
|
||||
"build:production": "NODE_ENV=\"production\" webpack --config webpack.prod.js",
|
||||
"lint": "eslint \"./\"",
|
||||
"stylelint": "npm run stylelint:css && npm run stylelint:scss",
|
||||
"stylelint:css": "stylelint \"src/**/*.css\"",
|
||||
"stylelint:scss": "stylelint --config=\".stylelintrc.scss.json\" \"src/**/*.scss\""
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.13.1",
|
||||
"npm": ">=8.1.2",
|
||||
"yarn": "YARN NO LONGER USED - use npm instead."
|
||||
}
|
||||
}
|
||||
|
|
2
src/apiclient.d.ts
vendored
2
src/apiclient.d.ts
vendored
|
@ -1,7 +1,7 @@
|
|||
// TODO: Move to jellyfin-apiclient
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare module 'jellyfin-apiclient' {
|
||||
import {
|
||||
import type {
|
||||
AllThemeMediaResult,
|
||||
AuthenticationResult,
|
||||
BaseItemDto,
|
||||
|
|
|
@ -219,6 +219,9 @@
|
|||
flex-direction: column;
|
||||
contain: layout style paint;
|
||||
transition: background ease-in-out 0.5s;
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-top: env(safe-area-inset-top);
|
||||
}
|
||||
|
||||
.layout-tv .skinHeader {
|
||||
|
@ -1366,20 +1369,24 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards {
|
|||
.padded-left {
|
||||
[dir="ltr"] & {
|
||||
padding-left: 3.3%;
|
||||
padding-left: max(env(safe-area-inset-left), 3.3%);
|
||||
}
|
||||
|
||||
[dir="rtl"] & {
|
||||
padding-right: 3.3%;
|
||||
padding-right: max(env(safe-area-inset-right), 3.3%);
|
||||
}
|
||||
}
|
||||
|
||||
.padded-right {
|
||||
[dir="ltr"] & {
|
||||
padding-right: 3.3%;
|
||||
padding-right: max(env(safe-area-inset-right), 3.3%);
|
||||
}
|
||||
|
||||
[dir="rtl"] & {
|
||||
padding-left: 3.3%;
|
||||
padding-left: max(env(safe-area-inset-left), 3.3%);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1404,6 +1411,7 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards {
|
|||
@media all and (min-height: 31.25em) {
|
||||
.padded-right-withalphapicker {
|
||||
padding-right: 7.5%;
|
||||
padding-right: max(env(safe-area-inset-left), 7.5%);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -84,6 +84,7 @@ div[data-role="page"] {
|
|||
.pageWithAbsoluteTabs .pageTabContent {
|
||||
/* provides room for the music controls */
|
||||
padding-bottom: 5em !important;
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 5em) !important;
|
||||
}
|
||||
|
||||
.readOnlyContent {
|
||||
|
|
|
@ -12,8 +12,11 @@
|
|||
right: 0;
|
||||
position: fixed;
|
||||
background: linear-gradient(0deg, rgba(16, 16, 16, 0.75) 0%, rgba(16, 16, 16, 0) 100%);
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-top: 7.5em;
|
||||
padding-bottom: 1.75em;
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 1.75em);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
@ -112,10 +115,6 @@
|
|||
padding: 0 0.8em;
|
||||
}
|
||||
|
||||
.layout-desktop .osdControls {
|
||||
max-width: calc(100vh * 1.77 - 2vh);
|
||||
}
|
||||
|
||||
.videoOsdBottom .buttons {
|
||||
padding: 0.25em 0 0;
|
||||
display: -webkit-box;
|
||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
@ -1,5 +1,5 @@
|
|||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
|
||||
import alert from './alert';
|
||||
import { appRouter } from './appRouter';
|
||||
|
@ -33,7 +33,6 @@ type ConnectionRequiredProps = {
|
|||
* If a condition fails, this component will navigate to the appropriate page.
|
||||
*/
|
||||
const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
||||
children,
|
||||
isAdminRequired = false,
|
||||
isUserRequired = true
|
||||
}) => {
|
||||
|
@ -147,12 +146,14 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
|||
setIsLoading(false);
|
||||
};
|
||||
|
||||
loading.show();
|
||||
validateConnection();
|
||||
}, [ isAdminRequired, isUserRequired, navigate ]);
|
||||
|
||||
// Show/hide the loading indicator
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
if (isLoading) {
|
||||
loading.show();
|
||||
} else {
|
||||
loading.hide();
|
||||
}
|
||||
}, [ isLoading ]);
|
||||
|
@ -162,7 +163,9 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<>{children}</>
|
||||
<div className='skinBody'>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -6,8 +6,10 @@ type PageProps = {
|
|||
id: string, // id is required for libraryMenu
|
||||
title?: string,
|
||||
isBackButtonEnabled?: boolean,
|
||||
isMenuButtonEnabled?: boolean,
|
||||
isNowPlayingBarEnabled?: boolean,
|
||||
isThemeMediaSupported?: boolean
|
||||
isThemeMediaSupported?: boolean,
|
||||
backDropType?: string
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -20,8 +22,10 @@ const Page: FunctionComponent<PageProps & HTMLAttributes<HTMLDivElement>> = ({
|
|||
className = '',
|
||||
title,
|
||||
isBackButtonEnabled = true,
|
||||
isMenuButtonEnabled = false,
|
||||
isNowPlayingBarEnabled = true,
|
||||
isThemeMediaSupported = false
|
||||
isThemeMediaSupported = false,
|
||||
backDropType
|
||||
}) => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
@ -59,7 +63,9 @@ const Page: FunctionComponent<PageProps & HTMLAttributes<HTMLDivElement>> = ({
|
|||
data-role='page'
|
||||
className={`page ${className}`}
|
||||
data-title={title}
|
||||
data-backbutton={`${isBackButtonEnabled}`}
|
||||
data-backbutton={isBackButtonEnabled}
|
||||
data-menubutton={isMenuButtonEnabled}
|
||||
data-backdroptype={backDropType}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -58,7 +58,7 @@ class ServerConnections extends ConnectionManager {
|
|||
);
|
||||
|
||||
apiClient.enableAutomaticNetworking = false;
|
||||
apiClient.manualAddressOnly = true;
|
||||
apiClient.manualAddressOnly = false;
|
||||
|
||||
this.addApiClient(apiClient);
|
||||
|
||||
|
|
|
@ -301,7 +301,7 @@ export function show(options) {
|
|||
|
||||
resolve(selectedId);
|
||||
} else {
|
||||
reject();
|
||||
reject('ActionSheet closed without resolving');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
.alphaPicker-fixed {
|
||||
position: fixed;
|
||||
bottom: 5.5em;
|
||||
bottom: max(env(safe-area-inset-bottom), 5.5em);
|
||||
}
|
||||
|
||||
.alphaPickerRow {
|
||||
|
@ -45,6 +46,7 @@
|
|||
@media all and (max-height: 50em) {
|
||||
.alphaPicker-fixed {
|
||||
bottom: 5em;
|
||||
bottom: max(env(safe-area-inset-bottom), 5em);
|
||||
}
|
||||
|
||||
.alphaPickerButton-vertical {
|
||||
|
@ -104,15 +106,18 @@
|
|||
|
||||
.alphaPicker-fixed.alphaPicker-tv {
|
||||
bottom: 1%;
|
||||
bottom: max(env(safe-area-inset-bottom), 1%);
|
||||
}
|
||||
|
||||
.alphaPicker-fixed-right {
|
||||
[dir="ltr"] & {
|
||||
right: 0.4em;
|
||||
right: max(env(safe-area-inset-right), 0.4em);
|
||||
}
|
||||
|
||||
[dir="rtl"] & {
|
||||
left: 0.4em;
|
||||
left: max(env(safe-area-inset-right), 0.4em)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -120,10 +125,12 @@
|
|||
.alphaPicker-fixed-right {
|
||||
[dir="ltr"] & {
|
||||
right: 1em;
|
||||
right: max(env(safe-area-inset-right), 1em);
|
||||
}
|
||||
|
||||
[dir="rtl"] & {
|
||||
left: 1em;
|
||||
left: max(env(safe-area-inset-right), 1em);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,13 @@
|
|||
bottom: 0;
|
||||
transition: transform 180ms linear;
|
||||
contain: layout style;
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.appfooter:empty {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.appfooter.headroom--unpinned {
|
||||
|
|
|
@ -9,7 +9,6 @@ import loading from './loading/loading';
|
|||
import viewManager from './viewManager/viewManager';
|
||||
import ServerConnections from './ServerConnections';
|
||||
import alert from './alert';
|
||||
import reactControllerFactory from './reactControllerFactory';
|
||||
|
||||
export const history = createHashHistory();
|
||||
|
||||
|
@ -264,9 +263,7 @@ class AppRouter {
|
|||
this.#sendRouteToViewManager(ctx, next, route, controllerFactory);
|
||||
};
|
||||
|
||||
if (route.pageComponent) {
|
||||
onInitComplete(reactControllerFactory);
|
||||
} else if (route.controller) {
|
||||
if (route.controller) {
|
||||
import('../controllers/' + route.controller).then(onInitComplete);
|
||||
} else {
|
||||
onInitComplete();
|
||||
|
@ -293,7 +290,6 @@ class AppRouter {
|
|||
fullscreen: route.fullscreen,
|
||||
controllerFactory: controllerFactory,
|
||||
options: {
|
||||
pageComponent: route.pageComponent,
|
||||
supportsThemeMedia: route.supportsThemeMedia || false,
|
||||
enableMediaControl: route.enableMediaControl !== false
|
||||
},
|
||||
|
|
|
@ -821,7 +821,7 @@ import { appRouter } from '../appRouter';
|
|||
if (isUsingLiveTvNaming(item)) {
|
||||
lines.push(escapeHtml(item.Name));
|
||||
|
||||
if (!item.EpisodeTitle) {
|
||||
if (!item.EpisodeTitle && !item.IndexNumber) {
|
||||
titleAdded = true;
|
||||
}
|
||||
} else {
|
||||
|
@ -1350,7 +1350,7 @@ import { appRouter } from '../appRouter';
|
|||
|
||||
cardImageContainerClose = '</div>';
|
||||
} else {
|
||||
const cardImageContainerAriaLabelAttribute = ` aria-label="${item.Name}"`;
|
||||
const cardImageContainerAriaLabelAttribute = ` aria-label="${escapeHtml(item.Name)}"`;
|
||||
|
||||
const url = appRouter.getRouteUrl(item);
|
||||
// Don't use the IMG tag with safari because it puts a white border around it
|
||||
|
@ -1434,7 +1434,7 @@ import { appRouter } from '../appRouter';
|
|||
if (tagName === 'button') {
|
||||
className += ' itemAction';
|
||||
actionAttribute = ' data-action="' + action + '"';
|
||||
ariaLabelAttribute = ` aria-label="${item.Name}"`;
|
||||
ariaLabelAttribute = ` aria-label="${escapeHtml(item.Name)}"`;
|
||||
} else {
|
||||
actionAttribute = '';
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import CheckBoxElement from './CheckBoxElement';
|
||||
import CheckBoxElement from '../../../elements/CheckBoxElement';
|
||||
|
||||
type IProps = {
|
||||
containerClassName?: string;
|
||||
|
@ -18,7 +18,11 @@ const AccessContainer: FunctionComponent<IProps> = ({containerClassName, headerT
|
|||
return (
|
||||
<div className={containerClassName}>
|
||||
<h2>{globalize.translate(headerTitle)}</h2>
|
||||
<CheckBoxElement labelClassName='checkboxContainer' type='checkbox' className={checkBoxClassName} title={checkBoxTitle} />
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
className={checkBoxClassName}
|
||||
title={checkBoxTitle}
|
||||
/>
|
||||
<div className={listContainerClassName}>
|
||||
<div className={accessClassName}>
|
||||
<h3 className='checkboxListLabel'>
|
||||
|
|
|
@ -1,21 +1,11 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import datetime from '../../../scripts/datetime';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import IconButtonElement from '../../../elements/IconButtonElement';
|
||||
|
||||
const createButtonElement = (index: number) => ({
|
||||
__html: `<button
|
||||
type='button'
|
||||
is='paper-icon-button-light'
|
||||
class='btnDelete listItemButton'
|
||||
data-index='${index}'
|
||||
>
|
||||
<span class='material-icons delete' aria-hidden='true' />
|
||||
</button>`
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
type AccessScheduleListProps = {
|
||||
index: number;
|
||||
Id: number;
|
||||
Id?: number;
|
||||
DayOfWeek?: string;
|
||||
StartHour?: number ;
|
||||
EndHour?: number;
|
||||
|
@ -32,7 +22,7 @@ function getDisplayTime(hours = 0) {
|
|||
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
|
||||
}
|
||||
|
||||
const AccessScheduleList: FunctionComponent<IProps> = ({index, DayOfWeek, StartHour, EndHour}: IProps) => {
|
||||
const AccessScheduleList: FunctionComponent<AccessScheduleListProps> = ({index, DayOfWeek, StartHour, EndHour}: AccessScheduleListProps) => {
|
||||
return (
|
||||
<div
|
||||
className='liSchedule listItem'
|
||||
|
@ -48,8 +38,12 @@ const AccessScheduleList: FunctionComponent<IProps> = ({index, DayOfWeek, StartH
|
|||
{getDisplayTime(StartHour) + ' - ' + getDisplayTime(EndHour)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
dangerouslySetInnerHTML={createButtonElement(index)}
|
||||
<IconButtonElement
|
||||
is='paper-icon-button-light'
|
||||
className='btnDelete listItemButton'
|
||||
title='Delete'
|
||||
icon='delete'
|
||||
dataIndex={index}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,15 +1,5 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
const createButtonElement = (tag?: string) => ({
|
||||
__html: `<button
|
||||
type='button'
|
||||
is='paper-icon-button-light'
|
||||
class='blockedTag btnDeleteTag listItemButton'
|
||||
data-tag='${tag}'
|
||||
>
|
||||
<span class='material-icons delete' aria-hidden='true' />
|
||||
</button>`
|
||||
});
|
||||
import IconButtonElement from '../../../elements/IconButtonElement';
|
||||
|
||||
type IProps = {
|
||||
tag?: string;
|
||||
|
@ -24,11 +14,14 @@ const BlockedTagList: FunctionComponent<IProps> = ({tag}: IProps) => {
|
|||
{tag}
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
dangerouslySetInnerHTML={createButtonElement(tag)}
|
||||
<IconButtonElement
|
||||
is='paper-icon-button-light'
|
||||
className='blockedTag btnDeleteTag listItemButton'
|
||||
title='Delete'
|
||||
icon='delete'
|
||||
dataTag={tag}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const createButtonElement = ({ type, className, title }: { type?: string, className?: string, title?: string }) => ({
|
||||
__html: `<button
|
||||
is="emby-button"
|
||||
type="${type}"
|
||||
class="${className}"
|
||||
>
|
||||
<span>${title}</span>
|
||||
</button>`
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
type?: string;
|
||||
className?: string;
|
||||
title?: string
|
||||
}
|
||||
|
||||
const ButtonElement: FunctionComponent<IProps> = ({ type, className, title }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={createButtonElement({
|
||||
type: type,
|
||||
className: className,
|
||||
title: globalize.translate(title)
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonElement;
|
|
@ -1,36 +0,0 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const createCheckBoxElement = ({ labelClassName, type, className, title }: { labelClassName?: string, type?: string, className?: string, title?: string }) => ({
|
||||
__html: `<label class="${labelClassName}">
|
||||
<input
|
||||
is="emby-checkbox"
|
||||
type="${type}"
|
||||
class="${className}"
|
||||
/>
|
||||
<span>${title}</span>
|
||||
</label>`
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
labelClassName?: string;
|
||||
type?: string;
|
||||
className?: string;
|
||||
title?: string
|
||||
}
|
||||
|
||||
const CheckBoxElement: FunctionComponent<IProps> = ({ labelClassName, type, className, title }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
className='sectioncheckbox'
|
||||
dangerouslySetInnerHTML={createCheckBoxElement({
|
||||
labelClassName: labelClassName ? labelClassName : '',
|
||||
type: type,
|
||||
className: className,
|
||||
title: globalize.translate(title)
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckBoxElement;
|
|
@ -1,41 +0,0 @@
|
|||
import escapeHtml from 'escape-html';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
type IProps = {
|
||||
className?: string;
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
ItemType?: string;
|
||||
AppName?: string;
|
||||
checkedAttribute?: string;
|
||||
}
|
||||
|
||||
const createCheckBoxElement = ({className, Name, dataAttributes, AppName, checkedAttribute}: {className?: string, Name?: string, dataAttributes?: string, AppName?: string, checkedAttribute?: string}) => ({
|
||||
__html: `<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
is="emby-checkbox"
|
||||
class="${className}"
|
||||
${dataAttributes} ${checkedAttribute}
|
||||
/>
|
||||
<span>${escapeHtml(Name || '')} ${AppName}</span>
|
||||
</label>`
|
||||
});
|
||||
|
||||
const CheckBoxListItem: FunctionComponent<IProps> = ({className, Name, Id, ItemType, AppName, checkedAttribute}: IProps) => {
|
||||
return (
|
||||
<div
|
||||
className='sectioncheckbox'
|
||||
dangerouslySetInnerHTML={createCheckBoxElement({
|
||||
className: className,
|
||||
Name: Name,
|
||||
dataAttributes: ItemType ? `data-itemtype='${ItemType}'` : `data-id='${Id}'`,
|
||||
AppName: AppName ? `- ${AppName}` : '',
|
||||
checkedAttribute: checkedAttribute
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckBoxListItem;
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
type IProps = {
|
||||
title: string;
|
||||
className?: string;
|
||||
icon: string,
|
||||
}
|
||||
|
||||
const createButtonElement = ({ className, title, icon }: { className?: string, title: string, icon: string }) => ({
|
||||
__html: `<button
|
||||
is="emby-button"
|
||||
type="button"
|
||||
class="${className}"
|
||||
style="margin-left:1em;"
|
||||
title="${title}"
|
||||
>
|
||||
<span class="material-icons ${icon}" aria-hidden="true"></span>
|
||||
</button>`
|
||||
});
|
||||
|
||||
const SectionTitleButtonElement: FunctionComponent<IProps> = ({ className, title, icon }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={createButtonElement({
|
||||
className: className,
|
||||
title: globalize.translate(title),
|
||||
icon: icon
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionTitleButtonElement;
|
|
@ -1,35 +0,0 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import SectionTitleButtonElement from './SectionTitleButtonElement';
|
||||
import SectionTitleLinkElement from './SectionTitleLinkElement';
|
||||
|
||||
type IProps = {
|
||||
title: string;
|
||||
isBtnVisible?: boolean;
|
||||
titleLink?: string;
|
||||
}
|
||||
|
||||
const SectionTitleContainer: FunctionComponent<IProps> = ({title, isBtnVisible = false, titleLink}: IProps) => {
|
||||
return (
|
||||
<div className='verticalSection'>
|
||||
<div className='sectionTitleContainer flex align-items-center'>
|
||||
<h2 className='sectionTitle'>
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
{isBtnVisible && <SectionTitleButtonElement
|
||||
className='fab btnAddUser submit sectionTitleButton'
|
||||
title='ButtonAddUser'
|
||||
icon='add'
|
||||
/>}
|
||||
|
||||
<SectionTitleLinkElement
|
||||
className='raised button-alt headerHelpButton'
|
||||
title='Help'
|
||||
url={titleLink}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionTitleContainer;
|
|
@ -1,44 +0,0 @@
|
|||
import escapeHtml from 'escape-html';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const createSelectElement = ({ className, label, option }: { className?: string, label: string, option: string[] }) => ({
|
||||
__html: `<select
|
||||
class="${className}"
|
||||
is="emby-select"
|
||||
label="${label}"
|
||||
>
|
||||
${option}
|
||||
</select>`
|
||||
});
|
||||
|
||||
type ProvidersArr = {
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
}
|
||||
|
||||
type IProps = {
|
||||
className?: string;
|
||||
label?: string;
|
||||
currentProviderId: string;
|
||||
providers: ProvidersArr[]
|
||||
}
|
||||
|
||||
const SelectElement: FunctionComponent<IProps> = ({ className, label, currentProviderId, providers }: IProps) => {
|
||||
const renderOption = providers.map((provider) => {
|
||||
const selected = provider.Id === currentProviderId || providers.length < 2 ? ' selected' : '';
|
||||
return '<option value="' + provider.Id + '"' + selected + '>' + escapeHtml(provider.Name) + '</option>';
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={createSelectElement({
|
||||
className: className,
|
||||
label: globalize.translate(label),
|
||||
option: renderOption
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectElement;
|
|
@ -1,47 +0,0 @@
|
|||
import escapeHtml from 'escape-html';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const createSelectElement = ({ className, label, option }: { className?: string, label: string, option: string }) => ({
|
||||
__html: `<select
|
||||
class="${className}"
|
||||
is="emby-select"
|
||||
label="${label}"
|
||||
>
|
||||
<option value=''></option>
|
||||
${option}
|
||||
</select>`
|
||||
});
|
||||
|
||||
type RatingsArr = {
|
||||
Name: string;
|
||||
Value: number;
|
||||
}
|
||||
|
||||
type IProps = {
|
||||
className?: string;
|
||||
label?: string;
|
||||
parentalRatings: RatingsArr[];
|
||||
}
|
||||
|
||||
const SelectMaxParentalRating: FunctionComponent<IProps> = ({ className, label, parentalRatings }: IProps) => {
|
||||
const renderOption = () => {
|
||||
let content = '';
|
||||
for (const rating of parentalRatings) {
|
||||
content += `<option value='${rating.Value}'>${escapeHtml(rating.Name)}</option>`;
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={createSelectElement({
|
||||
className: className,
|
||||
label: globalize.translate(label),
|
||||
option: renderOption()
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectMaxParentalRating;
|
|
@ -1,35 +0,0 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const createSelectElement = ({ className, id, label }: { className?: string, id?: string, label: string }) => ({
|
||||
__html: `<select
|
||||
class="${className}"
|
||||
is="emby-select"
|
||||
id="${id}"
|
||||
label="${label}"
|
||||
>
|
||||
<option value='CreateAndJoinGroups'>${globalize.translate('LabelSyncPlayAccessCreateAndJoinGroups')}</option>
|
||||
<option value='JoinGroups'>${globalize.translate('LabelSyncPlayAccessJoinGroups')}</option>
|
||||
<option value='None'>${globalize.translate('LabelSyncPlayAccessNone')}</option>
|
||||
</select>`
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
className?: string;
|
||||
id?: string;
|
||||
label?: string
|
||||
}
|
||||
|
||||
const SelectSyncPlayAccessElement: FunctionComponent<IProps> = ({ className, id, label }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={createSelectElement({
|
||||
className: className,
|
||||
id: id,
|
||||
label: globalize.translate(label)
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectSyncPlayAccessElement;
|
|
@ -1,9 +1,11 @@
|
|||
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { localeWithSuffix } from '../../../scripts/dfnshelper';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import cardBuilder from '../../cardbuilder/cardBuilder';
|
||||
import IconButtonElement from '../../../elements/IconButtonElement';
|
||||
import escapeHTML from 'escape-html';
|
||||
|
||||
const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl: string }) => ({
|
||||
__html: `<a
|
||||
|
@ -15,16 +17,6 @@ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl
|
|||
</a>`
|
||||
});
|
||||
|
||||
const createButtonElement = () => ({
|
||||
__html: `<button
|
||||
is="paper-icon-button-light"
|
||||
type="button"
|
||||
class="btnUserMenu flex-shrink-zero"
|
||||
>
|
||||
<span class="material-icons more_vert" aria-hidden="true"></span>
|
||||
</button>`
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
user?: UserDto;
|
||||
}
|
||||
|
@ -81,16 +73,20 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
|
|||
/>
|
||||
</div>
|
||||
<div className='cardFooter visualCardBox-cardFooter'>
|
||||
<div className='cardText flex align-items-center'>
|
||||
<div className='flex-grow' style={{overflow: 'hidden', textOverflow: 'ellipsis'}}>
|
||||
{user.Name}
|
||||
</div>
|
||||
<div
|
||||
dangerouslySetInnerHTML={createButtonElement()}
|
||||
<div
|
||||
style={{textAlign: 'right', float: 'right', paddingTop: '5px'}}
|
||||
>
|
||||
<IconButtonElement
|
||||
is='paper-icon-button-light'
|
||||
className='btnUserMenu flex-shrink-zero'
|
||||
icon='more_vert'
|
||||
/>
|
||||
</div>
|
||||
<div className='cardText'>
|
||||
<span>{escapeHTML(user.Name)}</span>
|
||||
</div>
|
||||
<div className='cardText cardText-secondary'>
|
||||
{lastSeen != '' ? lastSeen : ''}
|
||||
<span>{lastSeen != '' ? lastSeen : ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react';
|
||||
import Dashboard from '../../../utils/dashboard';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
@ -6,9 +6,9 @@ import LibraryMenu from '../../../scripts/libraryMenu';
|
|||
import confirm from '../../confirm/confirm';
|
||||
import loading from '../../loading/loading';
|
||||
import toast from '../../toast/toast';
|
||||
import ButtonElement from './ButtonElement';
|
||||
import CheckBoxElement from './CheckBoxElement';
|
||||
import InputElement from './InputElement';
|
||||
import ButtonElement from '../../../elements/ButtonElement';
|
||||
import CheckBoxElement from '../../../elements/CheckBoxElement';
|
||||
import InputElement from '../../../elements/InputElement';
|
||||
|
||||
type IProps = {
|
||||
userId: string;
|
||||
|
@ -40,11 +40,11 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
|||
let showLocalAccessSection = false;
|
||||
|
||||
if (user.HasConfiguredPassword) {
|
||||
(page.querySelector('.btnResetPassword') as HTMLDivElement).classList.remove('hide');
|
||||
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide');
|
||||
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide');
|
||||
showLocalAccessSection = true;
|
||||
} else {
|
||||
(page.querySelector('.btnResetPassword') as HTMLDivElement).classList.add('hide');
|
||||
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.add('hide');
|
||||
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide');
|
||||
}
|
||||
|
||||
|
@ -65,11 +65,11 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
|||
|
||||
if (user.HasConfiguredEasyPassword) {
|
||||
txtEasyPassword.placeholder = '******';
|
||||
(page.querySelector('.btnResetEasyPassword') as HTMLDivElement).classList.remove('hide');
|
||||
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.remove('hide');
|
||||
} else {
|
||||
txtEasyPassword.removeAttribute('placeholder');
|
||||
txtEasyPassword.placeholder = '';
|
||||
(page.querySelector('.btnResetEasyPassword') as HTMLDivElement).classList.add('hide');
|
||||
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.add('hide');
|
||||
}
|
||||
|
||||
const chkEnableLocalEasyPassword = page.querySelector('.chkEnableLocalEasyPassword') as HTMLInputElement;
|
||||
|
@ -206,8 +206,8 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
|||
(page.querySelector('.updatePasswordForm') as HTMLFormElement).addEventListener('submit', onSubmit);
|
||||
(page.querySelector('.localAccessForm') as HTMLFormElement).addEventListener('submit', onLocalAccessSubmit);
|
||||
|
||||
(page.querySelector('.btnResetEasyPassword') as HTMLButtonElement).addEventListener('click', resetEasyPassword);
|
||||
(page.querySelector('.btnResetPassword') as HTMLButtonElement).addEventListener('click', resetPassword);
|
||||
(page.querySelector('#btnResetEasyPassword') as HTMLButtonElement).addEventListener('click', resetEasyPassword);
|
||||
(page.querySelector('#btnResetPassword') as HTMLButtonElement).addEventListener('click', resetPassword);
|
||||
}, [loadUser, userId]);
|
||||
|
||||
return (
|
||||
|
@ -250,7 +250,8 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
|||
/>
|
||||
<ButtonElement
|
||||
type='button'
|
||||
className='raised btnResetPassword button-cancel block hide'
|
||||
id='btnResetPassword'
|
||||
className='raised button-cancel block hide'
|
||||
title='ResetPassword'
|
||||
/>
|
||||
</div>
|
||||
|
@ -281,7 +282,6 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
|||
<br />
|
||||
<div className='checkboxContainer checkboxContainer-withDescription'>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkEnableLocalEasyPassword'
|
||||
title='LabelInNetworkSignInWithEasyPassword'
|
||||
/>
|
||||
|
@ -297,7 +297,8 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
|||
/>
|
||||
<ButtonElement
|
||||
type='button'
|
||||
className='raised btnResetEasyPassword button-cancel block hide'
|
||||
id='btnResetEasyPassword'
|
||||
className='raised button-cancel block hide'
|
||||
title='ButtonResetEasyPassword'
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -75,8 +75,7 @@
|
|||
*/
|
||||
function paramsToString(params) {
|
||||
return Object.entries(params)
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
.filter(([_, v]) => v !== null && v !== undefined && v !== '')
|
||||
.filter(([, v]) => v !== null && v !== undefined && v !== '')
|
||||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
||||
.join('&');
|
||||
}
|
||||
|
|
|
@ -123,7 +123,7 @@ export function canEdit(user, item) {
|
|||
}
|
||||
|
||||
export function isLocalItem(item) {
|
||||
if (item && item.Id && item.Id.indexOf('local') === 0) {
|
||||
if (item && item.Id && typeof item.Id === 'string' && item.Id.indexOf('local') === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,13 @@
|
|||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 1em 0.5em;
|
||||
padding-left: 0.5em;
|
||||
padding-left: max(env(safe-area-inset-left), 0.5em);
|
||||
padding-right: 0.5em;
|
||||
padding-right: max(env(safe-area-inset-right), 0.5em);
|
||||
padding-top: 1em;
|
||||
padding-top: max(env(safe-area-inset-top), 1em);
|
||||
padding-bottom: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 99999;
|
||||
|
|
|
@ -24,6 +24,7 @@ import { appRouter } from '../appRouter';
|
|||
|
||||
let currentTimeElement;
|
||||
let nowPlayingImageElement;
|
||||
let nowPlayingImageUrl;
|
||||
let nowPlayingTextElement;
|
||||
let nowPlayingUserData;
|
||||
let muteButton;
|
||||
|
@ -488,7 +489,6 @@ import { appRouter } from '../appRouter';
|
|||
return null;
|
||||
}
|
||||
|
||||
let currentImgUrl;
|
||||
function updateNowPlayingInfo(state) {
|
||||
const nowPlayingItem = state.NowPlayingItem;
|
||||
|
||||
|
@ -524,17 +524,14 @@ import { appRouter } from '../appRouter';
|
|||
height: imgHeight
|
||||
})) : null;
|
||||
|
||||
let isRefreshing = false;
|
||||
|
||||
if (url !== currentImgUrl) {
|
||||
currentImgUrl = url;
|
||||
isRefreshing = true;
|
||||
|
||||
if (url !== nowPlayingImageUrl) {
|
||||
if (url) {
|
||||
imageLoader.lazyImage(nowPlayingImageElement, url);
|
||||
nowPlayingImageUrl = url;
|
||||
imageLoader.lazyImage(nowPlayingImageElement, nowPlayingImageUrl);
|
||||
nowPlayingImageElement.style.display = null;
|
||||
nowPlayingTextElement.style.marginLeft = null;
|
||||
} else {
|
||||
nowPlayingImageUrl = null;
|
||||
nowPlayingImageElement.style.backgroundImage = '';
|
||||
nowPlayingImageElement.style.display = 'none';
|
||||
nowPlayingTextElement.style.marginLeft = '1em';
|
||||
|
@ -542,36 +539,34 @@ import { appRouter } from '../appRouter';
|
|||
}
|
||||
|
||||
if (nowPlayingItem.Id) {
|
||||
if (isRefreshing) {
|
||||
const apiClient = ServerConnections.getApiClient(nowPlayingItem.ServerId);
|
||||
apiClient.getItem(apiClient.getCurrentUserId(), nowPlayingItem.Id).then(function (item) {
|
||||
const userData = item.UserData || {};
|
||||
const likes = userData.Likes == null ? '' : userData.Likes;
|
||||
if (!layoutManager.mobile) {
|
||||
let contextButton = nowPlayingBarElement.querySelector('.btnToggleContextMenu');
|
||||
// We remove the previous event listener by replacing the item in each update event
|
||||
const contextButtonClone = contextButton.cloneNode(true);
|
||||
contextButton.parentNode.replaceChild(contextButtonClone, contextButton);
|
||||
contextButton = nowPlayingBarElement.querySelector('.btnToggleContextMenu');
|
||||
const options = {
|
||||
play: false,
|
||||
queue: false,
|
||||
stopPlayback: true,
|
||||
clearQueue: true,
|
||||
positionTo: contextButton
|
||||
};
|
||||
apiClient.getCurrentUser().then(function (user) {
|
||||
contextButton.addEventListener('click', function () {
|
||||
itemContextMenu.show(Object.assign({
|
||||
item: item,
|
||||
user: user
|
||||
}, options));
|
||||
});
|
||||
const apiClient = ServerConnections.getApiClient(nowPlayingItem.ServerId);
|
||||
apiClient.getItem(apiClient.getCurrentUserId(), nowPlayingItem.Id).then(function (item) {
|
||||
const userData = item.UserData || {};
|
||||
const likes = userData.Likes == null ? '' : userData.Likes;
|
||||
if (!layoutManager.mobile) {
|
||||
let contextButton = nowPlayingBarElement.querySelector('.btnToggleContextMenu');
|
||||
// We remove the previous event listener by replacing the item in each update event
|
||||
const contextButtonClone = contextButton.cloneNode(true);
|
||||
contextButton.parentNode.replaceChild(contextButtonClone, contextButton);
|
||||
contextButton = nowPlayingBarElement.querySelector('.btnToggleContextMenu');
|
||||
const options = {
|
||||
play: false,
|
||||
queue: false,
|
||||
stopPlayback: true,
|
||||
clearQueue: true,
|
||||
positionTo: contextButton
|
||||
};
|
||||
apiClient.getCurrentUser().then(function (user) {
|
||||
contextButton.addEventListener('click', function () {
|
||||
itemContextMenu.show(Object.assign({
|
||||
item: item,
|
||||
user: user
|
||||
}, options));
|
||||
});
|
||||
}
|
||||
nowPlayingUserData.innerHTML = '<button is="emby-ratingbutton" type="button" class="listItemButton mediaButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons favorite" aria-hidden="true"></span></button>';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
nowPlayingUserData.innerHTML = '<button is="emby-ratingbutton" type="button" class="listItemButton mediaButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons favorite" aria-hidden="true"></span></button>';
|
||||
});
|
||||
} else {
|
||||
nowPlayingUserData.innerHTML = '';
|
||||
}
|
||||
|
|
|
@ -238,6 +238,7 @@ function showWithUser(options, player, user) {
|
|||
|
||||
return actionsheet.show({
|
||||
items: menuItems,
|
||||
resolveOnClick: true,
|
||||
positionTo: options.positionTo
|
||||
}).then(function (id) {
|
||||
return handleSelectedOption(id, options, player);
|
||||
|
|
|
@ -34,7 +34,7 @@ class PluginManager {
|
|||
// translations won't be loaded for skins until needed
|
||||
return plugin;
|
||||
} else {
|
||||
return await this.#loadStrings(plugin);
|
||||
return this.#loadStrings(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
export default (view, params, { detail }) => {
|
||||
if (detail.options?.pageComponent) {
|
||||
// Fetch and render the page component to the view
|
||||
import(/* webpackChunkName: "[request]" */ `./pages/${detail.options.pageComponent}`)
|
||||
.then(({ default: component }) => {
|
||||
ReactDOM.render(React.createElement(component, params), view);
|
||||
});
|
||||
|
||||
// Unmount component when view is destroyed
|
||||
view.addEventListener('viewdestroy', () => {
|
||||
ReactDOM.unmountComponentAtNode(view);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -139,7 +139,7 @@ function updateNowPlayingInfo(context, state, serverId) {
|
|||
const displayName = item ? getNowPlayingNameHtml(item).replace('<br/>', ' - ') : '';
|
||||
if (item) {
|
||||
const nowPlayingServerId = (item.ServerId || serverId);
|
||||
if (item.Type == 'Audio' || item.MediaStreams[0].Type == 'Audio') {
|
||||
if (item.Type == 'AudioBook' || item.Type == 'Audio' || item.MediaStreams[0].Type == 'Audio') {
|
||||
let artistsSeries = '';
|
||||
let albumName = '';
|
||||
if (item.Artists != null) {
|
||||
|
|
|
@ -212,7 +212,10 @@
|
|||
height: 4.2em;
|
||||
right: 0;
|
||||
padding-left: 7.3%;
|
||||
padding-left: max(env(safe-area-inset-left), 7.3%);
|
||||
padding-right: 7.3%;
|
||||
padding-right: max(env(safe-area-inset-right), 7.3%);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.layout-desktop .playlistSectionButton,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import classNames from 'classnames';
|
||||
import { ApiClient } from 'jellyfin-apiclient';
|
||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import classNames from 'classnames';
|
||||
import { ApiClient } from 'jellyfin-apiclient';
|
||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
|
@ -33,6 +33,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
|||
const [ audioBooks, setAudioBooks ] = useState<BaseItemDto[]>([]);
|
||||
const [ books, setBooks ] = useState<BaseItemDto[]>([]);
|
||||
const [ people, setPeople ] = useState<BaseItemDto[]>([]);
|
||||
const [ collections, setCollections ] = useState<BaseItemDto[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const getDefaultParameters = () => ({
|
||||
|
@ -99,6 +100,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
|||
setAudioBooks([]);
|
||||
setBooks([]);
|
||||
setPeople([]);
|
||||
setCollections([]);
|
||||
|
||||
if (query) {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
@ -166,6 +168,9 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
|||
// Books row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Book' })
|
||||
.then(results => setBooks(results.Items || []));
|
||||
// Collections row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'BoxSet' })
|
||||
.then(result => setCollections(result.Items || []));
|
||||
}
|
||||
}
|
||||
}, [collectionType, parentId, query, serverId]);
|
||||
|
@ -257,6 +262,10 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
|||
title={globalize.translate('Books')}
|
||||
items={books}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('Collections')}
|
||||
items={collections}
|
||||
/>
|
||||
<SearchResultsRow
|
||||
title={globalize.translate('People')}
|
||||
items={people}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
||||
|
||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import escapeHtml from 'escape-html';
|
||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
|
||||
|
|
|
@ -13,9 +13,6 @@ import './style.scss';
|
|||
import 'material-design-icons-iconfont';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
// eslint-disable-next-line import/named, import/namespace
|
||||
import { Swiper } from 'swiper/swiper-bundle.esm';
|
||||
import 'swiper/swiper-bundle.css';
|
||||
import screenfull from 'screenfull';
|
||||
|
||||
/**
|
||||
|
@ -344,45 +341,51 @@ export default function (options) {
|
|||
slides = currentOptions.items;
|
||||
}
|
||||
|
||||
swiperInstance = new Swiper(dialog.querySelector('.slideshowSwiperContainer'), {
|
||||
direction: 'horizontal',
|
||||
// Loop is disabled due to the virtual slides option not supporting it.
|
||||
loop: false,
|
||||
zoom: {
|
||||
minRatio: 1,
|
||||
toggle: true
|
||||
},
|
||||
autoplay: !options.interactive || !!options.autoplay,
|
||||
keyboard: {
|
||||
enabled: true
|
||||
},
|
||||
preloadImages: true,
|
||||
slidesPerView: 1,
|
||||
slidesPerColumn: 1,
|
||||
initialSlide: options.startIndex || 0,
|
||||
speed: 240,
|
||||
navigation: {
|
||||
nextEl: '.btnSlideshowNext',
|
||||
prevEl: '.btnSlideshowPrevious'
|
||||
},
|
||||
// Virtual slides reduce memory consumption for large libraries while allowing preloading of images;
|
||||
virtual: {
|
||||
slides: slides,
|
||||
cache: true,
|
||||
renderSlide: getSwiperSlideHtml,
|
||||
addSlidesBefore: 1,
|
||||
addSlidesAfter: 1
|
||||
//eslint-disable-next-line import/no-unresolved
|
||||
import('swiper/css/bundle');
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import('swiper/bundle').then(({ Swiper }) => {
|
||||
swiperInstance = new Swiper(dialog.querySelector('.slideshowSwiperContainer'), {
|
||||
direction: 'horizontal',
|
||||
// Loop is disabled due to the virtual slides option not supporting it.
|
||||
loop: false,
|
||||
zoom: {
|
||||
minRatio: 1,
|
||||
toggle: true
|
||||
},
|
||||
autoplay: !options.interactive || !!options.autoplay,
|
||||
keyboard: {
|
||||
enabled: true
|
||||
},
|
||||
preloadImages: true,
|
||||
slidesPerView: 1,
|
||||
slidesPerColumn: 1,
|
||||
initialSlide: options.startIndex || 0,
|
||||
speed: 240,
|
||||
navigation: {
|
||||
nextEl: '.btnSlideshowNext',
|
||||
prevEl: '.btnSlideshowPrevious'
|
||||
},
|
||||
// Virtual slides reduce memory consumption for large libraries while allowing preloading of images;
|
||||
virtual: {
|
||||
slides: slides,
|
||||
cache: true,
|
||||
renderSlide: getSwiperSlideHtml,
|
||||
addSlidesBefore: 1,
|
||||
addSlidesAfter: 1
|
||||
}
|
||||
});
|
||||
|
||||
swiperInstance.on('autoplayStart', onAutoplayStart);
|
||||
swiperInstance.on('autoplayStop', onAutoplayStop);
|
||||
|
||||
if (useFakeZoomImage) {
|
||||
swiperInstance.on('zoomChange', onZoomChange);
|
||||
}
|
||||
|
||||
if (swiperInstance.autoplay?.running) onAutoplayStart();
|
||||
});
|
||||
|
||||
swiperInstance.on('autoplayStart', onAutoplayStart);
|
||||
swiperInstance.on('autoplayStop', onAutoplayStop);
|
||||
|
||||
if (useFakeZoomImage) {
|
||||
swiperInstance.on('zoomChange', onZoomChange);
|
||||
}
|
||||
|
||||
if (swiperInstance.autoplay?.running) onAutoplayStart();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${ButtonBack}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
|
||||
<h3 class="formDialogHeaderTitle">${Subtitles}</h3>
|
||||
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" data-autohide="true" class="button-link btnHelp flex align-items-center" href="https://docs.jellyfin.org/general/server/media/subtitles.html" target="_blank" style="margin-left:auto;margin-right:.5em;padding:.25em;" title="${Help}"><span class="material-icons info" aria-hidden="true"></span><span style="margin-left:.25em;">${Help}</span></a>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" data-autohide="true" class="button-link btnHelp flex align-items-center" href="https://jellyfin.org/docs/general/server/media/external-files.html" target="_blank" style="margin-left:auto;margin-right:.5em;padding:.25em;" title="${Help}"><span class="material-icons info" aria-hidden="true"></span><span style="margin-left:.25em;">${Help}</span></a>
|
||||
</div>
|
||||
<div class="formDialogContent smoothScrollY">
|
||||
<div class="dialogContentInner dialog-content-centered">
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
import { clearBackdrop } from '../backdrop/backdrop';
|
||||
import * as mainTabsManager from '../maintabsmanager';
|
||||
import layoutManager from '../layoutManager';
|
||||
import '../../elements/emby-tabs/emby-tabs';
|
||||
import LibraryMenu from '../../scripts/libraryMenu';
|
||||
|
||||
function onViewDestroy() {
|
||||
const tabControllers = this.tabControllers;
|
||||
|
||||
if (tabControllers) {
|
||||
tabControllers.forEach(function (t) {
|
||||
if (t.destroy) {
|
||||
t.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
this.tabControllers = null;
|
||||
}
|
||||
|
||||
this.view = null;
|
||||
this.params = null;
|
||||
this.currentTabController = null;
|
||||
this.initialTabIndex = null;
|
||||
}
|
||||
|
||||
class TabbedView {
|
||||
constructor(view, params) {
|
||||
this.tabControllers = [];
|
||||
this.view = view;
|
||||
this.params = params;
|
||||
|
||||
const self = this;
|
||||
|
||||
let currentTabIndex = parseInt(params.tab || this.getDefaultTabIndex(params.parentId));
|
||||
this.initialTabIndex = currentTabIndex;
|
||||
|
||||
function validateTabLoad(index) {
|
||||
return self.validateTabLoad ? self.validateTabLoad(index) : Promise.resolve();
|
||||
}
|
||||
|
||||
function loadTab(index, previousIndex) {
|
||||
validateTabLoad(index).then(function () {
|
||||
self.getTabController(index).then(function (controller) {
|
||||
const refresh = !controller.refreshed;
|
||||
|
||||
controller.onResume({
|
||||
autoFocus: previousIndex == null && layoutManager.tv,
|
||||
refresh: refresh
|
||||
});
|
||||
|
||||
controller.refreshed = true;
|
||||
|
||||
currentTabIndex = index;
|
||||
self.currentTabController = controller;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getTabContainers() {
|
||||
return view.querySelectorAll('.tabContent');
|
||||
}
|
||||
|
||||
function onTabChange(e) {
|
||||
const newIndex = parseInt(e.detail.selectedTabIndex);
|
||||
const previousIndex = e.detail.previousIndex;
|
||||
|
||||
const previousTabController = previousIndex == null ? null : self.tabControllers[previousIndex];
|
||||
if (previousTabController && previousTabController.onPause) {
|
||||
previousTabController.onPause();
|
||||
}
|
||||
|
||||
loadTab(newIndex, previousIndex);
|
||||
}
|
||||
|
||||
view.addEventListener('viewbeforehide', this.onPause.bind(this));
|
||||
|
||||
view.addEventListener('viewbeforeshow', function () {
|
||||
mainTabsManager.setTabs(view, currentTabIndex, self.getTabs, getTabContainers, null, onTabChange, false);
|
||||
});
|
||||
|
||||
view.addEventListener('viewshow', function (e) {
|
||||
self.onResume(e.detail);
|
||||
});
|
||||
|
||||
view.addEventListener('viewdestroy', onViewDestroy.bind(this));
|
||||
}
|
||||
|
||||
onResume() {
|
||||
this.setTitle();
|
||||
clearBackdrop();
|
||||
|
||||
const currentTabController = this.currentTabController;
|
||||
|
||||
if (!currentTabController) {
|
||||
mainTabsManager.selectedTabIndex(this.initialTabIndex);
|
||||
} else if (currentTabController && currentTabController.onResume) {
|
||||
currentTabController.onResume({});
|
||||
}
|
||||
}
|
||||
|
||||
onPause() {
|
||||
const currentTabController = this.currentTabController;
|
||||
|
||||
if (currentTabController && currentTabController.onPause) {
|
||||
currentTabController.onPause();
|
||||
}
|
||||
}
|
||||
|
||||
setTitle() {
|
||||
LibraryMenu.setTitle('');
|
||||
}
|
||||
}
|
||||
|
||||
export default TabbedView;
|
|
@ -3,7 +3,12 @@
|
|||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9999999;
|
||||
padding: 1em;
|
||||
padding-left: 1em;
|
||||
padding-left: max(env(safe-area-inset-left), 1em);
|
||||
padding-right: 1em;
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 1em);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
|
|
|
@ -21,9 +21,9 @@ viewContainer.setOnBeforeChange(function (newView, isRestored, options) {
|
|||
newView.initComplete = true;
|
||||
|
||||
if (typeof options.controllerFactory === 'function') {
|
||||
new options.controllerFactory(newView, eventDetail.detail.params, eventDetail);
|
||||
new options.controllerFactory(newView, eventDetail.detail.params);
|
||||
} else if (options.controllerFactory && typeof options.controllerFactory.default === 'function') {
|
||||
new options.controllerFactory.default(newView, eventDetail.detail.params, eventDetail);
|
||||
new options.controllerFactory.default(newView, eventDetail.detail.params);
|
||||
}
|
||||
|
||||
if (!options.controllerFactory || dispatchPageEvents) {
|
||||
|
|
|
@ -15,7 +15,6 @@ import alert from '../../components/alert';
|
|||
page.querySelector('#txtServerName').value = systemInfo.ServerName;
|
||||
page.querySelector('#txtCachePath').value = systemInfo.CachePath || '';
|
||||
page.querySelector('#chkQuickConnectAvailable').checked = config.QuickConnectAvailable === true;
|
||||
page.querySelector('#chkSplashScreenAvailable').checked = config.SplashscreenEnabled === true;
|
||||
$('#txtMetadataPath', page).val(systemInfo.InternalMetadataPath || '');
|
||||
$('#txtMetadataNetworkPath', page).val(systemInfo.MetadataNetworkPath || '');
|
||||
$('#selectLocalizationLanguage', page).html(languageOptions.map(function (language) {
|
||||
|
@ -108,6 +107,7 @@ import alert from '../../components/alert';
|
|||
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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ import alert from '../../components/alert';
|
|||
|
||||
/* eslint-disable indent */
|
||||
|
||||
function onSubmit() {
|
||||
function onSubmit(event) {
|
||||
event.preventDefault();
|
||||
loading.show();
|
||||
const form = this;
|
||||
ApiClient.getServerConfiguration().then(function (config) {
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<div id="editUserPage" data-role="page" class="page type-interior">
|
||||
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
<div id="userLibraryAccessPage" data-role="page" class="page type-interior">
|
||||
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
<div id="newUserPage" data-role="page" class="page type-interior">
|
||||
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
<div id="userParentalControlPage" data-role="page" class="page type-interior">
|
||||
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
<div id="userPasswordPage" data-role="page" class="page type-interior userPasswordPage">
|
||||
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
<div id="userProfilesPage" data-role="page" class="page type-interior userProfilesPage fullWidthContent">
|
||||
|
||||
</div>
|
|
@ -1,9 +0,0 @@
|
|||
<div id="indexPage" style="outline: none;" data-role="page" data-dom-cache="true" class="page homePage libraryPage allLibraryPage backdropPage pageWithAbsoluteTabs withTabs" data-backdroptype="movie,series,book">
|
||||
|
||||
<div class="tabContent pageTabContent" id="homeTab" data-index="0">
|
||||
<div class="sections"></div>
|
||||
</div>
|
||||
<div class="tabContent pageTabContent" id="favoritesTab" data-index="1">
|
||||
<div class="sections"></div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,69 +0,0 @@
|
|||
import TabbedView from '../components/tabbedview/tabbedview';
|
||||
import globalize from '../scripts/globalize';
|
||||
import '../elements/emby-tabs/emby-tabs';
|
||||
import '../elements/emby-button/emby-button';
|
||||
import '../elements/emby-scroller/emby-scroller';
|
||||
import LibraryMenu from '../scripts/libraryMenu';
|
||||
|
||||
class HomeView extends TabbedView {
|
||||
constructor(view, params) {
|
||||
super(view, params);
|
||||
}
|
||||
|
||||
setTitle() {
|
||||
LibraryMenu.setTitle(null);
|
||||
}
|
||||
|
||||
onPause() {
|
||||
super.onPause(this);
|
||||
document.querySelector('.skinHeader').classList.remove('noHomeButtonHeader');
|
||||
}
|
||||
|
||||
onResume(options) {
|
||||
super.onResume(this, options);
|
||||
document.querySelector('.skinHeader').classList.add('noHomeButtonHeader');
|
||||
}
|
||||
|
||||
getDefaultTabIndex() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
getTabs() {
|
||||
return [{
|
||||
name: globalize.translate('Home')
|
||||
}, {
|
||||
name: globalize.translate('Favorites')
|
||||
}];
|
||||
}
|
||||
|
||||
getTabController(index) {
|
||||
if (index == null) {
|
||||
throw new Error('index cannot be null');
|
||||
}
|
||||
|
||||
let depends = '';
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
depends = 'hometab';
|
||||
break;
|
||||
|
||||
case 1:
|
||||
depends = 'favorites';
|
||||
}
|
||||
|
||||
const instance = this;
|
||||
return import(/* webpackChunkName: "[request]" */ `../controllers/${depends}`).then(({ default: controllerFactory }) => {
|
||||
let controller = instance.tabControllers[index];
|
||||
|
||||
if (!controller) {
|
||||
controller = new controllerFactory(instance.view.querySelector(".tabContent[data-index='" + index + "']"), instance.params);
|
||||
instance.tabControllers[index] = controller;
|
||||
}
|
||||
|
||||
return controller;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default HomeView;
|
|
@ -1985,7 +1985,9 @@ export default function (view, params) {
|
|||
download([{
|
||||
url: downloadHref,
|
||||
itemId: currentItem.Id,
|
||||
serverId: currentItem.serverId
|
||||
serverId: currentItem.ServerId,
|
||||
title: currentItem.Name,
|
||||
filename: currentItem.Path.replace(/^.*[\\/]/, '')
|
||||
}]);
|
||||
}
|
||||
|
||||
|
|
|
@ -62,6 +62,14 @@
|
|||
<div class="fieldDescription checkboxFieldDescription">${EnableStreamLoopingHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldIgnoreDts hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkIgnoreDts" checked />
|
||||
<span>${IgnoreDts}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${IgnoreDtsHelp}</div>
|
||||
</div>
|
||||
|
||||
<p class="drmMessage hide">${DrmChannelsNotImported}</p>
|
||||
<br />
|
||||
<input type="hidden" class="fldDeviceId" />
|
||||
|
|
|
@ -61,6 +61,7 @@ function fillTunerHostInfo(view, info) {
|
|||
view.querySelector('.chkFavorite').checked = info.ImportFavoritesOnly;
|
||||
view.querySelector('.chkTranscode').checked = info.AllowHWTranscoding;
|
||||
view.querySelector('.chkStreamLoop').checked = info.EnableStreamLooping;
|
||||
view.querySelector('.chkIgnoreDts').checked = info.IgnoreDts;
|
||||
view.querySelector('.txtTunerCount').value = info.TunerCount || '0';
|
||||
}
|
||||
|
||||
|
@ -75,7 +76,8 @@ function submitForm(page) {
|
|||
TunerCount: page.querySelector('.txtTunerCount').value || 0,
|
||||
ImportFavoritesOnly: page.querySelector('.chkFavorite').checked,
|
||||
AllowHWTranscoding: page.querySelector('.chkTranscode').checked,
|
||||
EnableStreamLooping: page.querySelector('.chkStreamLoop').checked
|
||||
EnableStreamLooping: page.querySelector('.chkStreamLoop').checked,
|
||||
IgnoreDts: page.querySelector('.chkIgnoreDts').checked
|
||||
};
|
||||
|
||||
if (isM3uVariant(info.Type)) {
|
||||
|
@ -120,6 +122,7 @@ function onTypeChange() {
|
|||
const supportsTunerIpAddress = value === 'hdhomerun';
|
||||
const supportsTunerFileOrUrl = value === 'm3u';
|
||||
const supportsStreamLooping = value === 'm3u';
|
||||
const supportsIgnoreDts = value === 'm3u';
|
||||
const supportsTunerCount = value === 'm3u';
|
||||
const supportsUserAgent = value === 'm3u';
|
||||
const suppportsSubmit = value !== 'other';
|
||||
|
@ -168,6 +171,12 @@ function onTypeChange() {
|
|||
view.querySelector('.fldStreamLoop').classList.add('hide');
|
||||
}
|
||||
|
||||
if (supportsIgnoreDts) {
|
||||
view.querySelector('.fldIgnoreDts').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldIgnoreDts').classList.add('hide');
|
||||
}
|
||||
|
||||
if (supportsTunerCount) {
|
||||
view.querySelector('.fldTunerCount').classList.remove('hide');
|
||||
view.querySelector('.txtTunerCount').setAttribute('required', 'required');
|
||||
|
|
|
@ -92,6 +92,7 @@
|
|||
<div class="pageTabContent" id="songsTab" data-index="5">
|
||||
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
|
||||
<div class="paging"></div>
|
||||
<button is="paper-icon-button-light" class="btnShuffle autoSize" title="${Shuffle}"><span class="material-icons shuffle" aria-hidden="true"></span></button>
|
||||
<button is="paper-icon-button-light" class="btnSort autoSize" title="${Sort}"><span class="material-icons sort_by_alpha" aria-hidden="true"></span></button>
|
||||
<button is="paper-icon-button-light" class="btnFilter autoSize" title="${Filter}"><span class="material-icons filter_list" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
|
|
|
@ -8,197 +8,207 @@ import * as userSettings from '../../scripts/settings/userSettings';
|
|||
import globalize from '../../scripts/globalize';
|
||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import {playbackManager} from '../../components/playback/playbackmanager';
|
||||
|
||||
/* eslint-disable indent */
|
||||
export default function (view, params, tabContent) {
|
||||
function getPageData(context) {
|
||||
const key = getSavedQueryKey(context);
|
||||
let pageData = data[key];
|
||||
|
||||
export default function (view, params, tabContent) {
|
||||
function getPageData(context) {
|
||||
const key = getSavedQueryKey(context);
|
||||
let pageData = data[key];
|
||||
|
||||
if (!pageData) {
|
||||
pageData = data[key] = {
|
||||
query: {
|
||||
SortBy: 'Album,SortName',
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'Audio',
|
||||
Recursive: true,
|
||||
Fields: 'AudioInfo,ParentId',
|
||||
StartIndex: 0,
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary'
|
||||
}
|
||||
};
|
||||
|
||||
if (userSettings.libraryPageSize() > 0) {
|
||||
pageData.query['Limit'] = userSettings.libraryPageSize();
|
||||
if (!pageData) {
|
||||
pageData = data[key] = {
|
||||
query: {
|
||||
SortBy: 'Album,SortName',
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'Audio',
|
||||
Recursive: true,
|
||||
Fields: 'AudioInfo,ParentId',
|
||||
StartIndex: 0,
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary'
|
||||
}
|
||||
};
|
||||
|
||||
pageData.query.ParentId = params.topParentId;
|
||||
libraryBrowser.loadSavedQueryValues(key, pageData.query);
|
||||
if (userSettings.libraryPageSize() > 0) {
|
||||
pageData.query['Limit'] = userSettings.libraryPageSize();
|
||||
}
|
||||
|
||||
return pageData;
|
||||
pageData.query.ParentId = params.topParentId;
|
||||
libraryBrowser.loadSavedQueryValues(key, pageData.query);
|
||||
}
|
||||
|
||||
function getQuery(context) {
|
||||
return getPageData(context).query;
|
||||
}
|
||||
|
||||
function getSavedQueryKey(context) {
|
||||
if (!context.savedQueryKey) {
|
||||
context.savedQueryKey = libraryBrowser.getSavedQueryKey('songs');
|
||||
}
|
||||
|
||||
return context.savedQueryKey;
|
||||
}
|
||||
|
||||
function reloadItems(page) {
|
||||
loading.show();
|
||||
isLoading = true;
|
||||
const query = getQuery(page);
|
||||
ApiClient.getItems(Dashboard.getCurrentUserId(), query).then(function (result) {
|
||||
function onNextPageClick() {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (userSettings.libraryPageSize() > 0) {
|
||||
query.StartIndex += query.Limit;
|
||||
}
|
||||
reloadItems(tabContent);
|
||||
}
|
||||
|
||||
function onPreviousPageClick() {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (userSettings.libraryPageSize() > 0) {
|
||||
query.StartIndex = Math.max(0, query.StartIndex - query.Limit);
|
||||
}
|
||||
reloadItems(tabContent);
|
||||
}
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
const pagingHtml = libraryBrowser.getQueryPagingHtml({
|
||||
startIndex: query.StartIndex,
|
||||
limit: query.Limit,
|
||||
totalRecordCount: result.TotalRecordCount,
|
||||
showLimit: false,
|
||||
updatePageSizeSetting: false,
|
||||
addLayoutButton: false,
|
||||
sortButton: false,
|
||||
filterButton: false
|
||||
});
|
||||
const html = listView.getListViewHtml({
|
||||
items: result.Items,
|
||||
action: 'playallfromhere',
|
||||
smallIcon: true,
|
||||
artist: true,
|
||||
addToListButton: true
|
||||
});
|
||||
let elems = tabContent.querySelectorAll('.paging');
|
||||
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
elems[i].innerHTML = pagingHtml;
|
||||
}
|
||||
|
||||
elems = tabContent.querySelectorAll('.btnNextPage');
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
elems[i].addEventListener('click', onNextPageClick);
|
||||
}
|
||||
|
||||
elems = tabContent.querySelectorAll('.btnPreviousPage');
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
elems[i].addEventListener('click', onPreviousPageClick);
|
||||
}
|
||||
|
||||
const itemsContainer = tabContent.querySelector('.itemsContainer');
|
||||
itemsContainer.innerHTML = html;
|
||||
imageLoader.lazyChildren(itemsContainer);
|
||||
libraryBrowser.saveQueryValues(getSavedQueryKey(page), query);
|
||||
loading.hide();
|
||||
isLoading = false;
|
||||
|
||||
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const data = {};
|
||||
let isLoading = false;
|
||||
|
||||
self.showFilterMenu = function () {
|
||||
import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => {
|
||||
const filterDialog = new filterDialogFactory({
|
||||
query: getQuery(tabContent),
|
||||
mode: 'songs',
|
||||
serverId: ApiClient.serverId()
|
||||
});
|
||||
Events.on(filterDialog, 'filterchange', function () {
|
||||
getQuery(tabContent).StartIndex = 0;
|
||||
reloadItems(tabContent);
|
||||
});
|
||||
filterDialog.show();
|
||||
});
|
||||
};
|
||||
|
||||
self.getCurrentViewStyle = function () {
|
||||
return getPageData(tabContent).view;
|
||||
};
|
||||
|
||||
function initPage(tabContent) {
|
||||
tabContent.querySelector('.btnFilter').addEventListener('click', function () {
|
||||
self.showFilterMenu();
|
||||
});
|
||||
tabContent.querySelector('.btnSort').addEventListener('click', function (e) {
|
||||
libraryBrowser.showSortMenu({
|
||||
items: [{
|
||||
name: globalize.translate('OptionTrackName'),
|
||||
id: 'Name'
|
||||
}, {
|
||||
name: globalize.translate('Album'),
|
||||
id: 'Album,SortName'
|
||||
}, {
|
||||
name: globalize.translate('AlbumArtist'),
|
||||
id: 'AlbumArtist,Album,SortName'
|
||||
}, {
|
||||
name: globalize.translate('Artist'),
|
||||
id: 'Artist,Album,SortName'
|
||||
}, {
|
||||
name: globalize.translate('OptionDateAdded'),
|
||||
id: 'DateCreated,SortName'
|
||||
}, {
|
||||
name: globalize.translate('OptionDatePlayed'),
|
||||
id: 'DatePlayed,SortName'
|
||||
}, {
|
||||
name: globalize.translate('OptionPlayCount'),
|
||||
id: 'PlayCount,SortName'
|
||||
}, {
|
||||
name: globalize.translate('OptionReleaseDate'),
|
||||
id: 'PremiereDate,AlbumArtist,Album,SortName'
|
||||
}, {
|
||||
name: globalize.translate('Runtime'),
|
||||
id: 'Runtime,AlbumArtist,Album,SortName'
|
||||
}],
|
||||
callback: function () {
|
||||
getQuery(tabContent).StartIndex = 0;
|
||||
reloadItems(tabContent);
|
||||
},
|
||||
query: getQuery(tabContent),
|
||||
button: e.target
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initPage(tabContent);
|
||||
|
||||
self.renderTab = function () {
|
||||
reloadItems(tabContent);
|
||||
};
|
||||
return pageData;
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
function getQuery(context) {
|
||||
return getPageData(context).query;
|
||||
}
|
||||
|
||||
function getSavedQueryKey(context) {
|
||||
if (!context.savedQueryKey) {
|
||||
context.savedQueryKey = libraryBrowser.getSavedQueryKey('songs');
|
||||
}
|
||||
|
||||
return context.savedQueryKey;
|
||||
}
|
||||
|
||||
function reloadItems(page) {
|
||||
loading.show();
|
||||
isLoading = true;
|
||||
const query = getQuery(page);
|
||||
ApiClient.getItems(Dashboard.getCurrentUserId(), query).then(function (result) {
|
||||
function onNextPageClick() {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (userSettings.libraryPageSize() > 0) {
|
||||
query.StartIndex += query.Limit;
|
||||
}
|
||||
reloadItems(tabContent);
|
||||
}
|
||||
|
||||
function onPreviousPageClick() {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (userSettings.libraryPageSize() > 0) {
|
||||
query.StartIndex = Math.max(0, query.StartIndex - query.Limit);
|
||||
}
|
||||
reloadItems(tabContent);
|
||||
}
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
const pagingHtml = libraryBrowser.getQueryPagingHtml({
|
||||
startIndex: query.StartIndex,
|
||||
limit: query.Limit,
|
||||
totalRecordCount: result.TotalRecordCount,
|
||||
showLimit: false,
|
||||
updatePageSizeSetting: false,
|
||||
addLayoutButton: false,
|
||||
sortButton: false,
|
||||
filterButton: false
|
||||
});
|
||||
const html = listView.getListViewHtml({
|
||||
items: result.Items,
|
||||
action: 'playallfromhere',
|
||||
smallIcon: true,
|
||||
artist: true,
|
||||
addToListButton: true
|
||||
});
|
||||
let elems = tabContent.querySelectorAll('.paging');
|
||||
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
elems[i].innerHTML = pagingHtml;
|
||||
}
|
||||
|
||||
elems = tabContent.querySelectorAll('.btnNextPage');
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
elems[i].addEventListener('click', onNextPageClick);
|
||||
}
|
||||
|
||||
elems = tabContent.querySelectorAll('.btnPreviousPage');
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
elems[i].addEventListener('click', onPreviousPageClick);
|
||||
}
|
||||
|
||||
const itemsContainer = tabContent.querySelector('.itemsContainer');
|
||||
itemsContainer.innerHTML = html;
|
||||
imageLoader.lazyChildren(itemsContainer);
|
||||
libraryBrowser.saveQueryValues(getSavedQueryKey(page), query);
|
||||
|
||||
tabContent.querySelector('.btnShuffle').classList.toggle('hide', result.TotalRecordCount < 1);
|
||||
|
||||
loading.hide();
|
||||
isLoading = false;
|
||||
|
||||
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const data = {};
|
||||
let isLoading = false;
|
||||
|
||||
self.showFilterMenu = function () {
|
||||
import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => {
|
||||
const filterDialog = new filterDialogFactory({
|
||||
query: getQuery(tabContent),
|
||||
mode: 'songs',
|
||||
serverId: ApiClient.serverId()
|
||||
});
|
||||
Events.on(filterDialog, 'filterchange', function () {
|
||||
getQuery(tabContent).StartIndex = 0;
|
||||
reloadItems(tabContent);
|
||||
});
|
||||
filterDialog.show();
|
||||
});
|
||||
};
|
||||
|
||||
function shuffle() {
|
||||
ApiClient.getItem(ApiClient.getCurrentUserId(), params.topParentId).then(function (item) {
|
||||
playbackManager.shuffle(item);
|
||||
});
|
||||
}
|
||||
|
||||
self.getCurrentViewStyle = function () {
|
||||
return getPageData(tabContent).view;
|
||||
};
|
||||
|
||||
function initPage(tabContent) {
|
||||
tabContent.querySelector('.btnFilter').addEventListener('click', function () {
|
||||
self.showFilterMenu();
|
||||
});
|
||||
tabContent.querySelector('.btnSort').addEventListener('click', function (e) {
|
||||
libraryBrowser.showSortMenu({
|
||||
items: [{
|
||||
name: globalize.translate('OptionTrackName'),
|
||||
id: 'Name'
|
||||
}, {
|
||||
name: globalize.translate('Album'),
|
||||
id: 'Album,SortName'
|
||||
}, {
|
||||
name: globalize.translate('AlbumArtist'),
|
||||
id: 'AlbumArtist,Album,SortName'
|
||||
}, {
|
||||
name: globalize.translate('Artist'),
|
||||
id: 'Artist,Album,SortName'
|
||||
}, {
|
||||
name: globalize.translate('OptionDateAdded'),
|
||||
id: 'DateCreated,SortName'
|
||||
}, {
|
||||
name: globalize.translate('OptionDatePlayed'),
|
||||
id: 'DatePlayed,SortName'
|
||||
}, {
|
||||
name: globalize.translate('OptionPlayCount'),
|
||||
id: 'PlayCount,SortName'
|
||||
}, {
|
||||
name: globalize.translate('OptionReleaseDate'),
|
||||
id: 'PremiereDate,AlbumArtist,Album,SortName'
|
||||
}, {
|
||||
name: globalize.translate('Runtime'),
|
||||
id: 'Runtime,AlbumArtist,Album,SortName'
|
||||
}, {
|
||||
name: globalize.translate('OptionRandom'),
|
||||
id: 'Random,SortName'
|
||||
}],
|
||||
callback: function () {
|
||||
getQuery(tabContent).StartIndex = 0;
|
||||
reloadItems(tabContent);
|
||||
},
|
||||
query: getQuery(tabContent),
|
||||
button: e.target
|
||||
});
|
||||
});
|
||||
tabContent.querySelector('.btnShuffle').addEventListener('click', shuffle);
|
||||
}
|
||||
|
||||
initPage(tabContent);
|
||||
|
||||
self.renderTab = function () {
|
||||
reloadItems(tabContent);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -259,8 +259,11 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
|||
name: globalize.translate('OptionImdbRating'),
|
||||
id: 'CommunityRating,SortName'
|
||||
}, {
|
||||
name: globalize.translate('OptionDateAdded'),
|
||||
name: globalize.translate('OptionDateShowAdded'),
|
||||
id: 'DateCreated,SortName'
|
||||
}, {
|
||||
name: globalize.translate('OptionDateEpisodeAdded'),
|
||||
id: 'DateLastContentAdded,SortName'
|
||||
}, {
|
||||
name: globalize.translate('OptionDatePlayed'),
|
||||
id: 'SeriesDatePlayed,SortName'
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="readOnlyContent" style="margin: 0 auto;">
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<h2 class="sectionTitle headerUsername" style="padding-left:.25em;"></h2>
|
||||
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="lnkMyProfile listItem-border">
|
||||
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="lnkUserProfile listItem-border">
|
||||
<div class="listItem">
|
||||
<span class="material-icons listItemIcon listItemIcon-transparent person" aria-hidden="true"></span>
|
||||
<div class="listItemBody">
|
||||
|
|
|
@ -26,7 +26,7 @@ export default function (view, params) {
|
|||
const userId = params.userId || Dashboard.getCurrentUserId();
|
||||
const page = this;
|
||||
|
||||
page.querySelector('.lnkMyProfile').setAttribute('href', '#/myprofile.html?userId=' + userId);
|
||||
page.querySelector('.lnkUserProfile').setAttribute('href', '#/userprofile.html?userId=' + userId);
|
||||
page.querySelector('.lnkDisplayPreferences').setAttribute('href', '#/mypreferencesdisplay.html?userId=' + userId);
|
||||
page.querySelector('.lnkHomePreferences').setAttribute('href', '#/mypreferenceshome.html?userId=' + userId);
|
||||
page.querySelector('.lnkPlaybackPreferences').setAttribute('href', '#/mypreferencesplayback.html?userId=' + userId);
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<div id="userProfilePage" data-role="page" class="page libraryPage userPreferencesPage userPasswordPage noSecondaryNavPage" data-title="${Profile}" data-menubutton="false">
|
||||
|
||||
</div>
|
41
src/elements/ButtonElement.tsx
Normal file
41
src/elements/ButtonElement.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../scripts/globalize';
|
||||
|
||||
const createButtonElement = ({ type, id, className, title, leftIcon, rightIcon }: IProps) => ({
|
||||
__html: `<button
|
||||
is="emby-button"
|
||||
type="${type}"
|
||||
${id}
|
||||
class="${className}"
|
||||
>
|
||||
${leftIcon}
|
||||
<span>${title}</span>
|
||||
${rightIcon}
|
||||
</button>`
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
type?: string;
|
||||
id?: string;
|
||||
className?: string;
|
||||
title?: string;
|
||||
leftIcon?: string;
|
||||
rightIcon?: string;
|
||||
}
|
||||
|
||||
const ButtonElement: FunctionComponent<IProps> = ({ type, id, className, title, leftIcon, rightIcon }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={createButtonElement({
|
||||
type: type,
|
||||
id: id ? `id="${id}"` : '',
|
||||
className: className,
|
||||
title: globalize.translate(title),
|
||||
leftIcon: leftIcon ? `<span class="material-icons ${leftIcon}" aria-hidden="true"></span>` : '',
|
||||
rightIcon: rightIcon ? `<span class="material-icons ${rightIcon}" aria-hidden="true"></span>` : ''
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonElement;
|
57
src/elements/CheckBoxElement.tsx
Normal file
57
src/elements/CheckBoxElement.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import escapeHTML from 'escape-html';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../scripts/globalize';
|
||||
|
||||
const createCheckBoxElement = ({ labelClassName, className, id, dataFilter, dataItemType, dataId, checkedAttribute, renderContent }: { labelClassName?: string, type?: string, className?: string, id?: string, dataFilter?: string, dataItemType?: string, dataId?: string, checkedAttribute?: string, renderContent?: string }) => ({
|
||||
__html: `<label ${labelClassName}>
|
||||
<input
|
||||
is="emby-checkbox"
|
||||
type="checkbox"
|
||||
class="${className}"
|
||||
${id}
|
||||
${dataFilter}
|
||||
${dataItemType}
|
||||
${dataId}
|
||||
${checkedAttribute}
|
||||
/>
|
||||
${renderContent}
|
||||
</label>`
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
labelClassName?: string;
|
||||
className?: string;
|
||||
elementId?: string;
|
||||
dataFilter?: string;
|
||||
itemType?: string;
|
||||
itemId?: string;
|
||||
itemAppName?: string;
|
||||
itemCheckedAttribute?: string;
|
||||
itemName?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
const CheckBoxElement: FunctionComponent<IProps> = ({ labelClassName, className, elementId, dataFilter, itemType, itemId, itemAppName, itemCheckedAttribute, itemName, title }: IProps) => {
|
||||
const appName = itemAppName ? `- ${itemAppName}` : '';
|
||||
const renderContent = itemName ?
|
||||
`<span>${escapeHTML(itemName || '')} ${appName}</span>` :
|
||||
`<span>${globalize.translate(title)}</span>`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className='sectioncheckbox'
|
||||
dangerouslySetInnerHTML={createCheckBoxElement({
|
||||
labelClassName: labelClassName ? `class='${labelClassName}'` : '',
|
||||
className: className,
|
||||
id: elementId ? `id='${elementId}'` : '',
|
||||
dataFilter: dataFilter ? `data-filter='${dataFilter}'` : '',
|
||||
dataItemType: itemType ? `data-itemtype='${itemType}'` : '',
|
||||
dataId: itemId ? `data-id='${itemId}'` : '',
|
||||
checkedAttribute: itemCheckedAttribute ? itemCheckedAttribute : '',
|
||||
renderContent: renderContent
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckBoxElement;
|
47
src/elements/IconButtonElement.tsx
Normal file
47
src/elements/IconButtonElement.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../scripts/globalize';
|
||||
|
||||
type IProps = {
|
||||
is?: string;
|
||||
id?: string;
|
||||
title?: string;
|
||||
className?: string;
|
||||
icon?: string,
|
||||
dataIndex?: string | number;
|
||||
dataTag?: string | number;
|
||||
dataProfileid?: string | number;
|
||||
}
|
||||
|
||||
const createIconButtonElement = ({ is, id, className, title, icon, dataIndex, dataTag, dataProfileid }: IProps) => ({
|
||||
__html: `<button
|
||||
is="${is}"
|
||||
type="button"
|
||||
${id}
|
||||
class="${className}"
|
||||
${title}
|
||||
${dataIndex}
|
||||
${dataTag}
|
||||
${dataProfileid}
|
||||
>
|
||||
<span class="material-icons ${icon}" aria-hidden="true"></span>
|
||||
</button>`
|
||||
});
|
||||
|
||||
const IconButtonElement: FunctionComponent<IProps> = ({ is, id, className, title, icon, dataIndex, dataTag, dataProfileid }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={createIconButtonElement({
|
||||
is: is,
|
||||
id: id ? `id="${id}"` : '',
|
||||
className: className,
|
||||
title: title ? `title="${globalize.translate(title)}"` : '',
|
||||
icon: icon,
|
||||
dataIndex: dataIndex ? `data-index="${dataIndex}"` : '',
|
||||
dataTag: dataTag ? `data-tag="${dataTag}"` : '',
|
||||
dataProfileid: dataProfileid ? `data-profileid="${dataProfileid}"` : ''
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconButtonElement;
|
|
@ -1,5 +1,5 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import globalize from '../scripts/globalize';
|
||||
|
||||
const createInputElement = ({ type, id, label, options }: { type?: string, id?: string, label?: string, options?: string }) => ({
|
||||
__html: `<input
|
41
src/elements/SectionTitleContainer.tsx
Normal file
41
src/elements/SectionTitleContainer.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import IconButtonElement from './IconButtonElement';
|
||||
import SectionTitleLinkElement from './SectionTitleLinkElement';
|
||||
|
||||
type IProps = {
|
||||
SectionClassName?: string;
|
||||
title?: string;
|
||||
isBtnVisible?: boolean;
|
||||
btnId?: string;
|
||||
btnClassName?: string;
|
||||
btnTitle?: string;
|
||||
btnIcon?: string;
|
||||
isLinkVisible?: boolean;
|
||||
url?: string;
|
||||
}
|
||||
const SectionTitleContainer: FunctionComponent<IProps> = ({SectionClassName, title, isBtnVisible = false, btnId, btnClassName, btnTitle, btnIcon, isLinkVisible = true, url}: IProps) => {
|
||||
return (
|
||||
<div className={`${SectionClassName} sectionTitleContainer flex align-items-center`}>
|
||||
<h2 className='sectionTitle'>
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
{isBtnVisible && <IconButtonElement
|
||||
is='emby-button'
|
||||
id={btnId}
|
||||
className={btnClassName}
|
||||
title={btnTitle}
|
||||
icon={btnIcon}
|
||||
/>}
|
||||
|
||||
{isLinkVisible && <SectionTitleLinkElement
|
||||
className='raised button-alt headerHelpButton'
|
||||
title='Help'
|
||||
url={url}
|
||||
/>}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionTitleContainer;
|
|
@ -1,5 +1,5 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import globalize from '../scripts/globalize';
|
||||
|
||||
const createLinkElement = ({ className, title, href }: { className?: string, title?: string, href?: string }) => ({
|
||||
__html: `<a
|
38
src/elements/SelectElement.tsx
Normal file
38
src/elements/SelectElement.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../scripts/globalize';
|
||||
|
||||
const createSelectElement = ({ name, id, required, label, option }: { name?: string, id?: string, required?: string, label?: string, option?: React.ReactNode }) => ({
|
||||
__html: `<select
|
||||
is="emby-select"
|
||||
${name}
|
||||
id="${id}"
|
||||
${required}
|
||||
label="${label}"
|
||||
>
|
||||
${option}
|
||||
</select>`
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
name?: string;
|
||||
id?: string;
|
||||
required?: string;
|
||||
label?: string;
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const SelectElement: FunctionComponent<IProps> = ({ name, id, required, label, children }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={createSelectElement({
|
||||
name: name ? `name='${name}'` : '',
|
||||
id: id,
|
||||
required: required ? `required='${required}'` : '',
|
||||
label: globalize.translate(label),
|
||||
option: children
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectElement;
|
|
@ -50,6 +50,9 @@ const EmbyScrollButtonsPrototype = Object.create(HTMLDivElement.prototype);
|
|||
if (scrollWidth <= scrollSize + 20) {
|
||||
scrollButtons.scrollButtonsLeft.classList.add('hide');
|
||||
scrollButtons.scrollButtonsRight.classList.add('hide');
|
||||
} else {
|
||||
scrollButtons.scrollButtonsLeft.classList.remove('hide');
|
||||
scrollButtons.scrollButtonsRight.classList.remove('hide');
|
||||
}
|
||||
|
||||
if (localeAwarePos > 0) {
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
|
||||
.emby-scroller {
|
||||
margin-left: 3.3%;
|
||||
margin-left: max(env(safe-area-inset-left), 3.3%);
|
||||
margin-right: 3.3%;
|
||||
margin-right: max(env(safe-area-inset-right), 3.3%);
|
||||
}
|
||||
|
||||
/* align first card in scroller to heading */
|
||||
|
@ -28,7 +30,9 @@
|
|||
.layout-tv .emby-scroller,
|
||||
.layout-mobile .emby-scroller {
|
||||
padding-left: 3.3%;
|
||||
padding-left: max(env(safe-area-inset-left), 3.3%);
|
||||
padding-right: 3.3%;
|
||||
padding-right: max(env(safe-area-inset-right), 3.3%);
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
|
|
@ -75,13 +75,27 @@
|
|||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.mouseIdle,
|
||||
.mouseIdle button,
|
||||
.mouseIdle select,
|
||||
.mouseIdle input,
|
||||
.mouseIdle textarea,
|
||||
.mouseIdle a,
|
||||
.mouseIdle label {
|
||||
.layout-tv .mouseIdle,
|
||||
.layout-tv .mouseIdle button,
|
||||
.layout-tv .mouseIdle select,
|
||||
.layout-tv .mouseIdle input,
|
||||
.layout-tv .mouseIdle textarea,
|
||||
.layout-tv .mouseIdle a,
|
||||
.layout-tv .mouseIdle label,
|
||||
.transparentDocument .mouseIdle,
|
||||
.transparentDocument .mouseIdle button,
|
||||
.transparentDocument .mouseIdle select,
|
||||
.transparentDocument .mouseIdle input,
|
||||
.transparentDocument .mouseIdle textarea,
|
||||
.transparentDocument .mouseIdle a,
|
||||
.transparentDocument .mouseIdle label,
|
||||
.screensaver-noScroll.mouseIdle,
|
||||
.screensaver-noScroll.mouseIdle button,
|
||||
.screensaver-noScroll.mouseIdle select,
|
||||
.screensaver-noScroll.mouseIdle input,
|
||||
.screensaver-noScroll.mouseIdle textarea,
|
||||
.screensaver-noScroll.mouseIdle a,
|
||||
.screensaver-noScroll.mouseIdle label {
|
||||
cursor: none !important;
|
||||
}
|
||||
|
||||
|
@ -101,6 +115,7 @@
|
|||
bottom: 0;
|
||||
z-index: 1;
|
||||
width: 0.8em;
|
||||
padding-left: env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
[dir="ltr"] .mainDrawerHandle {
|
||||
|
|
|
@ -6,44 +6,44 @@ import 'intersection-observer';
|
|||
import 'classlist.js';
|
||||
import 'whatwg-fetch';
|
||||
import 'resize-observer-polyfill';
|
||||
import '../assets/css/site.scss';
|
||||
import './assets/css/site.scss';
|
||||
import React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { Events } from 'jellyfin-apiclient';
|
||||
import ServerConnections from '../components/ServerConnections';
|
||||
import globalize from './globalize';
|
||||
import browser from './browser';
|
||||
import keyboardNavigation from './keyboardNavigation';
|
||||
import './mouseManager';
|
||||
import autoFocuser from '../components/autoFocuser';
|
||||
import { appHost } from '../components/apphost';
|
||||
import { getPlugins } from './settings/webSettings';
|
||||
import { pluginManager } from '../components/pluginManager';
|
||||
import packageManager from '../components/packageManager';
|
||||
import { appRouter, history } from '../components/appRouter';
|
||||
import '../elements/emby-button/emby-button';
|
||||
import './autoThemes';
|
||||
import './libraryMenu';
|
||||
import './routes';
|
||||
import '../components/themeMediaPlayer';
|
||||
import './autoBackdrops';
|
||||
import { pageClassOn, serverAddress } from '../utils/dashboard';
|
||||
import './screensavermanager';
|
||||
import './serverNotifications';
|
||||
import '../components/playback/playerSelectionMenu';
|
||||
import '../legacy/domParserTextHtml';
|
||||
import '../legacy/focusPreventScroll';
|
||||
import '../legacy/htmlMediaElement';
|
||||
import '../legacy/vendorStyles';
|
||||
import SyncPlay from '../components/syncPlay/core';
|
||||
import { playbackManager } from '../components/playback/playbackmanager';
|
||||
import SyncPlayNoActivePlayer from '../components/syncPlay/ui/players/NoActivePlayer';
|
||||
import SyncPlayHtmlVideoPlayer from '../components/syncPlay/ui/players/HtmlVideoPlayer';
|
||||
import SyncPlayHtmlAudioPlayer from '../components/syncPlay/ui/players/HtmlAudioPlayer';
|
||||
import { currentSettings } from './settings/userSettings';
|
||||
import taskButton from './taskbutton';
|
||||
import { HistoryRouter } from '../components/HistoryRouter.tsx';
|
||||
import AppRoutes from '../routes/index.tsx';
|
||||
import ServerConnections from './components/ServerConnections';
|
||||
import globalize from './scripts/globalize';
|
||||
import browser from './scripts/browser';
|
||||
import keyboardNavigation from './scripts/keyboardNavigation';
|
||||
import './scripts/mouseManager';
|
||||
import autoFocuser from './components/autoFocuser';
|
||||
import { appHost } from './components/apphost';
|
||||
import { getPlugins } from './scripts/settings/webSettings';
|
||||
import { pluginManager } from './components/pluginManager';
|
||||
import packageManager from './components/packageManager';
|
||||
import { appRouter, history } from './components/appRouter';
|
||||
import './elements/emby-button/emby-button';
|
||||
import './scripts/autoThemes';
|
||||
import './scripts/libraryMenu';
|
||||
import './scripts/routes';
|
||||
import './components/themeMediaPlayer';
|
||||
import './scripts/autoBackdrops';
|
||||
import { pageClassOn, serverAddress } from './utils/dashboard';
|
||||
import './scripts/screensavermanager';
|
||||
import './scripts/serverNotifications';
|
||||
import './components/playback/playerSelectionMenu';
|
||||
import './legacy/domParserTextHtml';
|
||||
import './legacy/focusPreventScroll';
|
||||
import './legacy/htmlMediaElement';
|
||||
import './legacy/vendorStyles';
|
||||
import SyncPlay from './components/syncPlay/core';
|
||||
import { playbackManager } from './components/playback/playbackmanager';
|
||||
import SyncPlayNoActivePlayer from './components/syncPlay/ui/players/NoActivePlayer';
|
||||
import SyncPlayHtmlVideoPlayer from './components/syncPlay/ui/players/HtmlVideoPlayer';
|
||||
import SyncPlayHtmlAudioPlayer from './components/syncPlay/ui/players/HtmlAudioPlayer';
|
||||
import { currentSettings } from './scripts/settings/userSettings';
|
||||
import taskButton from './scripts/taskbutton';
|
||||
import { HistoryRouter } from './components/HistoryRouter.tsx';
|
||||
import AppRoutes from './routes/index.tsx';
|
||||
|
||||
function loadCoreDictionary() {
|
||||
const languages = ['af', 'ar', 'be-by', 'bg-bg', 'bn_bd', 'ca', 'cs', 'cy', 'da', 'de', 'el', 'en-gb', 'en-us', 'eo', 'es', 'es-419', 'es-ar', 'es_do', 'es-mx', 'et', 'fa', 'fi', 'fil', 'fr', 'fr-ca', 'gl', 'gsw', 'he', 'hi-in', 'hr', 'hu', 'id', 'it', 'ja', 'kk', 'ko', 'lt-lt', 'lv', 'mr', 'ms', 'nb', 'nl', 'nn', 'pl', 'pr', 'pt', 'pt-br', 'pt-pt', 'ro', 'ru', 'sk', 'sl-si', 'sq', 'sv', 'ta', 'th', 'tr', 'uk', 'ur_pk', 'vi', 'zh-cn', 'zh-hk', 'zh-tw'];
|
||||
|
@ -94,13 +94,13 @@ function onGlobalizeInit() {
|
|||
|
||||
if (browser.tv && !browser.android) {
|
||||
console.debug('using system fonts with explicit sizes');
|
||||
import('../assets/css/fonts.sized.scss');
|
||||
import('./assets/css/fonts.sized.scss');
|
||||
} else {
|
||||
console.debug('using default fonts');
|
||||
import('../assets/css/fonts.scss');
|
||||
import('./assets/css/fonts.scss');
|
||||
}
|
||||
|
||||
import('../assets/css/librarybrowser.scss');
|
||||
import('./assets/css/librarybrowser.scss');
|
||||
|
||||
loadPlugins().then(function () {
|
||||
initSyncPlay();
|
||||
|
@ -164,7 +164,7 @@ async function onAppReady() {
|
|||
console.debug('onAppReady: loading dependencies');
|
||||
|
||||
if (browser.iOS) {
|
||||
import('../assets/css/ios.scss');
|
||||
import('./assets/css/ios.scss');
|
||||
}
|
||||
|
||||
Events.on(appHost, 'resume', () => {
|
||||
|
@ -181,29 +181,29 @@ async function onAppReady() {
|
|||
);
|
||||
|
||||
if (!browser.tv && !browser.xboxOne && !browser.ps4) {
|
||||
import('../components/nowPlayingBar/nowPlayingBar');
|
||||
import('./components/nowPlayingBar/nowPlayingBar');
|
||||
}
|
||||
|
||||
if (appHost.supports('remotecontrol')) {
|
||||
import('../components/playback/playerSelectionMenu');
|
||||
import('../components/playback/remotecontrolautoplay');
|
||||
import('./components/playback/playerSelectionMenu');
|
||||
import('./components/playback/remotecontrolautoplay');
|
||||
}
|
||||
|
||||
if (!appHost.supports('physicalvolumecontrol') || browser.touch) {
|
||||
import('../components/playback/volumeosd');
|
||||
import('./components/playback/volumeosd');
|
||||
}
|
||||
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
if (navigator.mediaSession || window.NativeShell) {
|
||||
import('../components/playback/mediasession');
|
||||
import('./components/playback/mediasession');
|
||||
}
|
||||
|
||||
if (!browser.tv && !browser.xboxOne) {
|
||||
import('../components/playback/playbackorientation');
|
||||
import('./components/playback/playbackorientation');
|
||||
registerServiceWorker();
|
||||
|
||||
if (window.Notification) {
|
||||
import('../components/notifications/notifications');
|
||||
import('./components/notifications/notifications');
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@
|
|||
top: 0;
|
||||
bottom: 0;
|
||||
contain: strict;
|
||||
box-sizing: border-box;
|
||||
padding-left: env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
.touch-menu-la {
|
||||
|
|
|
@ -9,6 +9,7 @@ import Screenfull from 'screenfull';
|
|||
import TableOfContents from './tableOfContents';
|
||||
import dom from '../../scripts/dom';
|
||||
import { translateHtml } from '../../scripts/globalize';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
|
||||
|
@ -294,6 +295,12 @@ export class BookPlayer {
|
|||
this.currentSrc = downloadHref;
|
||||
this.rendition = rendition;
|
||||
|
||||
rendition.themes.register('dark', { 'body': { 'color': '#fff' } });
|
||||
|
||||
if (userSettings.theme(undefined) === 'dark' || userSettings.theme(undefined) === null) {
|
||||
rendition.themes.select('dark');
|
||||
}
|
||||
|
||||
return rendition.display().then(() => {
|
||||
const epubElem = document.querySelector('.epub-container');
|
||||
epubElem.style.opacity = '0';
|
||||
|
|
|
@ -42,6 +42,12 @@
|
|||
|
||||
#dialogToc {
|
||||
background-color: white;
|
||||
height: fit-content;
|
||||
width: fit-content;
|
||||
max-height: 80%;
|
||||
max-width: 60%;
|
||||
padding-right: 50px;
|
||||
padding-bottom: 15px;
|
||||
|
||||
.bookplayerButtonIcon {
|
||||
color: black;
|
||||
|
@ -49,5 +55,19 @@
|
|||
|
||||
.toc li {
|
||||
margin-bottom: 5px;
|
||||
|
||||
list-style-type: none;
|
||||
font-size: 120%;
|
||||
|
||||
a:link {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
color: #00a4dc;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,7 @@ import dialogHelper from '../../components/dialogHelper/dialogHelper';
|
|||
import keyboardnavigation from '../../scripts/keyboardNavigation';
|
||||
import { appRouter } from '../../components/appRouter';
|
||||
import ServerConnections from '../../components/ServerConnections';
|
||||
// eslint-disable-next-line import/named, import/namespace
|
||||
import { Swiper } from 'swiper/swiper-bundle.esm';
|
||||
import 'swiper/swiper-bundle.css';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
|
||||
import './style.scss';
|
||||
|
||||
|
@ -27,6 +25,9 @@ export class ComicsPlayer {
|
|||
this.currentPage = 0;
|
||||
this.pageCount = 0;
|
||||
|
||||
const mediaSourceId = options.items[0].Id;
|
||||
this.comicsPlayerSettings = userSettings.getComicsPlayerSettings(mediaSourceId);
|
||||
|
||||
const elem = this.createMediaElement();
|
||||
return this.setCurrentSrc(elem, options);
|
||||
}
|
||||
|
@ -40,6 +41,9 @@ export class ComicsPlayer {
|
|||
|
||||
Events.trigger(this, 'stopped', [stopInfo]);
|
||||
|
||||
const mediaSourceId = this.item.Id;
|
||||
userSettings.setComicsPlayerSettings(this.comicsPlayerSettings, mediaSourceId);
|
||||
|
||||
this.archiveSource?.release();
|
||||
|
||||
const elem = this.mediaElement;
|
||||
|
@ -87,6 +91,85 @@ export class ComicsPlayer {
|
|||
this.stop();
|
||||
}
|
||||
|
||||
onDirChanged = () => {
|
||||
let langDir = this.comicsPlayerSettings.langDir;
|
||||
|
||||
if (!langDir || langDir === 'ltr')
|
||||
langDir = 'rtl';
|
||||
else
|
||||
langDir = 'ltr';
|
||||
|
||||
this.changeLanguageDirection(langDir);
|
||||
|
||||
this.comicsPlayerSettings.langDir = langDir;
|
||||
};
|
||||
|
||||
changeLanguageDirection(langDir) {
|
||||
const currentPage = this.currentPage;
|
||||
|
||||
this.swiperInstance.changeLanguageDirection(langDir);
|
||||
|
||||
const prevIcon = langDir === 'ltr' ? 'arrow_circle_left' : 'arrow_circle_right';
|
||||
this.mediaElement.querySelector('.btnToggleLangDir > span').classList.remove(prevIcon);
|
||||
|
||||
const newIcon = langDir === 'ltr' ? 'arrow_circle_right' : 'arrow_circle_left';
|
||||
this.mediaElement.querySelector('.btnToggleLangDir > span').classList.add(newIcon);
|
||||
|
||||
const dirTitle = langDir === 'ltr' ? 'Right To Left' : 'Left To Right';
|
||||
this.mediaElement.querySelector('.btnToggleLangDir').title = dirTitle;
|
||||
|
||||
this.reload(currentPage);
|
||||
}
|
||||
|
||||
onViewChanged = () => {
|
||||
let view = this.comicsPlayerSettings.pagesPerView;
|
||||
|
||||
if (!view || view === 1)
|
||||
view = 2;
|
||||
else
|
||||
view = 1;
|
||||
|
||||
this.changeView(view);
|
||||
|
||||
this.comicsPlayerSettings.pagesPerView = view;
|
||||
};
|
||||
|
||||
changeView(view) {
|
||||
const currentPage = this.currentPage;
|
||||
|
||||
this.swiperInstance.params.slidesPerView = view;
|
||||
this.swiperInstance.params.slidesPerGroup = view;
|
||||
|
||||
const prevIcon = view === 1 ? 'devices_fold' : 'import_contacts';
|
||||
this.mediaElement.querySelector('.btnToggleView > span').classList.remove(prevIcon);
|
||||
|
||||
const newIcon = view === 1 ? 'import_contacts' : 'devices_fold';
|
||||
this.mediaElement.querySelector('.btnToggleView > span').classList.add(newIcon);
|
||||
|
||||
const viewTitle = view === 1 ? 'Double Page View' : 'Single Page View';
|
||||
this.mediaElement.querySelector('.btnToggleView').title = viewTitle;
|
||||
|
||||
this.reload(currentPage);
|
||||
}
|
||||
|
||||
reload(currentPage) {
|
||||
const effect = this.swiperInstance.params.effect;
|
||||
|
||||
this.swiperInstance.params.effect = 'none';
|
||||
this.swiperInstance.update();
|
||||
|
||||
this.swiperInstance.slideNext();
|
||||
this.swiperInstance.slidePrev();
|
||||
|
||||
if (this.currentPage != currentPage) {
|
||||
this.swiperInstance.slideTo(currentPage);
|
||||
this.swiperInstance.update();
|
||||
}
|
||||
|
||||
this.swiperInstance.params.effect = effect;
|
||||
this.swiperInstance.update();
|
||||
}
|
||||
|
||||
onWindowKeyUp(e) {
|
||||
const key = keyboardnavigation.getKeyName(e);
|
||||
switch (key) {
|
||||
|
@ -101,6 +184,8 @@ export class ComicsPlayer {
|
|||
|
||||
elem?.addEventListener('close', this.onDialogClosed, { once: true });
|
||||
elem?.querySelector('.btnExit').addEventListener('click', this.onDialogClosed, { once: true });
|
||||
elem?.querySelector('.btnToggleLangDir').addEventListener('click', this.onDirChanged);
|
||||
elem?.querySelector('.btnToggleView').addEventListener('click', this.onViewChanged);
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
|
@ -114,6 +199,8 @@ export class ComicsPlayer {
|
|||
|
||||
elem?.removeEventListener('close', this.onDialogClosed);
|
||||
elem?.querySelector('.btnExit').removeEventListener('click', this.onDialogClosed);
|
||||
elem?.querySelector('.btnToggleLangDir').removeEventListener('click', this.onDirChanged);
|
||||
elem?.querySelector('.btnToggleView').removeEventListener('click', this.onViewChanged);
|
||||
}
|
||||
|
||||
unbindEvents() {
|
||||
|
@ -139,18 +226,40 @@ export class ComicsPlayer {
|
|||
removeOnClose: true
|
||||
});
|
||||
|
||||
const viewIcon = this.comicsPlayerSettings.pagesPerView === 1 ? 'import_contacts' : 'devices_fold';
|
||||
const dirIcon = this.comicsPlayerSettings.langDir === 'ltr' ? 'arrow_circle_right' : 'arrow_circle_left';
|
||||
|
||||
elem.id = 'comicsPlayer';
|
||||
elem.classList.add('slideshowDialog');
|
||||
|
||||
elem.innerHTML = `<div class="slideshowSwiperContainer"><div class="swiper-wrapper"></div></div>
|
||||
<div class="actionButtons">
|
||||
<button is="paper-icon-button-light" class="autoSize btnExit" tabindex="-1"><span class="material-icons actionButtonIcon close" aria-hidden="true"></span></button>
|
||||
</div>`;
|
||||
elem.innerHTML = `<div dir=${this.comicsPlayerSettings.langDir} class="slideshowSwiperContainer">
|
||||
<div class="swiper-wrapper"></div>
|
||||
<div class="swiper-button-next actionButtonIcon"></div>
|
||||
<div class="swiper-button-prev actionButtonIcon"></div>
|
||||
<div class="swiper-pagination"></div>
|
||||
</div>
|
||||
<div class="actionButtons">
|
||||
<button is="paper-icon-button-light" class="autoSize btnToggleLangDir" tabindex="-1">
|
||||
<span class="material-icons actionButtonIcon ${dirIcon}" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button is="paper-icon-button-light" class="autoSize btnToggleView" tabindex="-1">
|
||||
<span class="material-icons actionButtonIcon ${viewIcon}" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button is="paper-icon-button-light" class="autoSize btnExit" tabindex="-1">
|
||||
<span class="material-icons actionButtonIcon close" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
dialogHelper.open(elem);
|
||||
}
|
||||
|
||||
this.mediaElement = elem;
|
||||
|
||||
const dirTitle = this.comicsPlayerSettings.langDir === 'ltr' ? 'Right To Left' : 'Left To Right';
|
||||
this.mediaElement.querySelector('.btnToggleLangDir').title = dirTitle;
|
||||
|
||||
const viewTitle = this.comicsPlayerSettings.pagesPerView === 1 ? 'Double Page View' : 'Single Page View';
|
||||
this.mediaElement.querySelector('.btnToggleView').title = viewTitle;
|
||||
|
||||
this.bindEvents();
|
||||
return elem;
|
||||
}
|
||||
|
@ -179,45 +288,61 @@ export class ComicsPlayer {
|
|||
const downloadUrl = apiClient.getItemDownloadUrl(item.Id);
|
||||
this.archiveSource = new ArchiveSource(downloadUrl);
|
||||
|
||||
return this.archiveSource.load().then(() => {
|
||||
loading.hide();
|
||||
//eslint-disable-next-line import/no-unresolved
|
||||
import('swiper/css/bundle');
|
||||
|
||||
this.pageCount = this.archiveSource.urls.length;
|
||||
this.currentPage = options.startPositionTicks / 10000 || 0;
|
||||
return this.archiveSource.load()
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
.then(() => import('swiper/bundle'))
|
||||
.then(({ Swiper }) => {
|
||||
loading.hide();
|
||||
|
||||
this.swiperInstance = new Swiper(elem.querySelector('.slideshowSwiperContainer'), {
|
||||
direction: 'horizontal',
|
||||
// loop is disabled due to the lack of Swiper support in virtual slides
|
||||
loop: false,
|
||||
zoom: {
|
||||
minRatio: 1,
|
||||
toggle: true,
|
||||
containerClass: 'slider-zoom-container'
|
||||
},
|
||||
autoplay: false,
|
||||
keyboard: {
|
||||
enabled: true
|
||||
},
|
||||
preloadImages: true,
|
||||
slidesPerView: 1,
|
||||
slidesPerColumn: 1,
|
||||
initialSlide: this.currentPage,
|
||||
// reduces memory consumption for large libraries while allowing preloading of images
|
||||
virtual: {
|
||||
slides: this.archiveSource.urls,
|
||||
cache: true,
|
||||
renderSlide: this.getImgFromUrl,
|
||||
addSlidesBefore: 1,
|
||||
addSlidesAfter: 1
|
||||
}
|
||||
this.pageCount = this.archiveSource.urls.length;
|
||||
this.currentPage = options.startPositionTicks / 10000 || 0;
|
||||
|
||||
this.swiperInstance = new Swiper(elem.querySelector('.slideshowSwiperContainer'), {
|
||||
direction: 'horizontal',
|
||||
// loop is disabled due to the lack of Swiper support in virtual slides
|
||||
loop: false,
|
||||
zoom: {
|
||||
minRatio: 1,
|
||||
toggle: true,
|
||||
containerClass: 'slider-zoom-container'
|
||||
},
|
||||
autoplay: false,
|
||||
keyboard: {
|
||||
enabled: true
|
||||
},
|
||||
preloadImages: true,
|
||||
slidesPerView: this.comicsPlayerSettings.pagesPerView,
|
||||
slidesPerGroup: this.comicsPlayerSettings.pagesPerView,
|
||||
slidesPerColumn: 1,
|
||||
initialSlide: this.currentPage,
|
||||
navigation: {
|
||||
nextEl: '.swiper-button-next',
|
||||
prevEl: '.swiper-button-prev'
|
||||
},
|
||||
pagination: {
|
||||
el: '.swiper-pagination',
|
||||
clickable: true,
|
||||
type: 'fraction'
|
||||
},
|
||||
// reduces memory consumption for large libraries while allowing preloading of images
|
||||
virtual: {
|
||||
slides: this.archiveSource.urls,
|
||||
cache: true,
|
||||
renderSlide: this.getImgFromUrl,
|
||||
addSlidesBefore: 1,
|
||||
addSlidesAfter: 1
|
||||
}
|
||||
});
|
||||
|
||||
// save current page ( a page is an image file inside the archive )
|
||||
this.swiperInstance.on('slideChange', () => {
|
||||
this.currentPage = this.swiperInstance.activeIndex;
|
||||
Events.trigger(this, 'pause');
|
||||
});
|
||||
});
|
||||
|
||||
// save current page ( a page is an image file inside the archive )
|
||||
this.swiperInstance.on('slideChange', () => {
|
||||
this.currentPage = this.swiperInstance.activeIndex;
|
||||
Events.trigger(this, 'pause');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getImgFromUrl(url) {
|
||||
|
|
|
@ -12,5 +12,31 @@
|
|||
|
||||
.swiper-slide-img {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.swiper-pagination {
|
||||
width: max-content;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
padding: 2px 5px 2px 5px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0%);
|
||||
text-shadow: 0 0 20px #fff;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
right: 0.5vh;
|
||||
top: 0.5vh;
|
||||
z-index: 1002;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.actionButtonIcon {
|
||||
color: #000;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1150,8 +1150,7 @@ function tryRemoveElement(elem) {
|
|||
return true;
|
||||
}
|
||||
|
||||
// This is unfortunate, but we're unable to remove the textTrack that gets added via addTextTrack
|
||||
if (browser.firefox || browser.web0s) {
|
||||
if (browser.web0s) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
background: #000 !important;
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.videoPlayerContainer-onTop {
|
||||
|
@ -58,6 +62,9 @@ video[controls]::-webkit-media-controls {
|
|||
right: 0;
|
||||
color: #fff;
|
||||
font-size: 170%;
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.videoSubtitlesInner {
|
||||
|
|
|
@ -7,7 +7,6 @@ import { appRouter } from '../../components/appRouter';
|
|||
import './style.scss';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import { Events } from 'jellyfin-apiclient';
|
||||
import { GlobalWorkerOptions, getDocument } from 'pdfjs-dist';
|
||||
|
||||
export class PdfPlayer {
|
||||
constructor() {
|
||||
|
@ -200,14 +199,14 @@ export class PdfPlayer {
|
|||
const serverId = item.ServerId;
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
return import('pdfjs-dist').then(({ GlobalWorkerOptions, getDocument }) => {
|
||||
const downloadHref = apiClient.getItemDownloadUrl(item.Id);
|
||||
|
||||
this.bindEvents();
|
||||
GlobalWorkerOptions.workerSrc = appRouter.baseUrl() + '/libraries/pdf.worker.js';
|
||||
|
||||
const downloadTask = getDocument(downloadHref);
|
||||
downloadTask.promise.then(book => {
|
||||
return downloadTask.promise.then(book => {
|
||||
if (this.cancellationToken) return;
|
||||
this.book = book;
|
||||
this.loaded = true;
|
||||
|
@ -219,8 +218,6 @@ export class PdfPlayer {
|
|||
} else {
|
||||
this.loadPage(1);
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,21 +5,21 @@
|
|||
overflow: none;
|
||||
z-index: 100;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
#canvas {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
right: 0.5vh;
|
||||
top: 0.5vh;
|
||||
z-index: 1002;
|
||||
position: absolute;
|
||||
}
|
||||
.actionButtons {
|
||||
right: 0.5vh;
|
||||
top: 0.5vh;
|
||||
z-index: 1002;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.actionButtonIcon {
|
||||
color: black;
|
||||
opacity: 0.7;
|
||||
.actionButtonIcon {
|
||||
color: #000;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.youtubePlayerContainer.onTop {
|
||||
|
|
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